Немного примеров match/case в Python 3.10 +33


Не так давно (а именно 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, также существует возможность создавать более детальные и глубокие проверки в шаблонах, поэтому каждому стоит поэкспериментировать с данным оператором, так как, возможно, он позволит вам сделать код еще чище и проще.




К сожалению, не доступен сервер mySQL