Конструкция match/case. Первое знакомство

Курс по Python: https://stepik.org/course/100707

В Python версии 3.10 появилась новая конструкция под названием match/case, которая позволяет достаточно гибко анализировать переменные на соответствие шаблонов и для найденного совпадения выполнять некоторые заданные операции. Данный оператор имеет следующий синтаксис:

match <переменная>:
    case <шаблон_1>:
        операторы
    ...
    case < шаблон_n>:
        операторы
    case _:
        иначе (default)

На первый взгляд, вроде понятно, только что означает здесь слово «шаблон»? Это и некоторые другие моменты мы, как раз, и будем постепенно раскрывать на занятиях по оператору match/case.

Давайте вначале запишем эту конструкцию в очень простом варианте:

cmd = "top"
 
match cmd:
    case "top":
        print("вверх")
    case "left":
        print("влево")
    case "right":
        print("вправо")
    case _:  # wildcard
        print("другое")
 
print("проверки завершены")

Смотрите, здесь есть некая переменная cmd на строку «top», затем, она указывается после оператора match, внутри которого записаны блоки case. После каждого case указано значение в виде строки. Как только значение переменной оказывается равным значению после case, то выполняются операторы внутри этого блока case и далее выполнение программы переходит к следующей строчке после match – функции print("проверки завершены").

То есть, как только выполняется один из блоков case, оператор match завершает свою работу. Соответственно, внутри match обязательно должен быть записан хотя бы один оператор case. А общее число блоков case может быть произвольным. Конечно, всегда следует помнить о читабельности программы и удобстве ее последующего редактирования.

Внутри каждого блока case должен быть хотя бы один оператор. Например, запись вида:

match cmd:
    case "top":
    case "left":
    case "right":
        print("вверх, влево или вправо")
    case _:  # wildcard
        print("другое")

приведет к синтаксической ошибке. Если нужно в одном блоке case учесть сразу несколько констант, это можно сделать с помощью оператора ‘|’ следующим образом:

match cmd:
    case "top" | "left" | "right":
        print("вверх, влево или вправо")
    case _:  # wildcard
        print("другое")

Обратите внимание на последний блок case с символом подчеркивания. Это, так называемый, wildcard символ. В Python мы его обычно используем, если по синтаксису нужно прописать переменную, но в программе она нам не нужна. В частности здесь блок case с переменной _ будет отрабатывать всегда, если не отработали все предыдущие блоки. То есть, это некий аналог условного оператора else в if. Например, если переменная:

cmd = "top2"

то мы увидим сообщения:

другое
проверки завершены

Как раз здесь отработал последний блок case.

Те из вас, кто знаком с другими языками программирования, например, С++ или Java, возможно сейчас смотрят на эту программу и думают, ага, так это же аналог конструкции switch/case для проверки переменных на равенство заданных констант. Но не спешите с выводами. В действительности оператор match/case гораздо более гибкий, чем switch/case и гибкость определяется тем, что после case прописывается не просто константа, а шаблон проверки. Сейчас используется простейший шаблон, когда мы сравниваем переменную cmd на равенство указанным после case значениям. Но вариаций здесь гораздо больше и об это мы сейчас пойдет речь.

Шаблоны сравнений оператора case

Вместо конкретных значений после оператора case можно записывать переменные. Например, так:

cmd = "top"
 
match cmd:
    case command:
        print(f"команда: {command}")

В результате при выполнении блока case будет создана переменная command (если ранее ее не было в программе) и ссылаться на то же значение, что и переменная cmd, то есть, на строку «top». Поэтому при выполнении программы увидим на экране сообщение:

команда: top

То есть, фактически, в блоке case выполняется присваивание:

command = cmd

и выводится значение переменной command в консоль. Причем, этот блок case будет выполняться всегда при любых значениях переменной cmd. Потому что такой шаблон не делает никаких проверок, он просто создает переменную command и присваивает значение переменной, указанной после оператора match. Причем, если после этого case прописать еще какой-либо, например:

match cmd:
    case command:
        print(f"команда: {command}")
    case "top":
        print("top")

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

match cmd:
    case "top":
        print("top")
    case command:
        print(f"команда: {command}")

И если здесь вместо переменной command прописать нижнее подчеркивание, то получим уже знакомый нам блок default (отбойник), который перехватывает все, что не вошло в предыдущие блоки:

match cmd:
    case "top":
        print("top")
    case _:
        print(f"другая команда")

Шаблоны проверки типов

Давайте теперь усложним наш шаблон и в первом блоке case будем перехватывать все строковые команды, то есть, те, которые записаны в виде строки. Сделать это можно следующим образом:

cmd = "top"
 
match cmd:
    case str() as command:
        print(f"строковая команда: {command}")
    case _:  # wildcard
        print(f"другая команда")

Смотрите, после case записано str(). Мы привыкли такую запись воспринимать как создание или пустой строки или преобразование объекта в строку. Здесь же это не что иное, как проверка переменной cmd строковому типу. То есть, если переменная cmd является строкой, то только в этом случае выполнится этот блок case и будет создана переменная command, которая, как и ранее, ссылается на значение переменной cmd.

