Не так давно (а именно 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)
funca
25.10.2021 09:51+2UserRequest(name=str(name), access=1|2 as access, request=request):
Интересно, есть-ли возможность передать такую конструкцию в качестве аргумента функции (допустим в декоратор), или этот синтаксис доступен только внутри match/case?
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, )
Andy_U
25.10.2021 09:53+2Если 3.10, то я бы так написал:
match values := data_string.split("~"): ...
daniilgorbenko Автор
25.10.2021 10:10лично я не сторонник "моржового" оператора
но да, такая конструкция имеет место бытьAndy_U
25.10.2021 10:22А type annotations Вы тоже не любите?
daniilgorbenko Автор
25.10.2021 10:51+2аннотация типов - полезная штука, которая делает код более понятным и читаемым
как по мне, так использование := в условиях/циклах и т.д. нагружает эти конструкции
мне проще и понятнее объявить переменную до конструкции, чем использовать :=Andy_U
25.10.2021 11:23+1аннотация типов - полезная штука
Тогда почему не используете? У меня так PyCharm настроен ругаться на их отсутствие (как и docstring) ...
использование := в условиях/циклах и т.д. нагружает эти конструкции
мне проще и понятнее объявить переменную до конструкции, чем использовать :=Даже в таком случае?
for i in something: if (f := func(i)) > 0: print(log(f))
daniilgorbenko Автор
25.10.2021 11:42+1в основной работе аннотация типов - это неотъемлемая часть всего процесса
в статьях нет таких мест, где бы не было понятно, какой тип данных передаётся, так как либо переменные называются по типу данных, либо описывается в статье, что передается
на будущее постараюсь использовать аннотации, чтобы код в статьях был понятнее
____
читать такой синтаксис не трудно, но в сложных условиях мне не доставляет, что объявление переменной идет прямо внутри конструкции if, поэтому для себя я отметил этот оператор не несущим никакой пользы при написании кода
конечно, если данный оператор работает быстрее обычного присваивания f = func(i) до if, то я бы посмотрел в сторону :=, чтобы ускорить код, но пока я не проверял эту теорию
northzen
26.10.2021 12:49Аннотация типов удобна тем, что предоставляет определенный формат комментирования кода. И все.
Я не всегда пишу аннотации, особенно когда кручу эксперименты. Но все, что потом составляет продакшн-код -- аннотировано. Ровно как к каждой функции написан doc-string.
С моржовым оператором чуть сложнее. Он появился недавно, под его использование приходится чуток перешивать мозги, он когнитивно при первых порах использования скорее нагружает и при написании, и при чтении.
Match еще хуже. Менять значение Class(variable) в контексте match'а -- ИМХО плохая практика.
С type-hints обратная история. Они мгновенно становятся и понятными, и удобными.
technic93
25.10.2021 10:01-2Т.е вместо аннотации kwargs типами и datatype, когда компилятор сам подсказывает какие могут быть аргументы (как в том же TS) - в питоне сделали механизм для удобной обработки динамической лапши, чтобы дальше фигачить везде через any.
daniilgorbenko Автор
25.10.2021 10:19TS - имеет типизацию данных и имеет свой компилятор
Python - динамический язык с интерпретатором
думаю, даже не стоит сравнивать эти языки
если уж совсем всё будет плохо, то для Python есть Cyton
Lenod
25.10.2021 10:02-3Питонисты изобрели Switch{ case: }
daniilgorbenko Автор
25.10.2021 10:03+2это не совсем switch/case
pattern matching более усложненная конструкция
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. Код исправлен.
daniilgorbenko Автор
25.10.2021 10:54+3да, но словарь в качестве ключа не может использовать нехешируемые типы и более сложные логические конструкции
раньше приходилось изобретать велосипед, плодить кучу условий в if/else, использовать словари, создавать в классах отдельные функции для сравнения данных
сейчас стало попроще с этим
Andy_U
25.10.2021 11:58-3И все равно код "грязный". У Вас аргументы функций "x" не используются.
daniilgorbenko Автор
25.10.2021 12:00+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)
Может чуть менее красиво чем с паттерн матчингом, но уж точно не так ужасно как в изначальном примере.
Т.е. из того что я увидел паттерн матчинг не помогает в реальных задачах (во всяком случае из примеров я этого не увидел), а лишь позволяет замаскировать говнокод. Ведь от того что код стал лаконичнее, он не перестал быть говнокодом.
Andy_U
25.10.2021 11:42+4У вас в последних двух вариантах ошибка - default() вызывается всегда. Это к вопросу красивом, но неправильном коде ;)
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)
С точки зрения красоты вроде ничего не поменялось.
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)
daniilgorbenko Автор
25.10.2021 11:54каким бы хорошим не был проект, какие бы хорошие программисты не сидели за разработкой, всегда есть человеческий фактор ошибки, если с примером
values = data_string.split("~")
я полностью согласен, что лишнее было делать проверки на isinstance, то в случае, когда в функцию приходят какие-то параметры, особенно если эту функцию вызывает где-то другой человек, то стоит сделать лишнюю проверку, так как не все "гении" и могут допустить ошибку, передав не тот параметр, и тогда, в лучшем случае, тесты не пройдут, в худшем, все тесты пройдут, но в какой-то момент может лечь продукт
тут надо думать наперед, что придет какой-то программист N, прочитает документацию, забьет и решит, что в вашей функции уже есть все проверки и будет туда передавать, что попалоdmitrysvd
25.10.2021 21:39+1Для того, чтобы в функцию не прилетало непонятно что, мне кажется, лучше использовать type hints и mypy, чем каждый раз прописывать isinstance или match
Andy_U
25.10.2021 22:51+2Ну, чтобы mypy не выдал ошибок в strick mode, это я только раз осилил. Толку от mypy при работе с numpy - мало. Рекурсивных type hints так и не завезли. Фиг вам, а не произвольный json. А, еще иногда разная трактовка разными линтерами (mypy, PyCharm, уже не помню, какой-то плагин для Visual Studio). Я художник, я так вижу :)
bbc_69
26.10.2021 16:17А, еще иногда разная трактовка разными линтерами
Линтеры можно (и нужно) настроить.
Andy_U
26.10.2021 16:46Только вот иногда так приходится: https://github.com/python/mypy/issues/10552 В результате три вместо 2-х линтеров шагают в ногу, а mypy, может, когда и присоединится.
bbc_69
26.10.2021 17:35Если мне память не изменяет, то это pylint проверяет. Может pyflake тоже, не помню уже.
ИМХО, mypy и не должен это проверять - к проверке типов это отношения не имеет, это ближе к качеству кода.
Возвращаюсь к сообщению, не вижу проблемы в том, чтобы те правила, которые противоречивы в разных инструментах, проверялись только в одном из них. Собственно, это и называется настройка.
Andy_U
26.10.2021 17:58Вот нет идеала. Разные инструменты, увы, пропускают разные проблемы. А иногда сообщают о разных несуществующих проблемах. Как не настраивай опции проверки.
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
получается
technic93
26.10.2021 22:14Напомнило:
Заходит тестировщик в бар. Заказывает кружку пива. Заказывает 0 кружек пива. Заказывает 999999999 кружек пива. Заказывает -1 кружку пива. Заказывает ывфывв. Тут заходит реальный пользователь. Спрашивает, где здесь туалет. Бар сгорает в адском пламени, убивая всех вокруг.
morijndael
25.10.2021 13:01+4action, *data = data_string.split("~")
Это тоже pattern matching. Только если вдруг с паттерном не сошлось - вылетит исключение. match-case синтаксис просто позволяет удобно проверить соответствие нескольким паттернам
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
в иных случаях исключения тут не должно быть ни при каких обстоятельствах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)
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":
А тут код похож на тернарный оператор, однако это не так.
В общем забудьте весь синтаксис Питона, который вы знали, он вам не пригодится.
Andy_U
25.10.2021 12:56+1Хотя
|
это вообще-то бинарный или.Ну в 3.10 он уже и в type annotations не бинарный. Используется вместо Union. Да и переопределить его можно для своих классов, используя:
.__or__(self, other)
homm
25.10.2021 13:05+6Ну тут хоть какую-то логику можно понять, в конце концов не может быть бинарного представления типов.
Но попробуйте сходу сказать, что тут написано:
match permissions: case const.READ | const.WRITE: ...
Это полный бред, как по мне. В Python 4 придется это всё выпиливать.
funca
25.10.2021 14:03+1Вот тоже про это подумал. Хотя в питоне полисемия уже встречается. Например in в `for x in xs` и `if x in xs` делает разные вещи. Может и здесь они посчитали, что это pythonic way? Я давно перестал понимать по каким принципам развивается язык.
morijndael
25.10.2021 14:04Python 4 придется это всё выпиливать.
Мне кажется проще будет сделать новый язык с нуля. Даже если вдруг питон переделают, он станет несовместим со своими старыми модулями, а без модулей кому он вообще нужен?
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')
homm
25.10.2021 14:56match x:
case xif x & (a | b):
print(x)Andy_U
25.10.2021 15:07В данном примере, да, но если у вас 100 вариантов, где сравнение по маске одно, а остальные "ложатся" в match естественным образом? Или извратиться один раз с маской или писать 100 elif?
homm
25.10.2021 17:06100 elif? И много у вас такого кода?
Andy_U
25.10.2021 22:59-1Вот как раз сейчас пытаюсь собрать pyi из html-документации. Там каждый раз формат меняется, как новую серию функций добавляли. Т.е. 3-4 логических ветки при парсинге тэга - легко.
homm
26.10.2021 10:26Это понятно, а 100 elif?
Andy_U
26.10.2021 11:53См. мое первое сообщение:
если у вас 100 вариантов
откуда следует, что 100 - это, очевидно, гипербола. Сразу уточню - не математическая кривая, а литературный термин. Но что 3-4 ветки, что 100, одна фигня. Меньше символов, меньше возможности для ошибки. В обсуждении этой статьи пример найдете.
Впрочем, могу представить и большее количество веток. Например, в PyQt6 в классе QtCore.Qt.Key (наследника от IntEnum) порядка 450 членов :). Если реакции на клавиши простые, то ну нафиг кучу отдельных функций писать и через словарь их диспетчеризовать.
homm
26.10.2021 12:52откуда следует, что 100 — это, очевидно, гипербола
Очень плохое доказательство с точки зрения формальной логики. Не очевидно, и не следует.
Но что 3-4 ветки, что 100, одна фигня.
Не одна фигня, абсолютно разная фигня. В коде из 100 веток вы наделаете ошибок хоть с elif, хоть с pattern matching.
Меньше символов, меньше возможности для ошибки.
Пока что вы показали пример, где больше символов.
Например, в PyQt6 в классе QtCore.Qt.Key (наследника от IntEnum) порядка 450 членов :~)
Я не поленился и нашел о чем речь. Там 0 веток, это не код с ветвлением, а объявление enum. В коде никто бы не стал 100 веток делать.
Итого: имеет супер-редкий кейс, который вы даже показать не можете, который весьма специфичен из-за невозможности использовать даже константы, из-за которого в язык встроили другой язык запросов.
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)
iroln
25.10.2021 13:07+4if isinstance(value, str) and value == "load":
Зачем у вас везде проверки на isinstance? Оператор сравнения — это не неравенство, сравнивать можно что угодно с чем угодно, будет просто False, строка не равна числу и т.п. У вас просто лишняя проверка.
Надеюсь, никто в здравом уме не возвращает из кастомных
__eq__
значениеNotImplemented
iroln
26.10.2021 15:08Я решил всё же проверить возврат
NotImplemented
из магического метода сравнения. Это равносильно вернутьFalse
и не кидает исключениеTypeError: unsupported operand type(s)
. Поэтому смело возвращайте его если сравниваете тёплое с мягким! :)То есть ещё раз: Оператор сравнения в Python никогда не кидает исключение TypeError, а всегда возвращает True или False, что бы вы ни сравнивали.
Andy_U
26.10.2021 19:37Но вот если вы вернете Notimplemented и из __eq__ и из __ne__, то можете сильно удивить пользователя Вашего класса. Лучше уж NonImplementeErrror кидать.
iroln
26.10.2021 19:49__ne__
чаще всего не нужно реализовывать, оно работает через__eq__
.NotImplementedError
/TypeError
в операторах сравнения не надо кидать. Должна быть возможность сравнить несравнимое, что, очевидно False. Кстати дефолтная реализация как раз возвращает NotImplemented.
https://docs.python.org/3/reference/datamodel.html#object.__eq__
QtRoS
25.10.2021 13:09+7По мне самое грустное, что не сработает такой банальный код:
QUIT = 0 RESET = 1 command = 0 match command: case QUIT: quit() case RESET: reset()
Первый кейс всегда будет выполняться. Почему? Потому что там переменной QUIT присвоится значение command. Парапарапам.
dzaytsev91
25.10.2021 19:22+3Вместо решения актуальных проблем языка добавляют синтаксический сахар, кажется разработчики питона забыли свой же дзен — Явное лучше, чем неявное.
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-а ворвался бешеный принтер или продук-директором сделали Элопа
MentalBlood
Гм, так можно же просто
qoj
В питоне type hint'ы не используются интерпретатором. В такую функцию можно передать любой параметр.
MentalBlood
Круто. Мне, наивному, даже в голову не пришло, что оно может так "работать". Пошел читать доки
Mastermind-S
Есть ещё pydantic и его валидация аргументов, но, это всё не из коробки и вообще от лукавого :D
Nikoobraz
Во первых, разве такая запись не имеет "чисто рекомендательный" характер, и может быть легко нарушена?
Во вторых, если нужна гарантия того, что у нас принята строка - проще проверять тип и бросать исключение в первой же строке функции. А вот если гарантия такая не нужна, и функция может принимать аргументы разных типов и каждый обрабатывать отдельно, вот тут уже не поможет ни один ни второй вариант.
Правда сразу же напрашивается вопрос о том нужна ли функция именно в таком виде и не дробится ли она на несколько функций поменьше, но это уже другой разговор.
daniilgorbenko Автор
к сожалению, аннотация типов в питоне игнорируется интерпретатором
конечно, можно использовать какой-нибудь mypy для проверки типов перед запуском, но и это не даст 100% гарантии, что функция будет принимать только тип str
Andy_U
pip install pydantic
masai
В данном случае он тут не поможет, так как решает совершенно другие задачи.
Andy_U
Да, и какую же совершенно другую задачу решает validation decorator: https://pydantic-docs.helpmanual.io/usage/validation_decorator/?
burdin
Там он до сих пор в бете
Я делал похожий
https://github.com/EvgeniyBurdin/valdec
По умолчанию использует pydantic
dmitrysvd
Такова плата за гибкость языка