Как стать автором
Обновить

Комментарии 71

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

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


def main(value: str)

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

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

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

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

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

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

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

pip install 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, )

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

А 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.

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

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

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

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

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
в иных случаях исключения тут не должно быть ни при каких обстоятельствах

Этот конкретный пример не может, я говорил про вид 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)
В таком случае символ “|” выступает в роли логического “или”.

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



Так же стоить помнить, что при работе 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 придется это всё выпиливать.

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

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

Так в том же С++ (который я нафиг забыл уже) в 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 x if 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__

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

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

QUIT = 0
RESET = 1

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

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

Это всё полвека назад проходили в PL/I. Такое, наверное, сработает?

case (QUIT):

Вместо решения актуальных проблем языка добавляют синтаксический сахар, кажется разработчики питона забыли свой же дзен — Явное лучше, чем неявное.
  • в 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-а ворвался бешеный принтер или продук-директором сделали Элопа

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации