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

Комментарии (69)


  1. MentalBlood
    25.10.2021 09:42

    получилось избавиться от лишних проверок на тип данных

    Гм, так можно же просто


    def main(value: str)


    1. qoj
      25.10.2021 10:01
      +8

      В питоне type hint'ы не используются интерпретатором. В такую функцию можно передать любой параметр.


      1. MentalBlood
        25.10.2021 10:20

        Круто. Мне, наивному, даже в голову не пришло, что оно может так "работать". Пошел читать доки


      1. Mastermind-S
        26.10.2021 18:44

        Есть ещё pydantic и его валидация аргументов, но, это всё не из коробки и вообще от лукавого :D


    1. Nikoobraz
      25.10.2021 10:05
      +4

      Во первых, разве такая запись не имеет "чисто рекомендательный" характер, и может быть легко нарушена?

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

      Правда сразу же напрашивается вопрос о том нужна ли функция именно в таком виде и не дробится ли она на несколько функций поменьше, но это уже другой разговор.


    1. daniilgorbenko Автор
      25.10.2021 10:05
      +5

      к сожалению, аннотация типов в питоне игнорируется интерпретатором
      конечно, можно использовать какой-нибудь mypy для проверки типов перед запуском, но и это не даст 100% гарантии, что функция будет принимать только тип str


      1. Andy_U
        25.10.2021 10:27
        +3

        pip install pydantic


        1. masai
          25.10.2021 16:51

          В данном случае он тут не поможет, так как решает совершенно другие задачи.


          1. Andy_U
            25.10.2021 16:58
            +4

            Да, и какую же совершенно другую задачу решает validation decorator: https://pydantic-docs.helpmanual.io/usage/validation_decorator/?


            1. burdin
              25.10.2021 21:05
              +1

              Там он до сих пор в бете

              Я делал похожий

              https://github.com/EvgeniyBurdin/valdec

              По умолчанию использует pydantic


      1. dmitrysvd
        25.10.2021 21:47

        Такова плата за гибкость языка


  1. funca
    25.10.2021 09:51
    +2

    UserRequest(name=str(name), access=1|2 as access, request=request):

    Интересно, есть-ли возможность передать такую конструкцию в качестве аргумента функции (допустим в декоратор), или этот синтаксис доступен только внутри match/case?


    1. daniilgorbenko Автор
      25.10.2021 10:14

      в функцию такую конструкцию передать не получится
      но в классах можно настраивать самому шаблон сопоставления
      вот кусок кода из PEP622:
      match expr:

      case BinaryOp(left=Number(value=x), op=op, right=Number(value=y)): ...

      from types import PatternObject

      BinaryOp.__match__(

      (),

      {

      "left": PatternObject(Number, (), {"value": ...}, -1, False),

      "op": ...,

      "right": PatternObject(Number, (), {"value": ...}, -1, False),

      },

      -1,

      False, )


  1. Andy_U
    25.10.2021 09:53
    +2

    Если 3.10, то я бы так написал:

    match values := data_string.split("~"):
        ...   


    1. daniilgorbenko Автор
      25.10.2021 10:10

      лично я не сторонник "моржового" оператора
      но да, такая конструкция имеет место быть


      1. Andy_U
        25.10.2021 10:22

        А type annotations Вы тоже не любите?


        1. daniilgorbenko Автор
          25.10.2021 10:51
          +2

          аннотация типов - полезная штука, которая делает код более понятным и читаемым
          как по мне, так использование := в условиях/циклах и т.д. нагружает эти конструкции
          мне проще и понятнее объявить переменную до конструкции, чем использовать :=


          1. Andy_U
            25.10.2021 11:23
            +1

            аннотация типов - полезная штука

            Тогда почему не используете? У меня так PyCharm настроен ругаться на их отсутствие (как и docstring) ...

            использование := в условиях/циклах и т.д. нагружает эти конструкции
            мне проще и понятнее объявить переменную до конструкции, чем использовать :=

            Даже в таком случае?

            for i in something:
              if (f := func(i)) > 0:
                print(log(f))


            1. daniilgorbenko Автор
              25.10.2021 11:42
              +1

              в основной работе аннотация типов - это неотъемлемая часть всего процесса
              в статьях нет таких мест, где бы не было понятно, какой тип данных передаётся, так как либо переменные называются по типу данных, либо описывается в статье, что передается
              на будущее постараюсь использовать аннотации, чтобы код в статьях был понятнее
              ____
              читать такой синтаксис не трудно, но в сложных условиях мне не доставляет, что объявление переменной идет прямо внутри конструкции if, поэтому для себя я отметил этот оператор не несущим никакой пользы при написании кода
              конечно, если данный оператор работает быстрее обычного присваивания f = func(i) до if, то я бы посмотрел в сторону :=, чтобы ускорить код, но пока я не проверял эту теорию


            1. northzen
              26.10.2021 12:49

              Аннотация типов удобна тем, что предоставляет определенный формат комментирования кода. И все.
              Я не всегда пишу аннотации, особенно когда кручу эксперименты. Но все, что потом составляет продакшн-код -- аннотировано. Ровно как к каждой функции написан doc-string.
              С моржовым оператором чуть сложнее. Он появился недавно, под его использование приходится чуток перешивать мозги, он когнитивно при первых порах использования скорее нагружает и при написании, и при чтении.
              Match еще хуже. Менять значение Class(variable) в контексте match'а -- ИМХО плохая практика.

              С type-hints обратная история. Они мгновенно становятся и понятными, и удобными.


  1. technic93
    25.10.2021 10:01
    -2

    Т.е вместо аннотации kwargs типами и datatype, когда компилятор сам подсказывает какие могут быть аргументы (как в том же TS) - в питоне сделали механизм для удобной обработки динамической лапши, чтобы дальше фигачить везде через any.


    1. daniilgorbenko Автор
      25.10.2021 10:19

      TS - имеет типизацию данных и имеет свой компилятор
      Python - динамический язык с интерпретатором
      думаю, даже не стоит сравнивать эти языки
      если уж совсем всё будет плохо, то для Python есть Cyton


  1. Lenod
    25.10.2021 10:02
    -3

    Питонисты изобрели Switch{ case: }


    1. daniilgorbenko Автор
      25.10.2021 10:03
      +2

      это не совсем switch/case
      pattern matching более усложненная конструкция


  1. Nikoobraz
    25.10.2021 10:36
    +1

    Еще кстати такой вариант вспомнился. Через дефолт-словарь, в котором каждому ключу сопоставляется соответствующая функция, а дефолтное состояние задается пользовательской функцией. Тут тоже никаких лишних проверок нет, но в первый раз когда видишь - немного пугает.

    def load(x):
        print("Загружаем")
    def save(x):
        print("Сохраняем")
    def default(x):
        print("Неизвестно как обработать")
    
    def main(value):
        dd = dict([("load", lambda: load(value)),
                    ("save", lambda: save(value))]
                    )
        return dd.get(value,lambda:default(value))()

    ________

    Edit: Пардон, наврал, используется обычный словарь, а не defaultdict, а дефолт-ветка получается через get. Код исправлен.


    1. daniilgorbenko Автор
      25.10.2021 10:54
      +3

      да, но словарь в качестве ключа не может использовать нехешируемые типы и более сложные логические конструкции
      раньше приходилось изобретать велосипед, плодить кучу условий в if/else, использовать словари, создавать в классах отдельные функции для сравнения данных
      сейчас стало попроще с этим


    1. Andy_U
      25.10.2021 11:58
      -3

      И все равно код "грязный". У Вас аргументы функций "x" не используются.


      1. daniilgorbenko Автор
        25.10.2021 12:00
        +1

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


  1. un1t
    25.10.2021 11:22
    +20

    Когда я вижу примеры паттерн матчинг в питоне, то у меня возникает множество вопросов.

    Т.е. берут некий говнокод, который написан, мягко говоря не питон стиле, и пытаются облагородить этот говнокод не существующими средствами языка, а модификацией самого языка.

    Использовать везде isinstance это не очень хорошая идея, в питоне же duck typing. Да иногда это может понадобиться, но скорее как исключение, а не общая практика.

    Возьмем пример 1.

    def main(value):
       if isinstance(value, str) and value == "load":
           load()
       elif isinstance(value, str) and value == "save":
           save()
       else:
           default()
    

    Зачем тут проверять isinstance(value, str) остается загадкой. Скорее всего незачем, просто чтобы оправдать добавление паттерн матчинга.

    Можно обойтись без паттерн матчинга.

    def main(value):
       if value == "load":
           load()
       elif value == "save":
           save()
       else:
           default()
    

    Возьмем другой пример:

    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)

    Нужна ли нам тут проверка isinstance(values, (list, tuple)) ? В данном примере точно нет, т.к. метод split возвращает list. Но что есть values это аргумент функции, и неизвестно что туда прилетает. Если у вас в одну функцию прилетает совсем непонятно что, ну list и tuple то ладно, но если там совсем рандомные объекты прилетают, то проблема в вашем коде, а не отсутствии паттерн матичнга.

    Можно переписать его так:

    def main(data_string):
        action, *data = data_string.split("~")
        if action == "load":
            load(*data)
        if action == "save":
            save(*data)
        default(action, *data)

    Да, мы не проверили длину. Но действительно ли это нам тут нужно? Из условий задачи это не ясно. Ну ладно, давайте проверим.

    def main(data_string):
        action, *data = data_string.split("~")
        if action == "load" and len(data) == 1:
            load(*data)
        if action == "save" and len(data) == 2:
            save(*data)
        default(action, *data)

    Может чуть менее красиво чем с паттерн матчингом, но уж точно не так ужасно как в изначальном примере.

    Т.е. из того что я увидел паттерн матчинг не помогает в реальных задачах (во всяком случае из примеров я этого не увидел), а лишь позволяет замаскировать говнокод. Ведь от того что код стал лаконичнее, он не перестал быть говнокодом.


    1. Andy_U
      25.10.2021 11:42
      +4

      У вас в последних двух вариантах ошибка - default() вызывается всегда. Это к вопросу красивом, но неправильном коде ;)


      1. un1t
        25.10.2021 11:55
        +5

        Да, спасибо. Сейчас уже не исправить, конечно же там должно быть elif и else

        def main(data_string):
            action, *data = data_string.split("~")
            if action == "load":
                load(*data)
            elif action == "save":
                save(*data)
            else:
                default(action, *data)
        def main(data_string):
            action, *data = data_string.split("~")
            if action == "load" and len(data) == 1:
                load(*data)
            elif action == "save" and len(data) == 2:
                save(*data)
            else:
                default(action, *data)

        С точки зрения красоты вроде ничего не поменялось.


        1. Andy_U
          25.10.2021 12:40
          +3

          Я имел ввиду, что использование if/elif/else может замаскировать логическую ошибку. "Else" удалили (оставив пустую строку), индент у вызова default() поменяли (а даже если и нет), вот все незаметно и сломалось. Оператор match более читабельный и устойчивый к подобным ошибкам. Особенно если код немного переписать:

          def main(data_string: str) -> None:
              match data_string.split("~"):
                  case 'load', arg2:
                      load(arg2)
                  case 'save', arg2, arg3:
                      save(arg2, arg3)
                  case error:
                      default(*error)


          1. un1t
            25.10.2021 17:58

            Соглашусь


    1. daniilgorbenko Автор
      25.10.2021 11:54

      каким бы хорошим не был проект, какие бы хорошие программисты не сидели за разработкой, всегда есть человеческий фактор ошибки, если с примером values = data_string.split("~") я полностью согласен, что лишнее было делать проверки на isinstance, то в случае, когда в функцию приходят какие-то параметры, особенно если эту функцию вызывает где-то другой человек, то стоит сделать лишнюю проверку, так как не все "гении" и могут допустить ошибку, передав не тот параметр, и тогда, в лучшем случае, тесты не пройдут, в худшем, все тесты пройдут, но в какой-то момент может лечь продукт
      тут надо думать наперед, что придет какой-то программист N, прочитает документацию, забьет и решит, что в вашей функции уже есть все проверки и будет туда передавать, что попало


      1. dmitrysvd
        25.10.2021 21:39
        +1

        Для того, чтобы в функцию не прилетало непонятно что, мне кажется, лучше использовать type hints и mypy, чем каждый раз прописывать isinstance или match


        1. Andy_U
          25.10.2021 22:51
          +2

          Ну, чтобы mypy не выдал ошибок в strick mode, это я только раз осилил. Толку от mypy при работе с numpy - мало. Рекурсивных type hints так и не завезли. Фиг вам, а не произвольный json. А, еще иногда разная трактовка разными линтерами (mypy, PyCharm, уже не помню, какой-то плагин для Visual Studio). Я художник, я так вижу :)


          1. bbc_69
            26.10.2021 16:17

            А, еще иногда разная трактовка разными линтерами

            Линтеры можно (и нужно) настроить.


            1. Andy_U
              26.10.2021 16:46

              Только вот иногда так приходится: https://github.com/python/mypy/issues/10552 В результате три вместо 2-х линтеров шагают в ногу, а mypy, может, когда и присоединится.


              1. bbc_69
                26.10.2021 17:35

                Если мне память не изменяет, то это pylint проверяет. Может pyflake тоже, не помню уже.

                ИМХО, mypy и не должен это проверять - к проверке типов это отношения не имеет, это ближе к качеству кода.

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


                1. Andy_U
                  26.10.2021 17:58

                  Вот нет идеала. Разные инструменты, увы, пропускают разные проблемы. А иногда сообщают о разных несуществующих проблемах. Как не настраивай опции проверки.


      1. mavriq
        25.10.2021 23:10
        +2

        но в этом случае лучше (и нагляднее) явно сделать проверку типа:

        def f(action, *args):
          if not isinstance(action, str):
            raise TypeErrorOrSubclass('action must be a string')
          elif action == 'load':
            # ....

        гораздо компактнее и аккуратнее чем match получается


      1. technic93
        26.10.2021 22:14

        Напомнило:

        Заходит тестировщик в бар. Заказывает кружку пива. Заказывает 0 кружек пива. Заказывает 999999999 кружек пива. Заказывает -1 кружку пива. Заказывает ывфывв. Тут заходит реальный пользователь. Спрашивает, где здесь туалет. Бар сгорает в адском пламени, убивая всех вокруг.


    1. morijndael
      25.10.2021 13:01
      +4

      action, *data = data_string.split("~")

      Это тоже pattern matching. Только если вдруг с паттерном не сошлось - вылетит исключение. match-case синтаксис просто позволяет удобно проверить соответствие нескольким паттернам


      1. mavriq
        25.10.2021 22:01
        +1

        подскажите как это может вызвать исключение?

        a, *d = 'asd'.split('~')
        print(f'a: {a!r}, d: {d!r}') # -> a: 'asd', d: []

        я вижу только один вариант - если data_string - не строка и не поддерживает метод split
        в иных случаях исключения тут не должно быть ни при каких обстоятельствах


        1. morijndael
          25.10.2021 22:08

          Этот конкретный пример не может, я говорил про вид pattern-matching-а в целом

          >>> a, b, *c = ''.split('test')
          Traceback (most recent call last):
            File "<stdin>", line 1, in <module>
          ValueError: not enough values to unpack (expected at least 2, got 1)


  1. homm
    25.10.2021 12:23
    +9

    В таком случае символ “|” выступает в роли логического “или”.

    Хотя | это вообще-то бинарный или.



    Так же стоить помнить, что при работе case UserRequest(str(name), access=2, request) оператор похож на создание нового экземпляра, однако это не так.


    case UserRequest(_, _, request) if request["func"] == "delete" and request["directory"] == "main_folder":

    А тут код похож на тернарный оператор, однако это не так.



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


    1. Andy_U
      25.10.2021 12:56
      +1

      Хотя | это вообще-то бинарный или.

      Ну в 3.10 он уже и в type annotations не бинарный. Используется вместо Union. Да и переопределить его можно для своих классов, используя:

      .__or__(self, other)


      1. homm
        25.10.2021 13:05
        +6

        Ну тут хоть какую-то логику можно понять, в конце концов не может быть бинарного представления типов.


        Но попробуйте сходу сказать, что тут написано:


        match permissions:
            case const.READ | const.WRITE:
                ...

        Это полный бред, как по мне. В Python 4 придется это всё выпиливать.


        1. funca
          25.10.2021 14:03
          +1

          Вот тоже про это подумал. Хотя в питоне полисемия уже встречается. Например in в `for x in xs` и `if x in xs` делает разные вещи. Может и здесь они посчитали, что это pythonic way? Я давно перестал понимать по каким принципам развивается язык.


        1. morijndael
          25.10.2021 14:04

          Python 4 придется это всё выпиливать.

          Мне кажется проще будет сделать новый язык с нуля. Даже если вдруг питон переделают, он станет несовместим со своими старыми модулями, а без модулей кому он вообще нужен?


          1. kx13
            13.11.2021 20:54

            У Python 4 есть все шансы повторить историю с Perl 6 :)


        1. Andy_U
          25.10.2021 14:49

          Так в том же С++ (который я нафиг забыл уже) в case тоже можно было только константы (с точки зрения runtime) писать? И, хотя для работы с битовыми масками оператор match подходит не очень, вот так работает:

              a = 1
              b = 2
          
              x = 3
          
              match x:
                  case x if x & (a | b):
                      print(x)
                  case _:
                      print('ops')
          


          1. homm
            25.10.2021 14:56

            match x:
                case x if x & (a | b):
                    print(x)


            1. Andy_U
              25.10.2021 15:07

              В данном примере, да, но если у вас 100 вариантов, где сравнение по маске одно, а остальные "ложатся" в match естественным образом? Или извратиться один раз с маской или писать 100 elif?


              1. homm
                25.10.2021 17:06

                100 elif? И много у вас такого кода?


                1. Andy_U
                  25.10.2021 22:59
                  -1

                  Вот как раз сейчас пытаюсь собрать pyi из html-документации. Там каждый раз формат меняется, как новую серию функций добавляли. Т.е. 3-4 логических ветки при парсинге тэга - легко.


                  1. homm
                    26.10.2021 10:26

                    Это понятно, а 100 elif?


                    1. Andy_U
                      26.10.2021 11:53

                      См. мое первое сообщение:

                      если у вас 100 вариантов

                      откуда следует, что 100 - это, очевидно, гипербола. Сразу уточню - не математическая кривая, а литературный термин. Но что 3-4 ветки, что 100, одна фигня. Меньше символов, меньше возможности для ошибки. В обсуждении этой статьи пример найдете.

                      Впрочем, могу представить и большее количество веток. Например, в PyQt6 в классе QtCore.Qt.Key (наследника от IntEnum) порядка 450 членов :). Если реакции на клавиши простые, то ну нафиг кучу отдельных функций писать и через словарь их диспетчеризовать.


                      1. homm
                        26.10.2021 12:52

                        откуда следует, что 100 — это, очевидно, гипербола

                        Очень плохое доказательство с точки зрения формальной логики. Не очевидно, и не следует.


                        Но что 3-4 ветки, что 100, одна фигня.

                        Не одна фигня, абсолютно разная фигня. В коде из 100 веток вы наделаете ошибок хоть с elif, хоть с pattern matching.


                        Меньше символов, меньше возможности для ошибки.

                        Пока что вы показали пример, где больше символов.


                        Например, в PyQt6 в классе QtCore.Qt.Key (наследника от IntEnum) порядка 450 членов :~)

                        Я не поленился и нашел о чем речь. Там 0 веток, это не код с ветвлением, а объявление enum. В коде никто бы не стал 100 веток делать.


                        Итого: имеет супер-редкий кейс, который вы даже показать не можете, который весьма специфичен из-за невозможности использовать даже константы, из-за которого в язык встроили другой язык запросов.


                      1. Andy_U
                        26.10.2021 13:44
                        +1

                        Не одна фигня, абсолютно разная фигня. В коде из 100 веток вы наделаете ошибок хоть с elif, хоть с pattern matching.

                        Но в данном обсуждении @un1t, конвертируя match обратно в if/elif/else ухитрился таки наделать таких ошибок даже в 3-4 ветках, которых было бы невозможно сделать в match.

                        Я не поленился и нашел нашел о чем речь. Там 0 веток, это не код с ветвлением, а объявление enum. В коде никто бы не стал 100 веток делать. Итого: имеет супер-редкий кейс, который вы даже показать не можете, который весьма специфичен из-за невозможности использовать даже константы

                        Начну с опровержения вашего второго утверждения: константы использовать можно, если их объединить в класс (наследник от IntEnum). Я уже приводил тут ссылку на stackoverflow: https://stackoverflow.com/questions/67525257/capture-makes-remaining-patterns-unreachable

                        Теперь про "супер-редкий кейс": У меня в коде ниже реально обрабатывается только нажатие на Enter, но можно и другие кнопки добавить, Я для примера Esc заблокировал... Естественно, про 100 веток речь не идет, но почему не добавить в пример ниже, например, 4 стрелочки. Вот уже 7 веток, почти 10. Что словарь делать, что match использовать. По строкам разница будет небольшая и неизвестно куда: в словаре будут или функции или лямбды однострочные, а так можно и пару строчек кода написать под case.

                        def keyPressEvent(self, e: QtGui.QKeyEvent) -> None:
                            match e.key():
                                case QtCore.Qt.Key.Key_Enter | QtCore.Qt.Key.Key_Return if self.run_button.isEnabled():
                                    self.run_something()
                                case QtCore.Qt.Key.Key_Escape:
                                    pass
                                case _:
                                    super(MyWindow, self).keyPressEvent(e)
                        


  1. iroln
    25.10.2021 13:07
    +4

    if isinstance(value, str) and value == "load":

    Зачем у вас везде проверки на isinstance? Оператор сравнения — это не неравенство, сравнивать можно что угодно с чем угодно, будет просто False, строка не равна числу и т.п. У вас просто лишняя проверка.


    Надеюсь, никто в здравом уме не возвращает из кастомных __eq__ значение NotImplemented


    1. iroln
      26.10.2021 15:08

      Я решил всё же проверить возврат NotImplemented из магического метода сравнения. Это равносильно вернуть False и не кидает исключение TypeError: unsupported operand type(s). Поэтому смело возвращайте его если сравниваете тёплое с мягким! :)


      То есть ещё раз: Оператор сравнения в Python никогда не кидает исключение TypeError, а всегда возвращает True или False, что бы вы ни сравнивали.


      1. Andy_U
        26.10.2021 19:37

        Но вот если вы вернете Notimplemented и из __eq__ и из __ne__, то можете сильно удивить пользователя Вашего класса. Лучше уж NonImplementeErrror кидать.


        1. iroln
          26.10.2021 19:49

          __ne__ чаще всего не нужно реализовывать, оно работает через __eq__. NotImplementedError/TypeError в операторах сравнения не надо кидать. Должна быть возможность сравнить несравнимое, что, очевидно False. Кстати дефолтная реализация как раз возвращает NotImplemented.
          https://docs.python.org/3/reference/datamodel.html#object.__eq__


          1. Andy_U
            26.10.2021 22:41

            У меня модуль, совместимый с Python 2.X/3.X Для второго питона, увы, надо.


  1. QtRoS
    25.10.2021 13:09
    +7

    По мне самое грустное, что не сработает такой банальный код:

    QUIT = 0
    RESET = 1
    
    command = 0
    match command:
        case QUIT:
            quit()
        case RESET:
            reset()

    Первый кейс всегда будет выполняться. Почему? Потому что там переменной QUIT присвоится значение command. Парапарапам.



  1. dzaytsev91
    25.10.2021 19:22
    +3

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


  1. mavriq
    25.10.2021 22:42
    +5

    • в match-выражениях задать значение по переменной нельзя:

    _cur_name = 'Masha'
    match ('Vasya', '1', 'test'):
        case _cur_name as name, "1"|"2" as access, request:
            print(f"ВНЕЗАПНО!! Пользователь {name} получил доступ к функции {request} с правами {access}")
        case _:
            print("Неудача")
    # ВНЕЗАПНО!! Пользователь Vasya получил доступ к функции test с правами 1
    • оператор |-"волшебный", и работает только в контексте match
      помнится - в 2.3 (кажется) ввели вместо except Exception, e формулировку except Exception_or_list_of_exception as e чтоб сделать более адекватной и тут ТАКОЕ

    • > Так же стоить помнить, что при работе case UserRequest...
      выглядит как утка, но ведет себя как черт-знает-что
      особенно в случае, если в конструкторе логика сложней чем просто определить пару свойств объекта

    Вангую - в будущих версиях языка, по многочисленным просьбам, эти нововведения выпилят как "не пользующиеся популярностью"

    Ощущение, будто в мейнтейнеры python-а ворвался бешеный принтер или продук-директором сделали Элопа