Комментарии 71
получилось избавиться от лишних проверок на тип данных
Гм, так можно же просто
def main(value: str)
В питоне type hint'ы не используются интерпретатором. В такую функцию можно передать любой параметр.
Во первых, разве такая запись не имеет "чисто рекомендательный" характер, и может быть легко нарушена?
Во вторых, если нужна гарантия того, что у нас принята строка - проще проверять тип и бросать исключение в первой же строке функции. А вот если гарантия такая не нужна, и функция может принимать аргументы разных типов и каждый обрабатывать отдельно, вот тут уже не поможет ни один ни второй вариант.
Правда сразу же напрашивается вопрос о том нужна ли функция именно в таком виде и не дробится ли она на несколько функций поменьше, но это уже другой разговор.
к сожалению, аннотация типов в питоне игнорируется интерпретатором
конечно, можно использовать какой-нибудь mypy для проверки типов перед запуском, но и это не даст 100% гарантии, что функция будет принимать только тип str
pip install pydantic
В данном случае он тут не поможет, так как решает совершенно другие задачи.
Да, и какую же совершенно другую задачу решает validation decorator: https://pydantic-docs.helpmanual.io/usage/validation_decorator/?
Там он до сих пор в бете
Я делал похожий
https://github.com/EvgeniyBurdin/valdec
По умолчанию использует pydantic
Такова плата за гибкость языка
UserRequest(name=str(name), access=1|2 as access, request=request):
Интересно, есть-ли возможность передать такую конструкцию в качестве аргумента функции (допустим в декоратор), или этот синтаксис доступен только внутри match/case?
в функцию такую конструкцию передать не получится
но в классах можно настраивать самому шаблон сопоставления
вот кусок кода из 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, )
Если 3.10, то я бы так написал:
match values := data_string.split("~"):
...
лично я не сторонник "моржового" оператора
но да, такая конструкция имеет место быть
А type annotations Вы тоже не любите?
аннотация типов - полезная штука, которая делает код более понятным и читаемым
как по мне, так использование := в условиях/циклах и т.д. нагружает эти конструкции
мне проще и понятнее объявить переменную до конструкции, чем использовать :=
аннотация типов - полезная штука
Тогда почему не используете? У меня так PyCharm настроен ругаться на их отсутствие (как и docstring) ...
использование := в условиях/циклах и т.д. нагружает эти конструкции
мне проще и понятнее объявить переменную до конструкции, чем использовать :=
Даже в таком случае?
for i in something:
if (f := func(i)) > 0:
print(log(f))
в основной работе аннотация типов - это неотъемлемая часть всего процесса
в статьях нет таких мест, где бы не было понятно, какой тип данных передаётся, так как либо переменные называются по типу данных, либо описывается в статье, что передается
на будущее постараюсь использовать аннотации, чтобы код в статьях был понятнее
____
читать такой синтаксис не трудно, но в сложных условиях мне не доставляет, что объявление переменной идет прямо внутри конструкции if, поэтому для себя я отметил этот оператор не несущим никакой пользы при написании кода
конечно, если данный оператор работает быстрее обычного присваивания f = func(i) до if, то я бы посмотрел в сторону :=, чтобы ускорить код, но пока я не проверял эту теорию
Ну вот смотрите, пример из валидатора класса Pydantic:
if all((
max_users := values.get('max_users', 0),
max_groups := values.get('max_groups', 0),
max_users <= max_groups
)):
raise ValueError(f'{max_users=} cannot be less than {max_groups=}')
Теперь представьте, что мы пишем лесенку из if-else, там везде используем конструкцию с values.get() и пишем отдельные сообщения об ошибке для каждого случая (потому что нельзя же сравнивать NoneType). Собственно, оно так и было до рефакторинга.
Явно же несомненный плюс моржа. Читаемость выросла, логика улучшилась, количество кода и отступов сократились.
Аннотация типов удобна тем, что предоставляет определенный формат комментирования кода. И все.
Я не всегда пишу аннотации, особенно когда кручу эксперименты. Но все, что потом составляет продакшн-код -- аннотировано. Ровно как к каждой функции написан doc-string.
С моржовым оператором чуть сложнее. Он появился недавно, под его использование приходится чуток перешивать мозги, он когнитивно при первых порах использования скорее нагружает и при написании, и при чтении.
Match еще хуже. Менять значение Class(variable) в контексте match'а -- ИМХО плохая практика.
С type-hints обратная история. Они мгновенно становятся и понятными, и удобными.
Т.е вместо аннотации kwargs типами и datatype, когда компилятор сам подсказывает какие могут быть аргументы (как в том же TS) - в питоне сделали механизм для удобной обработки динамической лапши, чтобы дальше фигачить везде через any.
Питонисты изобрели Switch{ case: }
Еще кстати такой вариант вспомнился. Через дефолт-словарь, в котором каждому ключу сопоставляется соответствующая функция, а дефолтное состояние задается пользовательской функцией. Тут тоже никаких лишних проверок нет, но в первый раз когда видишь - немного пугает.
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. Код исправлен.
да, но словарь в качестве ключа не может использовать нехешируемые типы и более сложные логические конструкции
раньше приходилось изобретать велосипед, плодить кучу условий в if/else, использовать словари, создавать в классах отдельные функции для сравнения данных
сейчас стало попроще с этим
И все равно код "грязный". У Вас аргументы функций "x" не используются.
Когда я вижу примеры паттерн матчинг в питоне, то у меня возникает множество вопросов.
Т.е. берут некий говнокод, который написан, мягко говоря не питон стиле, и пытаются облагородить этот говнокод не существующими средствами языка, а модификацией самого языка.
Использовать везде 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)
Может чуть менее красиво чем с паттерн матчингом, но уж точно не так ужасно как в изначальном примере.
Т.е. из того что я увидел паттерн матчинг не помогает в реальных задачах (во всяком случае из примеров я этого не увидел), а лишь позволяет замаскировать говнокод. Ведь от того что код стал лаконичнее, он не перестал быть говнокодом.
У вас в последних двух вариантах ошибка - default() вызывается всегда. Это к вопросу красивом, но неправильном коде ;)
Да, спасибо. Сейчас уже не исправить, конечно же там должно быть 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)
С точки зрения красоты вроде ничего не поменялось.
Я имел ввиду, что использование 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)
каким бы хорошим не был проект, какие бы хорошие программисты не сидели за разработкой, всегда есть человеческий фактор ошибки, если с примером values = data_string.split("~")
я полностью согласен, что лишнее было делать проверки на isinstance, то в случае, когда в функцию приходят какие-то параметры, особенно если эту функцию вызывает где-то другой человек, то стоит сделать лишнюю проверку, так как не все "гении" и могут допустить ошибку, передав не тот параметр, и тогда, в лучшем случае, тесты не пройдут, в худшем, все тесты пройдут, но в какой-то момент может лечь продукт
тут надо думать наперед, что придет какой-то программист N, прочитает документацию, забьет и решит, что в вашей функции уже есть все проверки и будет туда передавать, что попало
Для того, чтобы в функцию не прилетало непонятно что, мне кажется, лучше использовать type hints и mypy, чем каждый раз прописывать isinstance или match
Ну, чтобы mypy не выдал ошибок в strick mode, это я только раз осилил. Толку от mypy при работе с numpy - мало. Рекурсивных type hints так и не завезли. Фиг вам, а не произвольный json. А, еще иногда разная трактовка разными линтерами (mypy, PyCharm, уже не помню, какой-то плагин для Visual Studio). Я художник, я так вижу :)
А, еще иногда разная трактовка разными линтерами
Линтеры можно (и нужно) настроить.
Только вот иногда так приходится: https://github.com/python/mypy/issues/10552 В результате три вместо 2-х линтеров шагают в ногу, а mypy, может, когда и присоединится.
Если мне память не изменяет, то это pylint проверяет. Может pyflake тоже, не помню уже.
ИМХО, mypy и не должен это проверять - к проверке типов это отношения не имеет, это ближе к качеству кода.
Возвращаюсь к сообщению, не вижу проблемы в том, чтобы те правила, которые противоречивы в разных инструментах, проверялись только в одном из них. Собственно, это и называется настройка.
но в этом случае лучше (и нагляднее) явно сделать проверку типа:
def f(action, *args):
if not isinstance(action, str):
raise TypeErrorOrSubclass('action must be a string')
elif action == 'load':
# ....
гораздо компактнее и аккуратнее чем match
получается
Напомнило:
Заходит тестировщик в бар. Заказывает кружку пива. Заказывает 0 кружек пива. Заказывает 999999999 кружек пива. Заказывает -1 кружку пива. Заказывает ывфывв. Тут заходит реальный пользователь. Спрашивает, где здесь туалет. Бар сгорает в адском пламени, убивая всех вокруг.
action, *data = data_string.split("~")
Это тоже pattern matching. Только если вдруг с паттерном не сошлось - вылетит исключение. match-case синтаксис просто позволяет удобно проверить соответствие нескольким паттернам
подскажите как это может вызвать исключение?
a, *d = 'asd'.split('~')
print(f'a: {a!r}, d: {d!r}') # -> a: 'asd', d: []
я вижу только один вариант - если data_string
- не строка и не поддерживает метод split
в иных случаях исключения тут не должно быть ни при каких обстоятельствах
В таком случае символ “|” выступает в роли логического “или”.
Хотя |
это вообще-то бинарный или.

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

