Не так давно увидела свет версия языка пайтон 3.10. В ней был добавлен pattern matching statement (оператор сопоставления с шаблонами). Как гласит официальное описание этого оператора в PEP622, разработчики в большей мере вдохновлялись наработками таких языков как: Scala, Erlang, Rust.

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

match values:
  case name, "1"|"2" as access:
    print(f"Access for {name} granted with {access}")
  case _:
    print("Deny")

Здесь выражение "1"|"2" as access очень похоже на то, что мы уже много раз видели в пайтоне, например в with или except. Слева — обычное выражение на пайтоне, справа название переменной, которой присвоится это выражение. Но присмотритесь внимательнее, "1"|"2" это бессмыслица, так как | — это оператор бинарного ИЛИ, которое очевидно не может существовать для строк. Здесь оператор | — часть механизма pattern matching, а не языка пайтон.

Ещё хуже придется, если флаги, который вы хотите проверить, как раз будут битовыми:

match values:
  case name, 0b001 | 0b010 as access:
    print(f"Access for {name} granted with {access}")
  case _:
    print("Deny")

0b001 | 0b010 должно означать «права на то и на другое» (например, на чтение и на запись), однако в pattern matching это не является выражением пайтона и пользователь получает доступ имея права только на что-то одно.

Другое ограничение match/case — вы не можете использовать никакие внешние переменные для сопоставления. Попробуйте вспомнить другое место в пайтоне, где вы можете написать "Vadim", но не можете get_username() или использовать локальную переменную. Я не вспомнил.

class User:
  __match_args__ = ('name', 'access')
  def __init__(self, name, access):
    self.name = name
    self.access = access

def match_name(data_class, username):
  match data_class:
    case User(username) as req:
      print(f"Granted to {req.name}")
    case _:
      print("deny")

match_name(User("Anna", 1), "Vadim")
Granted to Anna

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

Из всего изложенного можно сделать вывод:

Операторы match/case не являются частью языка пайтон, это встроенный язык со своим уникальным синтаксисом!

Можно представить, что создатели решили бы встроить не pattern matching, а например SQL.

users = [
  User("Anna", 0x011),
  User("Vadim", 0x010)
]

granted = SELECT users.name FROM users WHERE users.access IN (1, 2);

Вряд ли в этом случае можно было бы ожидать, что внутри такого выражения стало бы работать всё богатство синтаксиса пайтона.

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

