Сопоставление шаблонов с помощью match-case в Python

Язык программирования Python постоянно развивается, и каждая его новая версия приносит что-то интересное и новое. В версии Python 3.10 в PEP 634 и 636 для структурного сопоставления шаблонов был предложен оператор "match case".
Как и в других языках программирования, таких как C, Java, JavaScript и других, в Python есть оператор switch, который позволяет выполнять условные операции на основе значений выражения. Оператор switch позволяет нам выполнить блок кода, основываясь на значении одного выражения.
Однако в Python с самого начала не было такой возможности вместо оператора if-else, поэтому в версии 3.10 появился оператор match case, который функционирует аналогично оператору switch.

Оператор if-else

До появления в Python оператора match case оператор if-else использовался в основном для сопоставления шаблонов или выполнения условных операций.
Приведем пример использования оператора if-else для выполнения условной операции.

user = input("Enter the number: ")

if int(user) > 10:
    print("Greater than 10")

elif int(user) < 10:
    print("Less than 10")

else:
    print("It's sleeping time.")

В приведенном выше коде мы указали несколько условий, и то, какое условие будет истинным, зависит от значения единственного выражения, которое введет пользователь в консоли.
Здесь, если пользователь введет значение больше 10, то первое условие будет истинным и программа завершится, но если значение меньше 10, то программа проверит первое условие, если оно не будет истинным, то перейдет к следующему условию, если найдет совпадение, то выполнит условие и на этом завершится.

Enter the number: 10
It's sleeping time.
-----------------------
Enter the number: 9
Less than 10
-----------------------
Enter the number: 11
Greater than 10

Программа всегда будет искать совпадения и в итоге выполнит то условие, которое соответствует требованию пользователя.

Оператор match case

Недавно добавленный оператор match case функционирует аналогично оператору if-else, и если вы работали с другими языками программирования, такими как C, JavaScript и т.д., то он покажется вам похожим на оператор switch.
Оператор match принимает выражение и сравнивает его значение с блоками case, имеющими некоторые заданные условия. Посмотрите на следующий пример, демонстрирующий простейшую демонстрацию оператора match.

greet = "Hey"

match greet:
    case "Hey":
        print("Welcome to site.")
    case "Hello":
        print("Welcome Geeks.")
    case _:
        print("Matches anything.")

В приведенном выше коде мы создали переменную greet, в которой хранится значение "Hey", а затем создали блок match case. Сначала мы написали ключевое слово match и указали параметр, затем определили блоки case, в которых написали условия, и то условие, которое окажется истинным, будет выполнено.

Welcome to site.

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

Guard

Guard в операторе match - это не что иное, как использование оператора if в блоке case. Он является частью блока case и имеет следующий синтаксис:
case case_here if named_expression
Идея использования защитного условия или оператора if в блоке case заключается в том, что когда условие совпадает с условием case, программа не выполняет его сразу, а переходит к защитному условию или условию if, и если условие if истинно, то блок выполняется, иначе он не будет выполнен.
Другой логический поток case с защитой заключается в том, что если условие case не совпадает, то защита не оценивается, и программа переходит к следующему блоку case.

num = 10
match ("Hey", 1):
    case ("Hello", 10):
        print("Case 1 matched")
    case ("Hey", 1) if num == 1: # Case matched but guard fails
        print("Case 2 matched")
    case ("Hey", n):
        print(f"Case 3 matched, n={n}")
    case _:
        print("Match anything")

Если мы посмотрим на второй блок case, то он соответствует условию, которое мы определили в условии совпадения, после чего программа оценивает защитное условие, но здесь защитное условие ложно, следовательно, программа переходит к следующему блоку case.
В третьем блоке случай совпадает с условием и связывает n со значением 1, после чего программа выполняет третий блок случай.

Case 3 matched, n=1

Использование шаблона OR

Сопоставление шаблонов не всегда должно соответствовать одному условию, иногда мы можем захотеть соответствовать тому или иному условию, и в этом случае для указания альтернативных шаблонов можно использовать символ | (pipe) в блоке case.

match ("Hey", 1):
    case ("Hello", 10):
        print("Case 1 matched")
    case ("Hey", 10) | ("Heya", 1) | ("Hey", 1):
        print("Case 2 matched")
    case _:
        print("Match anything")

Вот простая демонстрация использования шаблона or, где во втором блоке case были определены три условия, и если любое из них соответствует условию, которое ищет программа, то блок case будет выполнен.

Case 2 matched

Сопоставление последовательностей

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

letter = "Hello"

match (letter[4]):
    case "o":
        print("Case 1 matched")
    case "a":
        print("Case 2 matched")
    case _:
        print("Yohoho")

В приведенном выше коде мы пытаемся проверить, присутствует ли определенный символ в заданной последовательности или нет. Этот код будет искать случай, когда определенный символ соответствует условию, и затем программа выполнит его.

Case 1 matched

Еще один момент: если мы хотим сопоставить один определенный символ из последовательности, а затем всю последовательность вместе, то в этом случае можно использовать * с именем переменной. При этом будут просто взяты все значения, присутствующие в данных или последовательности.

def inputdata(data):
    match data:
        case ["Hello", *other]:
            print(f"First letter is 'Hello' and "
                  f"Others are {other}")
        case [*word, "Welcome"]:
            print(f"Last word is 'Welcome' and the words "
                  f"before is {word}")
        case _:
            print("Yohoho")

inputdata(["Hello", "Geeks"])
inputdata(["Hey", "Geeks", "Welcome"])

В приведенном выше коде первый блок case будет соответствовать слову "Hello" из заданной последовательности, а затем соответствовать всем остальным элементам, то же самое относится и ко второму блоку case.

