Не так давно (а именно 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, также существует возможность создавать более детальные и глубокие проверки в шаблонах, поэтому каждому стоит поэкспериментировать с данным оператором, так как, возможно, он позволит вам сделать код еще чище и проще.
ссылка на оригинал статьи https://habr.com/ru/articles/585216/
Добавить комментарий