PS. Более основательная критика pattern matching в том числе от контрибьютеров CPython: https://github.com/markshannon/pep622-critique (используется чуть более старый синтаксис, но отличия не принципиальны).

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


  1. NeoCode
    26.10.2021 12:41
    +4

    Странная тенденция - затаскивать синтаксис из функциональных языков. То что берут семантику - замечательно, полноценный паттерн матчинг давно пора ввести, и очень хорошо что он постепенно проникает во все языки. Но синтаксис... с этой дурацкой вертикальной чертой, в Rust ее затащили, теперь и в Python (хотя мне Python никогда не нравился за его форматно-несвбодный синтаксис).

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

    Кажется, из всех современных языков лучше всех сделано в Swift.


    1. bbc_69
      26.10.2021 15:56
      +4

      Идею с запятой поддерживаю.

      Вообще, есть ощущение, что после моржового оператора Питон идёт куда-то не туда.


      1. NeoCode
        26.10.2021 19:46
        +1

        «Моржовый оператор» лучше всего использован в Go.
        Вообще проблема Python (а также Ruby, Perl, PHP, JavaScript и т.д.) в том, что можно создавать переменные без явного объявления. Вот просто написать «x = 100», и у вас новая переменная. Или не новая, а ранее объявленная… Если имя переменной длинное, то одна случайная опечатка — и программа не работает. Или работает, но не совсем так как задумывалось… В Go придумали идеальное решение: отдельный оператор для объявления переменных.


        1. csshacker
          26.10.2021 19:56

          В js подобное прокатит разве что поиграться в нестрогом режиме. В реальной разработке всё будет объявляться const или в крайнем случае let.


        1. bbc_69
          26.10.2021 20:21

          Этому оператору присвоения сто лет в обед. :)

          Что касается динамической природы языка, так это достоинство и недостаток одновременно. Зато не надо придумывать название для той же сущности в другом формате или экономить на переменных. А для глупых ошибок есть линтеры.


          1. AnthonyMikh
            27.10.2021 20:06
            +2

            Зато не надо придумывать название для той же сущности в другом формате или экономить на переменных.

            Если в джаве так делать нельзя, то это не значит, что так делать в статически типизированных языках нельзя в принципе:


            fn increment(n: &str) -> u32 {
                let n = n.parse().unwrap_or(0);
                n + 1
            }


        1. tsukanov-as
          26.10.2021 21:59
          +6

          В Go придумали идеальное решение: отдельный оператор для объявления переменных.

          Ну не совсем идеальное.
          Можно взорваться случайно перекрыв переменную:

          i := 0
          for i < 10 {
            i := i + 1
            print(i)
          }

          Это надуманный пример только для иллюстрации сути, но я, например, наступал на подобные грабли.
          Идеально было бы, имхо, запретить перекрывать переменные.

          Еще, кстати, раздражает объявлять переменные через var отдельно в подобном коде:

          i := 0
          if i == 0 {
          	var err error
          	i, err = foo() 
          }

          Ко всему этому конечно привыкаешь, но идеальным это сложно назвать :)


          1. charypopper
            27.10.2021 11:40

            1 пример - затенение во многих яп есть, и умение кодить на go включает знание этой особенности. Тут кстати можно линтер наверно сделать на одинаковые имена в скоупе (не точно)

            2 пример - все от того что мы вводим короткие имена (читай ленимся), и вместо errFoo используем err - и удивляемся, что неудобно в некоторых местах


            1. freecoder_xx
              29.10.2021 12:40

              Например в Rust сложно представить, как затенить переменную случайно. Для затенения нужно новый байндинг делать с инструкцией let:

              let a = 25;
              let a = "test";

              А модифицируется имеющееся значение так:

              let mut a = 25;
              a = 42;


          1. NeoCode
            27.10.2021 22:12
            +1

            В C#10 сделали совмещенную деконструкцию в новые и существующие переменные. Может и в Go подтянутся

            string model = string.Empty;
            (model, var color) = car;

            и это кстати нужная фича для паттерн-матчинга частично определенными паттернами. Типа вот такого (на некотором гипотетическом C-like языке)
            match(car) {
            case "BMW X5", var color: foo(color); 
            case var model, Black: bar(model);
            }


    1. middle
      28.10.2021 00:48
      +1

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

      Кому он нужен, ваш мейнстримовый синтаксис.


      1. NeoCode
        28.10.2021 14:43
        +1

        Мне нужен. Это как математическая нотация: в разных разделах математики изучаются совершенно разные вопросы, но нотация на 90% общая. Если бы в каждом случае были бы свои обозначения для арифметических операций, функций и т.д., это был бы полный мрак...


  1. iroln
    26.10.2021 16:17
    +5

    match data_class:
        case User(username) as req:
          print(f"Granted to {req.name}")
        case _:
          print("deny")

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

    Это конечно вынос мозга. Где там что-то было про "явно лучше неявного"? Если запись User(username) это явное для сопоставления значения поля экземпляра, то я уже ни в чём не уверен.


  1. boojum
    26.10.2021 18:10
    +1

    так как | — это оператор бинарного ИЛИ

    Не всегда.

    Например dict1 | dict2 объединит два словаря.


    1. homm Автор
      26.10.2021 18:16
      +2

      Два словаря объединит оператор бинарного ИЛИ, у одного из словаря будет вызван метод __or__, который входит в "binary arithmetic operations".


      В случае с pattern matching это не так.


      1. dmitrysvd
        27.10.2021 09:11

        Такой же синтаксис у type hints в 3.10 вместо Union добавлен


  1. event1
    26.10.2021 18:48
    +3

    Использование вертикальной черты для разделения вариантов вполне логично, ведь она означает "или".

    По поводу скобок, вы, в принципе правы, но не вполне понятно, какой мог бы быть альтернативный вариант для биндинга? Угловые скобки?


    1. DrMefistO
      26.10.2021 19:52
      +1

      Ну, а в случае бинарной операции это совсем другое. Куда логичнее было бы использовать ранее используемое or, которое точно ИЛИ.


      1. dmitrysvd
        27.10.2021 09:18

        or не лучше. Для выражения 1 or 2 можно предположить, что будет проверяться только 1, так как or возвращает первое истинное выражение


        1. DrMefistO
          27.10.2021 11:04

          Тогда and?


  1. iroln
    26.10.2021 19:14
    +3

    Практически о том же говорит Larry Hastings:


    I dislike the syntax and semantics expressed in PEP 634. I see the match statement as a DSL contrived to look like Python, and to be used inside of Python, but with very different semantics. When you enter a PEP 634 match statement, the rules of the language change completely, and code that looks like existing Python code does something surprisingly very different. It also adds unprecedented new rules to Python, e.g. you can replace one expression with another in the exact same spot in your code, and if one has dots and the other doesn’t, the semantics of what the statement expresses changes completely. And it changes to yet a third set of semantics if you replace the expression with a single _.

    https://discuss.python.org/t/gauging-sentiment-on-pattern-matching/5770/21


    Я эту штуку в своём коде точно использовать не буду.


  1. funca
    27.10.2021 08:35
    +2

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

    В Python все фантазии влиятельных персонажей сразу вливаются в стандарт, а потом ошибки уже не исправить, т.к. нужно поддерживать обратную совместимость. До второго пришествия Гвидо эта беда касалась, в основном, лишь непомерно разросшейся стандартной библиотеки. Но теперь эта же участь постигла и сам язык.


    1. MarkTanashchuk
      28.10.2021 10:46

      по пути избавляясь от ошибок дизайна.

      JS - один из последних из списка современных языков в котором это заметно


  1. piratarusso
    27.10.2021 09:42

    Совершенно справедливое замечание. На мой взгляд введение таких конструкций просто портит язык.

    Питоновский вариант без case выглядит более предсказуемым и естественным

    if name in {0b001, 0b010}:print(f"Access for {name} granted with access")

    elif <Что там синтаксически питоновскoе возвращающее True>:
    .....

    else: print("Deny")

    В принципе вместо if-elif можно было бы использовать case . Но это по сути ничего не меняет в смысле написания кода.

    case name in {0b001, 0b010}: print(f"Access for {name} granted with access")

    case <Что там синтаксически питоновскoе возвращающее True>: .....

    else: print("Deny")

    Мне кажется, что python вступил в эпоху архитектурных излишеств. Началось всё с расширения функционала для более строгой поддержки типов.