First letter is 'Hello' and Others are ['Geeks']
Last word is 'Welcome' and the words before is ['Hey', 'Geeks']

Классы Python в операторе совпадения

Мы также можем использовать классы Python для сопоставления шаблонов с помощью операторов match case. Это поможет нам легко управлять кодом, и код будет легко читаем в случае сложной структуры кода.

from dataclasses import dataclass

@dataclass
class Hero:
    real_name: str
    reel_name: str
    hero_name: str

def class_in_match(data):
    match data:
        case Hero(real_name, reel_name, hero_name):
            print(f"{real_name} plays the character of {reel_name} and "
                  f"hero name is {hero_name}.")

        case Hero("Tobey Maguire", "Peter Parker", "SpiderMan"):
            print("Tobey Maguire plays the character of Peter Parker and "
                  "hero name is SpiderMan.")

obj1 = Hero("RDJ", "Tony Stark", "IronMan")
obj2 = Hero("Tobey Maguire", "Peter Parker", "SpiderMan")

class_in_match(obj1)
class_in_match(obj2)

В приведенном выше коде мы использовали dataclass и создали наш класс Hero, который имеет три аргумента.
В следующем блоке кода мы создали функцию class_in_match, которая принимает данные в качестве аргумента, затем мы запустили лестницу match-case, где условием является совпадение данных, и в первом блоке case мы инстанцировали класс Hero и передали необходимые аргументы. Во втором блоке вместо аргументов мы передали значения аргументов.
Затем мы создали экземпляры obj1 и obj2 для класса Hero, передали значения и, наконец, вызвали функцию class_in_match и передали экземпляр класса.

RDJ plays the character of Tony Stark and hero name is IronMan.
Tobey Maguire plays the character of Peter Parker and hero name is SpiderMan.

Оператор match case с использованием словаря Python

Здесь мы рассмотрим, как можно создать оператор match-case с использованием словаря Python. Это может быть очень полезно, когда ваш проект находится в облаке и данные должны передаваться в формате json или словаря.

data = {
    "name": "GeekPython",
    "role": "Developer",
    "type": "Python"
}

match data["name"][4:]:
    case "GeekPython":
        print("Case 1 matched")
    case "Python":
        print("Case 2 matched")
    case "python":
        print("Case 3 matched")
    case _:
        print("Whatever")

В приведенном выше коде данные переменной представляют собой словарь и содержат некоторые пары ключ-значение, а затем мы создали условие, которое должно соответствовать значению имени ключа из данных словаря. Мы создали несколько блоков case, и тот из них, который удовлетворяет требуемому условию, будет выполнен.
Условие состоит в том, чтобы найти значение имени ключа, начиная с индекса 4, и второй случай полностью соответствует заданному условию.

Case 2 matched

Сравниванием скорость работы операторов match-case и if-else

Безусловно, данный оператор даёт определенное удобство при написании кода. Но как обстоит дело с производительностью оператора match-case?
Начнем с самого обыденного примера, когда приходится плодить переборы if/else - это необходимость сравнить переменную с какими-либо значениями. Создадим функцию для генерации случайных тестовых данных:

import random as rnd
def create_rnd_data():
    words = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0,
         'здесь', 'дом', 'да', 'потому', 'сторона',
         'какой-то', 'думать', 'сделать', 'страна',
         'жить', 'чем', 'мир', 'об', 'последний', 'случай',
         'голова', 'более', 'делать', 'что-то', 'смотреть',
         'ребенок', 'просто', 'конечно', 'сила', 'российский',
         'конец', 'перед', 'несколько']
    data = rnd.choices(words, k=500000)
    return data

Создадим несколько простых проверок данных, получаемых в функции create_rnd_data:

# простые проверки данных при помощи if/else
def test_if(data):
    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
# те же проверки при помощи match/case
def test_match(data):
    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

Самое интересное: при помощи модуля timeit проверим время, за которое в среднем будет выполняться каждая функция. Проведём 1000 испытаний каждой функции:

import timeit
# создаем случайные данные для теста
test_data = create_rnd_data()
# количество повторений
repeats = 1000
# считаем результаты
time_repeat_if = timeit.timeit("test_if(test_data)",
                               setup="from __main__ import test_if, test_data",
                               number=repeats)

time_repeat_match = timeit.timeit("test_match(test_data)",
                                  setup="from __main__ import test_match, test_data",
                                  number=repeats)
print("РЕЗУЛЬТАТ IF/ELSE:   ", time_repeat_if/repeats)
print("РЕЗУЛЬТАТ MATCH/CASE:", time_repeat_match/repeats)
>>> РЕЗУЛЬТАТ IF/ELSE:    0.1284590820000085
>>> РЕЗУЛЬТАТ MATCH/CASE: 0.4847222329999931

В 3.8 раза скорость выполнения match-case оказалась медленнее, чем скорость if-else. Но не будем загадывать наперед, может быть, match-case окажется быстрее при работе с более сложными структурами, например, при проверке словарей. С другими структурами, такими как словарями и классами скорость будет ещё ниже. Таким образом можно сделать вывод, что использование конструкции match-case для вычислений не является лучшим решением

Заключение

Оператор match-case был добавлен в Python v3.10, и при первом взгляде на него создается ощущение использования операторов switch, как в других языках программирования. Конечно, в Python есть оператор if-else, но добавление оператора match-case в Python является большим плюсом.
В этой статье мы рассмотрели использование оператора match-case и вместе с этим разобрали некоторые важные концепции оператора match-case с примерами кода.
Оператор match-case функционирует аналогично оператору if-else, и его синтаксис также в некоторой степени схож.

Прокрутить вверх