Match

It has been a long time since Python 3.10 was released. The most important and most anticipated was the introduction of the match / case operator (aka pattern matching).

However, not all developers from our community have entered this operator. This is evidenced even by the comments under the articles on Habré (article 1, article 2), which were devoted to match / case.

In my opinion, the new operator makes life easier for developers by taking over the work with data type checking or belonging to a particular class. But, as we all know, often the programmer has to pay for the cool features introduced into the language. In this article, I would like to highlight the performance of the match / case operator and compare it with a regular if / else.

Begin

The most common example of if / else iteration is the need to compare a variable with some values. Let’s generate a random data set and run each value through 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

Quite good for Python, now let’s write the same conditions only for match / case and compare the execution time:

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

Hmmm … The speed of match / case execution turned out to be 3 times slower than the speed of if / else. But let’s not think ahead, maybe match / case will turn out to be faster when working with more complex conditions, for example, when checking dictionaries.

Let’s create some list of dictionaries and check the performance of both operators:

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

AT 6 (six!) once the match / case operator is slower than if / else. It’s early to draw conclusions, let’s check the work with more complex structures, for example, create our own class and perform the same checks:

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 times …

Output

I did not check the match / case operator on lists, tuples, sets and other structures, I didn’t load it with complex conditions, because, I think, it’s obvious that we can draw a couple of conclusions:

  1. If you are doing any kind of computation in Python, or there are many loops / many checks in your application – NOT it is worth using the match / case operator, as it can slow down code execution several times;

  2. If the application contains checks for input forms, or if / else checks are too large, but checks are not performed very often (for example, when the user clicks on a button), then the match / case operator can be a good alternative to if / else, since it combines there are many good functions in itself (see previous articles in the introduction);

  3. Let’s hope and wait for the match / case optimization as Python 3.10 is young and only came out a month ago.

Similar Posts

Leave a Reply