Не так давно (а именно 4 октября 2021 года) официально увидела свет юбилейная версия языка python, а именно версия 3.10. В ней было добавлено несколько изменений, а самым интересным (на мой взгляд) было введение pattern matching statement (оператор сопоставления с шаблонами). Как гласит официальное описание этого оператора в PEP622, разработчики в большей мере вдохновлялись наработками таких языков как: Scala, Erlang, Rust.
Для тех, кто еще не знаком с данным оператором и всей его красотой, предлагаю познакомиться с pattern matching в данной статье.
Немного о pattern matching
Как говорится в официальной документации (PEP622) в Python очень часто требуется проверять данные на соответствие типов, обращаться к данным по индексу и к этим же данным применять проверку на тип. Также зачастую приходится проверять не только тип данных, но и количество, что приводит к появлению огромного числа веток if/else с вызовом функций isinstance, len и обращению к элементам по индексу, ключу или атрибуту. Именно для упрощения работы и уменьшения is/else был введен новый оператор match/case.
Очень важно не путать pattern matching и switch/case, их главное отличие состоит в том, что pattern matching - это не просто оператор для сравнения некоторой переменной со значениями, это целый механизм для проверки данных, их распаковки и управления потоком выполнения.
Давай же рассмотрим несколько примеров, как данный оператор поможет упростить написание кода и сделать код более читаемым.
Примеры
Самый простой пример - это сравнение некоторой переменной со значениями (сначала рассмотрим как это было бы с if/else):
def load():
print("Загружаем")
def save():
print("Сохраняем")
def default():
print("Неизвестно как обработать")
def main(value):
if isinstance(value, str) and value == "load":
load()
elif isinstance(value, str) and value == "save":
save()
else:
default()
main("load")
>>> Загружаем
main("save")
>>> Сохраняем
main("hello")
>>> Неизвестно как обработать
Теперь с match/case:
def main(value):
match value:
case "load":
load()
case "save":
save()
case _:
default()
main("load")
>>> Загружаем
main("save")
>>> Сохраняем
main(5645)
>>> Неизвестно как обработать
Стало заметно меньше "and" и "==", получилось избавиться от лишних проверок на тип данных и код стал более понятным, однако это лишь самый простой пример, углубимся дальше. Допустим, откуда-то приходят данные в виде строки, которые записаны с разделителем “~”, и заранее известно, что если в данных было ровно 2 значения, то выполнить одно действие, если 3 значения, то иное действие:
def load(link):
print("Загружаем", link)
return "hello"
def save(link, filename):
data = load(link)
print("Сохраняем в", filename)
def default(values):
print("Неизвестно как эти данные обработать")
def main(data_string):
values = data_string.split("~")
if isinstance(values, (list, tuple)) and len(values) == 2 and values[0] == "load":
load(values[1])
elif isinstance(values, (list, tuple)) and len(values) == 3 and values[0] == "save":
save(values[1], values[2])
else:
default(values)
main("load~http://example.com/files/test.txt")
>>> Загружаем http://example.com/files/test.txt
main("save~http://example.com/files/test.txt~file.txt")
>>> Загружаем http://example.com/files/test.txt
>>> Сохраняем в file.txt
main("use~http://example.com/files/test.txt~file.txt")
>>> Неизвестно как эти данные обработать
main("save~http://example.com/files/test.txt~file.txt~file2.txt")
>>> Неизвестно как эти данные обработать
И с match/case:
def main(data_string):
values = data_string.split("~")
match values:
case "load", link:
load(link)
case "save", link, filename:
save(link, filename)
case _:
default(values)
main("load~http://example.com/files/test.txt")
>>> Загружаем http://example.com/files/test.txt
main("save~http://example.com/files/test.txt~file.txt")
>>> Загружаем http://example.com/files/test.txt
>>> Сохраняем в file.txt
main("use~http://example.com/files/test.txt~file.txt")
>>> Неизвестно как эти данные обработать
main("save~http://example.com/files/test.txt~file.txt~file2.txt")
>>> Неизвестно как эти данные обработать
Также, если есть необходимо скачать несколько файлов:
def load(links):
print("Загружаем", links)
return "hello"
def main(data_string):
values = data_string.split("~")
match values:
case "load", *links:
load(links)
case _:
default(values)
main("load~http://example.com/files/test.txt~http://example.com/files/test1.txt")
>>> Загружаем ['http://example.com/files/test.txt', 'http://example.com/files/test1.txt']
Match/case сам решает проблему с проверкой типов данных, с проверкой значений и их количеством, что позволяет упростить логику и увеличить читаемость кода. И очень удобно, что можно объявлять переменные и помещать в них значения прямо в ветке case без использования моржового оператора.
Рассмотрим пример, когда необходимо использовать оператор “или” в примере. Допустим, приходит запрос от пользователя с правами, и необходимо проверить, может ли данный пользователь выполнять текущее действие:
def main(data_string):
values = data_string.split("~")
match values:
case name, "1"|"2" as access, request:
print(f"Пользователь {name} получил доступ к функции {request} с правами {access}")
case _:
print("Неудача")
main("Daniil~2~load")
>>> Пользователь Daniil получил доступ к функции load с правами 2
main("Kris~0~save")
>>> Неудача
В таком случае символ “|” выступает в роли логического “или”, а значение прав доступа в переменную access было записано при помощи оператора "as". Разберем аналогичный пример, но в качестве аргумента будем рассматривать словарь:
def main(data_dict):
match data_dict:
case {"name": str(name), "access": 1|2 as access, "request": request}:
print(f"Пользователь {name} получил доступ к функции {request} с правами {access}")
case _:
print("Неудача")
main({"name": "Daniil", "access": 1, "request": "save"})
>>> Пользователь Daniil получил доступ к функции save с правами 1
main({"name": ["Daniil"], "access": 1, "request": "save"})
>>> Неудача
main({"name": "Kris", "access": 0, "request": "load"})
>>> Неудача
Как видим, довольно просто делать сравнение шаблонов для словарей. Пойдем еще дальше и создадим класс для хранения всех этих данных, затем попробуем организовать блок match/case для классов:
class UserRequest:
def __init__(self, name, access, request):
self.name = name
self.access = access
self.request = request
def main(data_class):
match data_class:
case UserRequest(name=str(name), access=1|2 as access, request=request):
print(f"Пользователь {name} получил доступ к функции {request} с правами {access}")
case _:
print("Неудача")
main(UserRequest("Daniil", 1, "delete"))
>>> Пользователь Daniil получил доступ к функции delete с правами 1
main(UserRequest(1234, 1, "delete"))
>>> Неудача
main(UserRequest("Kris", 0, "save"))
>>> Неудача
Чтобы еще упростить код и не писать названия атрибутов класса, которые сравниваются, можно прописать в классе атрибут match_args, благодаря которому case будет рассматривать значения, передаваемые при сравнивании в том порядке, в котором они записаны в match_args:
class UserRequest:
__match_args__= ('name', 'access', 'request')
def __init__(self, name, access, request):
self.name = name
self.access = access
self.request = request
def main(data_class):
match data_class:
case UserRequest(str(name), 1|2 as access, request):
print(f"Пользователь {name} получил доступ к функции {request} с правами {access}")
case _:
print("Неудача")
main(UserRequest("Daniil", 1, "delete"))
>>> Пользователь Daniil получил доступ к функции delete с правами 1
main(UserRequest(1234, 1, "delete"))
>>> Неудача
main(UserRequest("Kris", 0, "save"))
>>> Неудача
Так же стоить помнить, что при работе case UserRequest(str(name), access=2, request) оператор похож на создание нового экземпляра, однако это так не работает. Рассмотрим пример, подтверждающий это:
class UserRequest:
__match_args__= ('name', 'access', 'request')
def __init__(self, name, access, request):
print("Создан новый UserRequest")
self.name = name
self.access = access
self.request = request
def main(data_class):
match data_class:
case UserRequest(str(name), 1|2 as access, request):
print(f"Пользователь {name} получил доступ к функции {request} с правами {access}")
case _:
print("Неудача")
main(UserRequest("Daniil", 1, "delete"))
>>> Создан новый UserRequest
>>> Пользователь Daniil получил доступ к функции delete с правами 1
Как видно, вызов init произошел всего один раз, поэтому при работе case с классами не создаются новые новые экземпляры классов!
Более сложный и не такой тривиальный пример со сравнением некоторых данных, пришедших в оператор match/case:
class UserRequest:
__match_args__= ('name', 'access', 'request')
def __init__(self, name, access, request):
self.name = name
self.access = access
self.request = request
def main(data_class):
match data_class:
case UserRequest(_, _, request) if request["func"] == "delete" and request["directory"] == "main_folder":
print(f"Нельзя удалять файлы из {request['directory']}")
case UserRequest(str(name), 1|2 as access, request) if request["func"] != "delete":
print(f"Пользователь {name} получил доступ к файлу {request['file']} с правами {access} на {request['func']}")
case _:
print("Неудача")
main(UserRequest("Daniil", 1, {"func": "delete", "file": "test.txt", "directory": "main_folder"}))
>>> Нельзя удалять файлы из main_folder
main(UserRequest("Daniil", 1, {"func": "save", "file": "test.txt", "directory": "main_folder"}))
>>> Пользователь Daniil получил доступ к файлу test.txt с правами 1 на save
“_” позволяет не объявлять переменную под данные, а просто указывает, что на этом месте должны быть какие-то данные, но их можно не задействовать дальше. Также можно использовать оператор if для того, чтобы добавлять новые условия на проверку шаблона.
Заключение
Новый оператор pattern matching сильно упрощает жизнь во многих моментах и делает код еще более читаемым и лаконичным, что позволяет писать код быстрее и эффективнее не боясь за то, что вдруг где-то в коде не стоит проверка на тип элемента или на количество элементов в списке.
В данной статье были рассмотрены лишь несколько примеров для знакомства с оператором match/case, также существует возможность создавать более детальные и глубокие проверки в шаблонах, поэтому каждому стоит поэкспериментировать с данным оператором, так как, возможно, он позволит вам сделать код еще чище и проще.