Прошло уже достаточно времени с момента релиза Python версии 3.10. Самым главным и самым ожидаемым было введение оператора match/case (он же pattern matching). 

Однако далеко не всем разработчикам из нашего комьюнити зашел данный оператор. Свидетельствуют этому даже комментарии под статьями на хабре (статья 1, статья 2), которые были посвящены match/case.

На мой взгляд, новый оператор упрощает жизнь разработчикам, принимая на себя работу с проверкой типов данных или принадлежность определенному классу. Но, как мы все знаем, зачастую за крутые фичи, введенные в язык, программисту приходится платить. В данной статье я хотел бы осветить тему производительности оператора match/case и сравнить его с обычным if/else.

Начинаем

Самый обыденный пример, когда приходится плодить переборы if/else - это необходимость сравнить переменную с какими-либо значениями. Сгенерируем случайный набор данных и прогоним по if/else каждое значение:

import time
import random as rnd
words = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
         'здесь', 'дом', 'да', 'потому', 'сторона',
         'какой-то', 'думать', 'сделать', 'страна',
         'жить', 'чем', 'мир', 'об', 'последний', 'случай',
         'голова', 'более', 'делать', 'что-то', 'смотреть',
         'ребенок', 'просто', 'конечно', 'сила', 'российский',
         'конец', 'перед', 'несколько']

data = rnd.choices(words, k=5000000)
s = time.time()
for word in data:
    if word in ['дом', 'думать', 'что-то', 'просто']:
        pass
    elif isinstance(word, int):
        pass
    elif isinstance(word, str) and len(word) > 3:
        pass
    elif isinstance(word, str) and word.startswith("д"):
        pass
    else:
        pass
print('РЕЗУЛЬТАТ IF/ELSE:', time.time()-s)
>>> РЕЗУЛЬТАТ IF/ELSE: 1.6040008068084717

Вполне неплохо для Python, теперь пропишем те же условия только для match/case и сравним время выполнения:

words = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
         'здесь', 'дом', 'да', 'потому', 'сторона',
         'какой-то', 'думать', 'сделать', 'страна',
         'жить', 'чем', 'мир', 'об', 'последний', 'случай',
         'голова', 'более', 'делать', 'что-то', 'смотреть',
         'ребенок', 'просто', 'конечно', 'сила', 'российский',
         'конец', 'перед', 'несколько']
    
data = rnd.choices(words, k=5000000)
s = time.time()
for word in data:
    if word in ['дом', 'думать', 'что-то', 'просто']:
        pass
    elif isinstance(word, int):
        pass
    elif isinstance(word, str) and len(word) > 3:
        pass
    elif isinstance(word, str) and word.startswith("д"):
        pass
    else:
        pass
print('РЕЗУЛЬТАТ IF/ELSE:', time.time()-s)

s = time.time()
for word in data:
    match word:
        case 'дом'|'думать'|'что-то'|'просто':
            pass
        case int(word):
            pass
        case str(word) if len(word) > 3:
            pass
        case str(word) if word.startswith("д"):
            pass
        case _:
            pass
print('РЕЗУЛЬТАТ MATCH/CASE:', time.time()-s)

>>> РЕЗУЛЬТАТ IF/ELSE: 1.6745779514312744
>>> РЕЗУЛЬТАТ MATCH/CASE: 4.577610015869141

Хммм… В 3 раза скорость выполнения match/case оказалась медленнее, чем скорость if/else. Но не будем загадывать наперед, может быть, match/case окажется быстрее при работе с более сложными условиями, например, при проверке словарей.

Создадим некоторый список словарей и проверим работоспособность обоих операторов:

names = ["phone", "TV", "PC", "car", "home", "case", "bird", "chicken",
            "dish", "float", "C++", "data", ""]
prices = [500, 100, 1400, 2000, 750, 3500, 5000, 120, 50, 4200]
goods = []
for i in range(5000000):
    name = names[i%len(names)]
    price = prices[i%len(prices)]
    goods.append({"name": name, "price": price})