Обратите еще раз внимание на конструкцию «str() as command». Вначале идет проверка строкового типа переменной cmd, и если это так, то после ключевого слова as можно указать переменную, которая будет ссылаться на эту строку. Кстати, если нам нужна только проверка на строковый тип данных, то фрагмент «as command» можно не писать, например:

match cmd:
    case str():
        print(f"строковая команда")
    case _:  # wildcard
        print(f"другая команда")

Или, вместо «as command» можно написать:

match cmd:
    case str(command):
        print(f"строковая команда: {command}")
    case _:  # wildcard
        print(f"другая команда")

Но выглядит это несколько путанно, так как в Python мы привыкли воспринимать такую конструкцию «str(command)», как преобразование объекта в строку. Поэтому предпочтительнее, на мой взгляд, использовать ключевое слово as.

По аналогии можно проверять и другие типы данных, например:

cmd = 10
 
match cmd:
    case str() as command:
        print(f"строковая команда: {command}")
    case int() as command:
        print(f"целочисленная команда: {command}")
    case bool() as command:
        print(f"булева команда: {command}")
    case _:  # wildcard
        print(f"другая команда")

При выполнении этой программы будет выполнен второй блок case, так как переменная cmd целочисленная. Однако, если мы ей присвоим булево значение, например:

cmd = True

то снова отработает второй блок case, а не третий, как этого можно было бы ожидать. Почему так произошло? Я думаю, что многие из вас уже догадались, что проверка на тип данных в операторах case выполняется по принципу функции isinstance(), то есть, с учетом цепочки наследования типов. В частности, булевый тип наследуется от целочисленного, поэтому isinstance() и для целых чисел и для булевых значений возвращает True.

Разрешить эту проблему очень просто. Так как шаблоны в операторах case проверяются по порядку (сверху-вниз), то сначала следует поставить проверку на булевый тип, а потом на целочисленный:

match cmd:
    case str() as command:
        print(f"строковая команда: {command}")
    case bool() as command:
        print(f"булева команда: {command}")
    case int() as command:
        print(f"целочисленная команда: {command}")
    case _:  # wildcard
        print(f"другая команда")

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

Guard (защитник)

Давайте сделаем еще один шаг усложнения наших шаблонов и предположим, что для целочисленных команд дополнительно нужно проверять, чтобы они были в диапазоне от 0 до 9 включительно. Для этого после определения переменной command можно прописать ключевое слово if и указать дополнительное условие. В нашем случае оно будет выглядеть так:

match cmd:
    case str() as command:
        print(f"строковая команда: {command}")
    case bool() as command:
        print(f"булева команда: {command}")
    case int() as command if 0 <= command <= 9:
        print(f"целочисленная команда: {command}")
    case _:  # wildcard
        print(f"другая команда")

Теперь блок «case int() as command if 0 <= command <= 9» отработает только в том случае, если переменная cmd является целочисленной и значение переменной command (которая ссылается на переменную cmd) лежит в диапазоне [0; 9]. Причем, обратите внимание, оператор if будет отрабатывать после команды int(), то есть, после проверки на целый тип данных и после формирования переменной command. Поэтому если проверка дошла до if, то мы точно знаем, что определена переменная command и она имеет целочисленный тип. Проверка шаблона в case всегда выполняется строго слева-направо.

В конструкции match/case оператор if получил название guard (то есть, защитник). Условия в этом операторе можно прописывать по тем же правилам, что и в условных операторах if языка Python.

По аналогии можно прописать guard и для строковых команд, например, так:

cmd = "c_top"
 
match cmd:
    case str() as command if len(command) < 10 and command[0] == 'c':
        print(f"строковая команда: {command}")
    case bool() as command:
        print(f"булева команда: {command}")
    case int() as command if 0 <= command <= 9:
        print(f"целочисленная команда: {command}")
    case _:  # wildcard
        print(f"другая команда")

Если же какая-либо переменная может принимать несколько разных типов, например, int и float, то их можно записать через оператор ‘|’ следующим образом:

case int() | float() as command if 0 <= command <= 9:

И так далее, то есть здесь мы можем определять любые нужные нам условия в шаблонах оператора case.

Возможно, некоторым из вас на протяжении всего занятия не давал покоя вопрос, зачем все это нужно. У нас же в Python есть условные операторы if/elif/else и через них можно делать все те же самые проверки? Все верно. Конструкция match/case не дает нам ничего принципиально нового. Она лишь может упростить написание программного кода (при ее грамотном использовании), а также повысить читаемость текста программы. Это, наверное, главные причины, по которым данные операторы были добавлены в язык Python. Как мы дальше увидим, шаблоны в операторах case позволяют очень гибко, просто и удобно обрабатывать коллекции с данными, но об этом речь пойдет уже на следующем занятии.

Курс по Python: https://stepik.org/course/100707

Видео по теме