case UserRequest(_, _, request) if request["func"] == "delete" and request["directory"] == "main_folder":
А тут код похож на тернарный оператор, однако это не так.

В общем забудьте весь синтаксис Питона, который вы знали, он вам не пригодится.
Хотя
|
это вообще-то бинарный или.
Ну в 3.10 он уже и в type annotations не бинарный. Используется вместо Union. Да и переопределить его можно для своих классов, используя:
.__or__(self, other)
Ну тут хоть какую-то логику можно понять, в конце концов не может быть бинарного представления типов.
Но попробуйте сходу сказать, что тут написано:
match permissions:
case const.READ | const.WRITE:
...
Это полный бред, как по мне. В Python 4 придется это всё выпиливать.
Вот тоже про это подумал. Хотя в питоне полисемия уже встречается. Например in в `for x in xs` и `if x in xs` делает разные вещи. Может и здесь они посчитали, что это pythonic way? Я давно перестал понимать по каким принципам развивается язык.
Python 4 придется это всё выпиливать.
Мне кажется проще будет сделать новый язык с нуля. Даже если вдруг питон переделают, он станет несовместим со своими старыми модулями, а без модулей кому он вообще нужен?
Так в том же С++ (который я нафиг забыл уже) в case тоже можно было только константы (с точки зрения runtime) писать? И, хотя для работы с битовыми масками оператор match подходит не очень, вот так работает:
a = 1
b = 2
x = 3
match x:
case x if x & (a | b):
print(x)
case _:
print('ops')
match x:
case xif x & (a | b):
print(x)
В данном примере, да, но если у вас 100 вариантов, где сравнение по маске одно, а остальные "ложатся" в match естественным образом? Или извратиться один раз с маской или писать 100 elif?
100 elif? И много у вас такого кода?
Вот как раз сейчас пытаюсь собрать pyi из html-документации. Там каждый раз формат меняется, как новую серию функций добавляли. Т.е. 3-4 логических ветки при парсинге тэга - легко.
Это понятно, а 100 elif?
См. мое первое сообщение:
если у вас 100 вариантов
откуда следует, что 100 - это, очевидно, гипербола. Сразу уточню - не математическая кривая, а литературный термин. Но что 3-4 ветки, что 100, одна фигня. Меньше символов, меньше возможности для ошибки. В обсуждении этой статьи пример найдете.
Впрочем, могу представить и большее количество веток. Например, в PyQt6 в классе QtCore.Qt.Key (наследника от IntEnum) порядка 450 членов :). Если реакции на клавиши простые, то ну нафиг кучу отдельных функций писать и через словарь их диспетчеризовать.
откуда следует, что 100 — это, очевидно, гипербола
Очень плохое доказательство с точки зрения формальной логики. Не очевидно, и не следует.
Но что 3-4 ветки, что 100, одна фигня.
Не одна фигня, абсолютно разная фигня. В коде из 100 веток вы наделаете ошибок хоть с elif, хоть с pattern matching.
Меньше символов, меньше возможности для ошибки.
Пока что вы показали пример, где больше символов.
Например, в PyQt6 в классе QtCore.Qt.Key (наследника от IntEnum) порядка 450 членов :~)
Я не поленился и нашел о чем речь. Там 0 веток, это не код с ветвлением, а объявление enum. В коде никто бы не стал 100 веток делать.
Итого: имеет супер-редкий кейс, который вы даже показать не можете, который весьма специфичен из-за невозможности использовать даже константы, из-за которого в язык встроили другой язык запросов.
Не одна фигня, абсолютно разная фигня. В коде из 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)
if isinstance(value, str) and value == "load":
Зачем у вас везде проверки на isinstance? Оператор сравнения — это не неравенство, сравнивать можно что угодно с чем угодно, будет просто False, строка не равна числу и т.п. У вас просто лишняя проверка.
Надеюсь, никто в здравом уме не возвращает из кастомных __eq__
значение NotImplemented
Я решил всё же проверить возврат NotImplemented
из магического метода сравнения. Это равносильно вернуть False
и не кидает исключение TypeError: unsupported operand type(s)
. Поэтому смело возвращайте его если сравниваете тёплое с мягким! :)
То есть ещё раз: Оператор сравнения в Python никогда не кидает исключение TypeError, а всегда возвращает True или False, что бы вы ни сравнивали.
Но вот если вы вернете Notimplemented и из __eq__ и из __ne__, то можете сильно удивить пользователя Вашего класса. Лучше уж NonImplementeErrror кидать.
__ne__
чаще всего не нужно реализовывать, оно работает через __eq__
. NotImplementedError
/TypeError
в операторах сравнения не надо кидать. Должна быть возможность сравнить несравнимое, что, очевидно False. Кстати дефолтная реализация как раз возвращает NotImplemented.
https://docs.python.org/3/reference/datamodel.html#object.__eq__
По мне самое грустное, что не сработает такой банальный код:
QUIT = 0
RESET = 1
command = 0
match command:
case QUIT:
quit()
case RESET:
reset()
Первый кейс всегда будет выполняться. Почему? Потому что там переменной QUIT присвоится значение command. Парапарапам.
в
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-а ворвался бешеный принтер или продук-директором сделали Элопа
Немного примеров match/case в Python 3.10