Главный секрет операторов match/case в пайтоне

Не так давно увидела свет версия языка пайтон 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 (используется чуть более старый синтаксис, но отличия не принципиальны).

@homm
26.10.2021 14:11 UTC
Первоисточник

Комментарии

@NeoCode
26.10.2021 09:41 UTC
+4

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

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

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

@bbc_69
26.10.2021 12:56 UTC
+4

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

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

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

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

26.10.2021 17:21 UTC
0

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

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

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

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


fn increment(n: &str) -> u32 {
    let n = n.parse().unwrap_or(0);
    n + 1
}
26.10.2021 18:59 UTC
0
НЛО прилетело и опубликовало эту надпись здесь
27.10.2021 08:40 UTC
0

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

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

29.10.2021 09:40 UTC
0

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

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

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

let mut a = 25;
a = 42;
27.10.2021 19:12 UTC
+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);
}
@middle
27.10.2021 21:48 UTC
+1

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

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

28.10.2021 11:43 UTC
+1

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

@iroln
26.10.2021 13:17 UTC
+5
match data_class:
    case User(username) as req:
      print(f"Granted to {req.name}")
    case _:
      print("deny")

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

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

@boojum
26.10.2021 15:10 UTC
+1

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

Не всегда.

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

@homm
26.10.2021 15:16 UTC
+2

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


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

27.10.2021 06:11 UTC
0

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

@event1
26.10.2021 15:48 UTC
+3

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

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

@DrMefistO
26.10.2021 16:52 UTC
+1

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

27.10.2021 06:18 UTC
0

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

27.10.2021 08:04 UTC
0

Тогда and?

@iroln
26.10.2021 16:14 UTC
+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


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

@funca
27.10.2021 05:35 UTC
+2

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

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

@MarkTanashchuk
28.10.2021 07:46 UTC
0

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

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

@piratarusso
27.10.2021 06:42 UTC
0

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

Питоновский вариант без 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 вступил в эпоху архитектурных излишеств. Началось всё с расширения функционала для более строгой поддержки типов.