s = time.time()
for element in goods:
    if element.get("name") in ["phone", "TV"] and isinstance(element.get("price"), int) and element.get("price") > 2000:
        pass
    elif element.get("name") == "case" and isinstance(element.get("price"), int) and element.get("price") <= 750:
        pass
    elif element.get("name") == "case" and isinstance(element.get("price"), int) and element.get("price") == 750:
        pass
    elif isinstance(element.get("name"), str) and element.get("name"):
        pass
    elif isinstance(element.get("price"), int) and element.get("price") > 1000:
        pass
    else:
        pass
print('РЕЗУЛЬТАТ IF/ELSE:', time.time()-s)

s = time.time()
for element in goods:
    match element:
        case {"name": "phone"|"TV", "price": int(price)} if price > 2000:
            pass
        case {"name": "case", "price": int(price)} if price <= 750:
            pass
        case {"name": "case", "price": 750}:
            pass
        case {"name": str(name), "price": _} if name:
            pass
        case {"name": _, "price": int(price)} if price > 1000:
            pass
        case _:
            pass
print('РЕЗУЛЬТАТ MATCH/CASE:', time.time()-s)

>>> РЕЗУЛЬТАТ IF/ELSE: 2.81404709815979
>>> РЕЗУЛЬТАТ MATCH/CASE: 16.747801780700684

В 6 (шесть!) раз оператор match/case уступил по скорости if/else. Рано делать выводы, проверим работу с более сложными структурами, например, создадим свой класс и выполним те же проверки:

class Goods:
    __match_args__= ('name', 'price')
    def __init__(self, name, price):
        self.name = name
        self.price = price
        
names = ["phone", "TV", "PC", "car", "home", "case", "bird", "chicken",
        "dish", "float", "C++", "data", ""]
prices = [500, 100, 1400, 2000, 750, 3500, 5000, 120, 50, 4200]
goods = []
for i in range(5000000):
    name = names[i%len(names)]
    price = prices[i%len(prices)]
    goods.append(Goods(name=name, price=price))

s = time.time()
for element in goods:
    if isinstance(element, Goods):
        if element.name in ["phone", "TV"] and isinstance(element.price, int) and element.price > 2000:
            pass
        elif element.name == "case" and isinstance(element.price, int) and element.price <= 750:
            pass
        elif element.name == "case" and isinstance(element.price, int) and element.price == 750:
            pass
        elif element.name:
            pass
        elif isinstance(element.price, int) and element.price > 1000:
            pass
        else:
            pass
print('РЕЗУЛЬТАТ IF/ELSE:', time.time()-s)

s = time.time()
for element in goods:
    match element:
        case Goods("phone"|"TV" as name, int(price)) if price > 2000:
            pass
        case Goods(name="case", price=int(price)) if price <= 750:
            pass
        case Goods(name="case", price=750):
            pass
        case Goods(str(name), _) if name:
            pass
        case Goods(_, price) if price > 1000:
            pass
        case _:
            pass
print('РЕЗУЛЬТАТ MATCH/CASE:', time.time()-s)

>>> РЕЗУЛЬТАТ IF/ELSE: 1.9059967994689941
>>> РЕЗУЛЬТАТ MATCH/CASE: 12.779330730438232

В 6.6 раз...

Вывод

Я не стал проверять оператор match/case на списках, кортежах, множествах и других структурах, не стал нагружать сложными условиями, так как, думаю, очевидно можно сделать пару выводов:

  1. Если вы занимаетесь каким-либо вычислениями на Python, или в вашем приложении много циклов/много проверок - НЕ стоит использовать оператор match/case, так как он может замедлить выполнение кода в несколько раз;

  2. Если в приложении есть проверки на формы ввода, либо проверки if/else слишком большие, но проверки выполняются не очень часто (например, при клике пользователя на кнопку), то оператор match/case может стать хорошей альтернативой if/else, так как он сочетает в себе много хороший функций (см предыдущие статьи во введении);

  3. Будем надеяться и ждать оптимизацию match/case, так как версия Python 3.10 молодая и вышла только месяц назад.

Комментарии (3)