Комментарии 153
Всё нижележащее - моё имхо
Хорошие рассуждения: вместо докстринги использовать хорошее имя функции.
Но продолжаем думать в этом направлении и приходим к чему?
хорошее именование переменных позволит выбросить монструозный тайп-хинтинг на помойку по той же логике, почему выбрасываем докстринг
всё равно тайпхинтинг не работает в рантайме
количество ошибок, которые позволяет обнаружить тайпхинтинг меньше того количества ошибок, которые позволят обнаружить тесты, содержащие столько же букв сколько потрачено на тайпхинтинг
За смешивание декораторов с объектно-ориентированным программированием где-то в аду для того, кто это придумал предусмотрен отдельный котёл. По мне, так это прямо образец "неправильно"
Я только из Ада. Там у них для пользователей класс декораторов котел задекорированный под сковородку.
А расскажите, что не так с декораторами и ООП, это особенность питона?
Я про питон мало знаю, но в C# и JS с декораторами в классах все классно.
Хорошие декораторы хороши, но проблема в том, что из-за отсутствия проверки типов перед запуском у нас одна функция что-то делает с аргументами другой функции и подсовывает её вместо оригинальной функции, так что когда что-то ломается, то программист точно знает, что что-то где-то не так, наверное.
В ООП поведение методов принято изменять при помощи штатного механизма - наследования..
В ФП поведение функций принято менять при помощи функций-обёрток (врапперы).
Декораторы - это мир ФП. Применение декораторов к ООП приводит к тому, что
декораторы почти всегда применяются к инстансу, а не классу
получается хак на саму парадигму программирования.
Давайте повсюду смешивать тёплое и мягкое и будет хорошо! Нет, этот принцип из разряда плохих советов, даже если очень многие или слишком многие ему следуют. Как-то так.
Еще есть аспектно-ориентированное программирование, которое вполне про декораторы в классах.
Да, но нет.
если например в Вики почитать описание зачем нужно АОП, то там написано "для таких вещей как обеспечение сквозной функциональности". Например доступ к системе логгирования отовсюду итп.
и тут наверное декораторы в классах можно было бы как-то применять (хотя и не очень они туда ложатся)
но я что хотел сказать: декораторы обычно применяются не для АОП, а как ФП-хак над ООП. Например, популярная в питон FastAPI
Скопирую кусок из документации сюда:
from typing import Union
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}
Что мы тут видим? а мы тут видим вместо создания наследника с определением нужного поведения - извращения над инстансом класса.
К чему это приводит?
Человек создаёт два сервера API с разным набором методов
При помощи декораторов наделяет два разных инстанса разным поведением
Затем когда он хочет для первого и второго сделать что-то специфичное, то
вернуться к наследованию ему крайне сложно (кода уже дофига написано)
поэтому приходится извращаться вводя дополнительные хаки
Если бы декораторы использовались в массе своей для ФП и даже для АОП, то я бы против них ничего не имел, но увы, чаще всего декораторы применяются не для этого
Ни над каким инстансом класса никаких извещений здесь не происходит, ведь меняется поведение функций, а не инстанса класса FastAPI. Ну это так, скорее придирка. А вот проблемы, честно говоря, не понятны, слишком уж высокая степень абстракции: что, например, такого специфичного можно захотеть сделать, чтобы стало нестерпимо больно от отсутствия наследования?
Охотно верю, что вы приедете пример, но, подозреваю, что вероятность такой ситуации нивелируется удобством и гибкостью, который даёт описанный выше подход, и это общее правило (ООП тоже не безупречно). Существуй универсальный удобный и лишённый изъянов способ сделать все, что угодно - все бы только им и пользовались.
Ни над каким инстансом класса никаких извещений здесь не происходит, ведь меняется поведение функций, а не инстанса класса FastAPI.
Инстанс создаётся в строке
app = FastAPI()
Затем, используя декораторы проставляются обработчики запросов.
Охотно верю, что вы приедете пример,но, подозреваю, что вероятность такой ситуации нивелируется удобством и гибкостью, который даёт описанный выше подход, и это общее правило (ООП тоже не безупречно). Существуй универсальный удобный и лишённый изъянов способ сделать все, что угодно - все бы только им и пользовались.
Я говорю о том, что заколачивание гвоздей микроскопами, даже если оно почему-то в конкретном случае удобнее - есть зло в чистой форме
То-то и оно, что здесь вполне себе заколачивание гвоздей молотком. Проблемы могут возникнуть от неправильной эксплуатации.
Повторюсь, над объектом FastAPI глумление не имеет места быть.
Ну вот представьте, что вместо декораторов обработчики запросов проставляются более прямым способом:
def read_root():
return {"Hello": "World"}
def read_item(item_id: int, q: Union[str, None] = None):
return {"item_id": item_id, "q": q}
app = AnotherFastAPI()
app.create_get_route("/", read_root)
app.create_get_route("/items/{item_id}", read_item)
Или вообще представим чистый ООП-способ:
class ReadRootRoute(RouteBase):
def __call__(self):
return {"Hello": "World"}
class ReadItemRoute(RouteBase):
def __call__(self, item_id, q):
return {"item_id": item_id, "q": q}
app = AnotherAnotherFastAPI()
app.create_get_route("/", ReadRootRoute)
app.create_get_route("/items/{item_id}", ReadItemRoute)
Стало ли лучше? Сомневаюсь.
Кстати, где вы тут вообще увидели применение декораторов к методам?
Не кидайте в меня помидорами, но первый вариант таки лучше, имхо. Сразу в одном месте определено, куда пойдёт юзер в зависимости от url
, вместо того, чтобы ходить по функциям и в декораторе смотреть их путь.
Но при написании каждой конечной точки надо править сразу два места в программе. Причём в этих двух местах надо согласовывать имена параметров маршрута. К тому же второе место, вмещая в себя вообще все маршруты, будет регулярно становиться источником конфликтов в гите.
Тем не менее, при желании так всё же можно сделать и с текущим api:
app = FastAPI()
app.get("/")(read_root)
app.get("/items/{item_id}")(read_item)
я не понимаю почему используя "чистый ООП-способ" Вы строите таблицу роутинга после создания инстанса.
Для меня этому находится только следующее объяснение: подход с декораторами настолько сильный отпечаток оставил, что не удаётся от него избавиться :-\
Тайп хинты крайне полезны не только как "тестирование" когда, но и как документация (валидируемая), и как инструмент помогающий изменять код (измените тип - сразу покажет в каких местах отваливается код).
Мне кажется в целом противопоставление тестов типизации это ложное сравнение. Вы же не будете рассказывать разработчику на С++ или Go что типы не нужны?
Питон хорош тем, что можно постепенно добавлять в кодовую базы как тесты, так и тайпинг. И скорее всего, если у вас серьезный проект, с самого начала там будут и тесты, и тайпинг, как средства помогающие сдерживать фрустрацию команды
Тайп хинты крайне полезны не только как "тестирование" когда, но и как
документация (валидируемая), и как инструмент помогающий изменять код
(измените тип - сразу покажет в каких местах отваливается код).
Автор статьи сделал потрясающий вывод (который я много лет пытаюсь вкладывать в головы людей, но заходит временами крайне тяжело): если из названия функции понятно что она делает, то документировать её поведение не обязательно. (то есть потратить силы на выбор хорошего названия всегда выгодно)
То же самое касается аргументов и возвращаемого значения.
Мне кажется в целом противопоставление тестов типизации это ложное
сравнение.
Для чего нужны тесты?
Чтобы выявить проблемы в коде
Чтобы выявлять проблемы, возникающие в коде после внесения в него правок
Для чего нужны типы? (в контексте тайпхинтов)
Чтобы выявить проблемы в коде: ожидаются килограммы, программист передаёт градусы
То есть сравнение не ложное, пересечение "зачем" очень обширное.
Вы же не будете рассказывать разработчику на С++ или Go что типы не нужны?
Мне крайне нравится то что Go взял в язык парадигму параллельности.
Мне крайне не нравится, то, что он отказался от исключений (и программисты де-факто при помощи кортежей nil, error
реализуют исключения сами
Мне так же очень хочется язык без возни с типами, но с аналогичной парадигмой на борту. Я не буду рассказывать что типы в Go не нужны, но язык с возможностями Go (многотред + файберы из коробки на борту) и на базе скриптовой парадигмы был бы конфеткой.
как-то так
"Выявить проблемы" это упрощение:
Допустим в автомобиле тормоза - это система для спасения жизни, и подушки безопасности - это система для спасения жизни.
Хочется убрать одно из двух?
Думаю нет.
У меня отношение к типизации-тестированию такое же, убирать одно из двух не хочется. Несмотря на общие цели, более детальный фокус иной.
По поводу исключений я думаю что вопрос спорный. Как по мне это неявная передача контекста выполнения в крайних условиях, без гарантируемой обработки этих крайних условий.
Мастадонты индустрии (например разработчик языка D, Александреску) также отмечают проблемы с исключениями. Свое мнение крайней инстанцией не считаю.
Я считаю что типы упрощают а) восприятие (простота) кода, б) изменение (рефакторинг) кода, в) поиск зависимостей и крайних случаев.
И я есть обычный груг, если что-то помогает сдерживать сложность (главный враг груг), учитывая что время есть бесконечность, груг брать типы в свой код!
тайпхинты, не работающие в рантайме, не несут иной функциональной нагрузки, кроме выявления проблем на стадии редактирования.
Передавать строки вместо словарей по прежнему возможно.
Писать неверные тайпхинты по прежнему возможно
итп
так... и?
тесты тоже в рантайме ошибки не ловят
и то, и другое нужно предварительно запустить
так... и?
Я комментировал тезис что "тайпхинты для выявления проблем на стадии редактирования" - это упрощение.
Это не упрощение, а факт.
Мое замечание направлено в сторону "выявления проблем". Проблемы бывают разные, что я пытался показать на примере тормозов и подушек безопасности.
То что тесты работают также не работают в рантайме решили не замечать?
То что тесты работают также не работают в рантайме решили не замечать?
я проигнорировал это, поскольку это коментарий совершенно о другом контексте.
цепочка:
тезис: тесты и тайпхинты дают возможность найти проблемы в коде
но поскольку тайпхинты дают делать это только в стадии редактирования кода (язык иного не даёт), то переброс усилий на написание тестов даст больше профита: ибо позволит дополнительно находить проблемы в изменениях кода
вы мне возражаете: тесты тоже только в стадии редактирования. - это тут при чём? не при чём.
вы мне возражаете: тайпхинты проблемы - это упрощение
я говорю: почему упрощение? тайпхинты этого языка только на стадии редактирования работают. Соответственно никакой смысловой нагрузки не несут кроме запуска верификатора в IDE.
Кажется что вы в одном контексте, а я в другом.
Смысловую нагрузку они несут, также как и тесты. Потому что ровно также как и тесты встраиваются в CI и не дают запушить код, который проверку не проходит.
Сами предложили противопоставление тестов и типов, сами теперь отрицаете что механизм работы обоих инструментов это дополнительная фича. Что тесты нужно запустить, что типы нужно проверить анализатором. Почему вы рады запускать тесты и включать их в CI, но не рады включить в CI mypy?
Сами предложили противопоставление тестов и типов
тайпхинты могут выявить ошибки вида "вместо килограмм передали градусы" (вместо словаря - строки итп)
тесты могут выявить гораздо бОльший спектр ошибок, в том числе и те, что выявляют тайпхинты
(повторяю: мы обсуждаем тайпхинты питона, работающие исключительно в IDE/стадии редактирования текста)
Еще при чтении кода помогают.
это как посмотреть. могут помогать, если помещаются в 80 символов вместе с именем функции и аргументов, а могут и мешать. Могут мешать очень сильно.
Хинты работают не только в IDE
Они работают как часть проверок в Github Actions или другом инструменте CI.
Условно:
шаг 1) скачать питон и зависимости,
шаг 2) запустить flake, если ошибка стоп
шаг 3) запустить тесты, если ошибка стоп,
шаг 4) запустить mypy, если ошибка стоп,
шаг 5) запушить на s3 если билд удачный
https://mypy.readthedocs.io/en/stable/
Тесты не имеют покрытия всех мест где вызывается функция.
Тесты не могут проверить что вызывающий использует только "публичные" методы и не руками не изменяет стейт класса.
Тесты не проверят что функция которая должна смотреть конфиг не пытается его изменить.
Тесты не проверят что вы реализовали __mul и всегда пишете например Price(amount=1.2, currency='RUB') * 0.2 ( где 0.2 скидка). Но вы забыли что можно 0.2 * Price(amount=1.2, currency='RUB') и вам также нужен __rmul и тесты этого не покрыли.
И так далее...
Только не убеждайте меня что вы все это покроете тестами, этого точно не произойдет =)
У вас собрана большая коллекция ошибок.
А где их можно посмотреть все вместе?
Код ошибки - описание - исходник проверки.
Может вы дали линк на GitHub, но я не заметил.
По ошибкам:
Ошибка №13: часто нужно отлавливать любой exception (catch all), не важно, чем он вызван.
Не уверен что для на все случаи жизни можно предусмотреть в try/except все возможные except.
Ошибка №23: отсутствует 'is'. Мелочь, но глаз режет.
По всем ошибкам я бы определился - либо все с заглавной, либо lowercase. Тоже режет глаз.
В чатах меня бесит
it's
вместоits
Но это же кошерно-неверное написание! :)
It's a kind of magic! :)
It's как краткая форма it is - да. А если притяжательное местоимение - то its.
В статье есть файл .flake8 под спойлером, там код ошибки, описание и ссылка на гитхаб
Не уверен что для на все случаи жизни можно предусмотреть в try/except все возможные except.
Как минимум есть разница между
`except`
и
`except Exception`.
Думаю линтит именно первую форму
В статье про ошибки присутствуют грамматические ошибки .)
Чем не устроил готовый "матерый" статический анализатор кода? В Java много всего в open source. Думаю из доступного джавистам богатства Sonar для Python должен сработать
У сонара очень мало встроенных проверок для Python. Flake8 с плагинами позволяет найти гораздо больше code smells и ошибок
Спасибо, понял. Но можно использовать Sonar в дополнение Pylint, Bandit, Flake8
sonar.python.flake8.reportPaths
Можно, но какой в этом смысл, если локальный flake8 умеет все ровно тоже самое?
По моему опыту, единственное, ради чего стоит использовать сонар - так это для отображения покрытия кода тестами, если этого не умеет CI/CD. Например, GitlabCI умеет отображать его только в рамках MR, и только для измененных файлов, а если нужно просто посмотреть на текущее покрытие пофайлово, то увы.
IMHO, преимущество сонара в комплексности метрик и их зрелости + приличная база знаний почему конкретный "душок" кода вреден проекту. Ну и к долгосрочным последствиям использования - наблюдение за эволюцией качества (не 100% верная) но лучше чем эфимерные размышления о нем. По крайней мере это позволяет переводить споры о тех долге в более конструктивное русло. Последнее не применимо к теме этой статьи, но первое очень даже!
Отличный топик по питону, а не очередной перевод. Спасибо!
Привет, @kesn. Как всегда годно, и как всегда в твоих статьях, я не со всем согласен. ;) это кстати хорошо, что есть разные взгляды, это развивает.
Спор про типы оставим я придерживаюсь противоложных взглядов. Ранний выход, кстати, тоже спорно. Мусорные переменные как '_' тоже так себе. get_text as '_' слегка в недоумении.
в последнее время использую raise ... from ..., удивительно улучшает работу с отладкой кода. https://docs.python.org/3/tutorial/errors.html#exception-chaining рекомендую.
Жаль, что, в той же Django, ты не отметил как ошибку формирование листа в куеву хучу элементов, а потом break в цикле прохода по этому листу. Вот это точно бесит.
А в остальном да, все так и есть.
Критика от Макса многого стоит! Спасибо за коммент.
raise from для меня как-то само собой разумеещееся, даже не подумал про это писать.
Про типы готов биться до последнего :)
Я про то, что можно еще поискать, как мало используется raise from. Уверен, что попадет в топ. Типа если исключительная ситуация обрабатывается и бросается другое или то же исключение но без создания цепочки исключений. И вообще, это была не критика а только восхищение и белая зависть к рукам из правильного места :)
Но оно ведь автоматически применяется
>Exception chaining happens automatically when an exception is raised inside an
except
or finally
section.
Если в обработке одного исключения меняешь его на свое исключение, то атрибут __cause__ нового исключения будет пустым. С from там будет стоять предыдущее исключение. https://peps.python.org/pep-3134/#explicit-exception-chaining.
pathlib
позволяет читать и писать в одну строчку:
Обычный open тоже так позволяет.
Кажется, у меня тут пробел в знаниях. Как?
open ("file.txt", "w"). write ("hello world \n")
open возвращает обычный file object, не вижу никаких препятствий вызывать его методы в этой же строчке.
А закрывать кто будет? ) Pathlib делает это автоматически
При выходе из области видимости (при переходе на следующую строку то есть) вызовется деструктор.
Hidden text
class t:
def __init__(self):
print("create")
def __del__(self):
print("delete")
def do(self):
print("do")
t().do()
python3 test.py
create
do
delete
Внутри IOBase деструктор явно вызывает close()
def __del__(self):
"""Destructor. Calls close()."""
Все нормально, так можно ;)
Нельзя. В спецификации языка нигде не указано когда выполняется удаление объекта, потому имеет право вообще не удаляться. GC на основе счётчика ссылок это особенность cpython, и если его отрубить/вырезать, то всё сломается.
Я ж кидал ссылку в статье - чувак исследовал garbage collection на файлах, в cpython вроде ещё ничего, в pypy - нет. Я бы вообще не стал полагаться на сборщик мусора, имплементации могут быть разными...
«Явное лучше неявного.»
Сейчас поправили, а раньше на виндоусе файл при падении программы оставался заблокированным и приходилось что-то делать, чтобы открыть его снова.
Можно упереться в лимит открытых файлов.
Насколько я знаю, в pathlib только пути до файлов (что следует из названия). Сама работа с файлами реализована в стандартной библиотеке, в том числе закрытие.
open ("file.txt", "w"). write ("hello world \n")
Кхм, а файл закрыть не надо?
Type hints делают код более понятным?
Если применять умеренно, то да.
Другое дело, зачем использовать Питон, если взять за правило "везде расставлять Type hints". Есть же языки, в которых это делает автоматика.
Ради библиотек, например.
Как на счёт того, чтобы читатель кода не охреневал от догадок, что тут вообще происходит)
Для этого не обязательно их расставлять "везде".
Ровно в том месте, где этого не сделано, возникает точка "а теперь происходит всё что угодно". После этого непонятно становится ровным счётом нихрена.
Никто не хочет читать ВСЮ функцию, чтобы понять что ожидается на входе и что ожидается на выходе.
Понимаете, вы этими расставлениями аннотаций типов делаете из Питона какой-то недоML-недоХаскель. В результате проверка типов проводится не сплошняком/нелинейно, а возможности динамически типизированных языков сильно урезаются. Да, где-то расстановка типов разумна, но не везде же.
И как там с ad-hock polymorphism, кстати?
Ровно в том месте, где этого не сделано, возникает точка "а теперь происходит всё что угодно". После этого непонятно становится ровным счётом нихрена.
Это всегда так — даже теорема есть.
Если не нравится — попробуйте Хаскель или Идрис. Там тоже происходить в функции может много чего, но очень часто сигнатура действительно определяет одно разумное поведение. К примеру, мы можем искать функцию bind по сигнатуре `[a]->(a -> [b]) -> [b]` или функцию map по сигнатуре `[a]->(a -> b) -> [b]`.
Понимаете, вы этими расставлениями аннотаций типов делаете из Питона какой-то недоML-недоХаскель.
Вы так говорите, как будто бы это что-то плохое. Статическая типизация - это хорошо, даже JS-ники это поняли и придумали TypeScript. Любой проект больше 2000 строк на динамически-типизированном - помойка из костылей и проблем.
а возможности динамически типизированных языков сильно урезаются
Моя бы воля, я бы их вообще запретил :) Может быть я мало на них писал, конечно, но на сегодняшний день я не понимаю в чём динамически-типизированные языки превосходят статически-типизированные. Ладно, я еще могу понять претензии к языкам вроде C++ или того же Haskell, где почти нет внятной рефлексии. В остальных случаях динамически-типизированные языки - это почти гарантированный бардак))
Если не нравится — попробуйте Хаскель или Идрис
Это как-то из крайности в крайность. Если еще Хаскель можно натянуть на ту же область, где используется Питон, то Идрис - это вообще не продакшн язык, служащий больше математикам для доказательства теорем. Людям попроще оно нафиг не упёрлось, формально верифицировать корректность программ.
Как бы если не нравится, наверное лучше-таки попробовать Go или там C#. Если хочется упороться, то можно и Scala / Rust / C++. Ну или тот же Haskell. Очень приятный язык, который, к сожалению, совершенно упоролся по чистоте функций и потому малоприменим в мире, где ничего не постоянно.
Любой проект больше20000 строк на динамически-типизированном — помойка из костылей и проблем.
Исправил.
Может быть я мало на них писал, конечно, но на сегодняшний день я не понимаю в чём динамически-типизированные языки превосходят статически-типизированные.
Пережиток тех времён, когда практически применимых языков с выводом типов не было, а писать всюду типы не хотелось.
Пережиток тех времён, когда практически применимых языков с выводом типов не было, а писать всюду типы не хотелось.
Есть ситуации, когда даже Хиндли-Милнер с разными доработками является обузой и стесняет полёт мысли. Разумеется, эти ситуации крайне далеки от классического цикла "написал-протестировал-PR-проверка CI-рецензия-слияние-проверка на Alpha/Beta/PROD".
Ну и поскольку я буквально только что писал очередной интерпретатор на Scheme, ответственно заявляю, что проект на 200 строк не превращается в помойку, если писать аккуратно. Проблемы начинаются именно с нескольких тысяч.
Вы так говорите, как будто бы это что-то плохое.
Конечно. Вместо простого, полезного инструмента получается какая-то фигня. Я не сторонник забивать шурупы молотком и гвозди отвёрткой.
В своё время я сделал пару экспериментов Haskell/Python и подтвердил для себя то, что в ряде случаев разработка идёт быстрее на языке с динамической типизацией, а в ряде случаев — со статической. Это, конечно, банальности, но банальности полезно перепроверять.
Так вот, если программа настолько линейна, что за один прогон проверяется целиком, а прогон занимает меньше 10 секунд, то на Питоне её ваять, скорее всего, быстрее, чем на Haskell.
В то же время, когда логика крайне развесиста, программа сложна, то тут Haskell чудовищно выигрывает по времени разработки и умственным усилиям.
Поэтому каждому инструменту своё поле применения.
но на сегодняшний день я не понимаю в чём динамически-типизированные языки превосходят статически-типизированные
На них проще пишутся короткие скриптовые программы, простые программы-черновики.
Заметьте, встречаются ситуации, когда вас устроит даже программа, в которой корректно только начало (классический пример, это Mathematica notebook, где только часть выражений вам нужна, а остальные — мусор, оставшийся от многочисленных правок). В таком случае полное сведение типов, компиляция, только замедлят цикл run-check results-correct и будут раздражать. Даже если это Ocaml или Clean с их компиляцией за 0.1 сек, а не GHC!
Пока что, все что вы написали лишь подтверждает то, что я говорю. Все ваши примеры сводятся к тому, что если вам надо что-то одноразовое наваять на коленке, то языки с динамической типизацией подойдут.
Я, если честно, могу расширить это утверждение ещё шире: абсолютно не важно на чем вы напишете write-only код.
Когда я защищал статическую типизацию, я прежде всего говорил о проектах, которые живут долго (от года и выше) и обновляются регулярно, и любые проекты над которыми работает больше трёх программистов.
P.S.
С аргументом про то, что уменьшается простота и понятность не согласен. Простота и понятность как раз-таки кратно возрастают, но да, за счёт того, что над кодом приходится немножечко больше думать, когда ты его пишешь.
Все ваши примеры сводятся к тому, что если вам надо что-то одноразовое наваять на коленке, то языки с динамической типизацией подойдут.
Если надо что-то небольшое, несложное, одноразовое наваять, то языки с динамической типизацией подойдут лучше, чем языки со статической типизацией и полным выводом типов.
А раз так, то не надо пытаться из крокодила делать крокодила летающего.
Я, если честно, могу расширить это утверждение ещё шире: абсолютно не важно на чем вы напишете write-only код.
Я этих устриц ел, и ответственно заявляю, что на OCaml, а уж тем более с включённым -Werror одноразовый write-only код писать менее комфортно, чем на Питоне. Вас задолбает компилятор, необходимость где-то таки описывать типы и т.д.
Простота и понятность как раз-таки кратно возрастают, но да, за счёт того, что над кодом приходится немножечко больше думать, когда ты его пишешь.
Я писал про простоту и понятность языка, а не кода. Код же, в контексте, write-only => как он будет там пониматься через месяц, не важно. В конце-концов, на тип прекрасно можно намекнуть названием переменных, если нет привычки использовать Хаскельно-Фортрановский подход (основные имена — i, j, k, l, m), конечно.
Сейчас же, усилиями разных комитетов Питон разросся по размеру до C++.
Я всё ещё не понимаю о чём мы спорим :) Я согласен с тем, что для задач "сделать за пару дней и забыть" - динамически типизированные языке вполне норм. Можно ничего нигде не указывать и делать как хочешь. Даже документацию можно не писать.
Однако и статья не о таких задачах, да и ветка комментариев по сути начинается с утверждения, что type hints нужны, чтобы читающий мог что-то понять в написанном коде.
Если читателя нет - то и проблемы нет. Хотя вероятно и программа никому не нужна, кроме писателя :)
Самое забавное, что, по моим ощущениям, везде я вижу одни и те же классы проблем. Я даже запилил сервис, где можно закинуть код и получить код ревью, и, собрав немного статистики, понял, что 50 типов ошибок достаточно, чтобы покрыть большую часть проблем в чужом коде. Но выборка у меня была небольшая, и я подумал: а что, если проверить много кода? И всё заверте...
А что если один раз "изобретенный велосипед" использовать во всем мире, а не изобретать его вновь и вновь в каждой компании и стране, где он нужен?
На кой разрабатывают однотипные программы в разных точках мира?
Да и не только программы, но и товары, придумывают, как оказать одни и теже услуги и еще много чего.
У организатора этого всего плохо с памятью и координацией?
Вполне допустимо, что "правая рука" не в курсе, что делает левая. А мозг то что? Тоже не в курсе, что делают руки?
А если в курсе, то на кой делать одно и тоже обеими руками, если каждой рукой можно делать что-то свое. Типа одна рука держит гвоздь, а другая машет молотком. Да, иногда нужно что-то держать обеими руками, но тогда в помощь другой чел, чтобы махать молотком.
Это упрощенная аналогия.
А что если посмотреть на деятельность всего человечества глобально?
Типа так достигается совершенствование технологий и товаров?
Так а зачем некоторые технологии разрабатываются с нуля?
Вот, например, есть Яндекс-доставка, там организован процесс доставки, создано ПО.
Но другая компания создает свой сервис доставки и создает свое ПО, типа наше будет лучше.
И да, конкуренция и все такое прочее сопуствующее.
А где развитие и оптимизация хотя бы на уровне регионов и стран?
5,500 постов в Хабе "python"
Пять с половиной постов?
А зачем в количестве постов точность до третьего знака после запятой?
Есть некоторые мелочи, с которыми я не согласен, но в целом получилась очень полезная и интересная статья. Большое спасибо!
contextlib.suppress(Exception)
это прям хорошо, спасибо за наводку.
Статья познавательная, спасибо.
Но вот пассаж про использование наследования классов для переиспользования кода - это та самая жесть, которой ООП мысль переболела в прошлом и теперь настойчиво рекомендует так не делать. Если очень нужны классы - то рекомендуется переиспользовать код путем агрегации. Если не нужны - хорош тот же функциональный подход типа partial.
Проблема в том, что иерархия наследования и переиспользование кода - это ортогональные вещи, которые могут по-другому сочетаться в другом проекте/ домене, который захочет переиспользовать ваш код. Или просто с течением времени это сочетание может измениться в изначальном проекте. А отделять их друг от друга в большой "заматеревшей" кодовой базе - тот еще квест.
Спасибо за статью, у меня похожий шаблон для flake8, обогащу его вашими находками.
Я использую совместно с black, это автоматом исключает многие ворнинги.
Сложные аннотации типов можно вынести в переменную. Часто вместо типа Generator
можно указать Iterator
так проще. Когда я хочу подчеркнуть, что dict который я возвращаю менять не надо, я использую аннотацию Mapping
Когнитивную сложность у себя я отключил. Цикломатическая и когнитивная на маленьких цифрах очень похожа, а так плагином меньше.
Они, наверно, супермены и всегда помнят, что надо закрывать файлы?
Если быть педантом, то нужно еще через try-finally
Для настраеваемых функций часто использую класс с __call__
. Его потом легко подменять в тестах.
from typing import Callable
from requests import Session
class Get:
def __init__(self, session: Session):
self._session = session
def __call__(self, url: str) -> bytes:
res = self._session.get(url)
res.raise_for_status()
return res.content
def application(getter: Callable[[str], bytes], url: str) -> bytes:
return getter(url)
application(Get(session=Session()), "https://google.com")
def stub(_: str):
return b"test"
application(stub, "https://google.com")
У меня боевой язык программирования питон, а любимый - rust. И я не понимаю почему я должен писать типы в питоне. Я нажимаю много кнопок (и даже думаю про всякие там суммы типов), а в обмен мне ничего не дают, кроме жалкой подсказки в автокомплите. Как насчёт правильной валидации типов? Ах, да, где-то там есть код без аннотаций типов, так что вывести типы не получается никаким образом. А у кого-то вот этот самый super(args[0])(**kwargs)
и угадай какой у него там тип.
В сравнении с нормальной типизацией, type hinting в питоне - чистой воды карго-культ. О, у Больших Белых Людей есть Большая Ржавая Компилятора, которая Умеет Типы. Надо нам добавить Типы и тогда мы будем как Большая Ржавая Компилятора и мы сможем гордиться и любить наши Типы.
Нет, не сможем. type hinting помогает для фиксации контракта, но этот контракт никем не энфорсится.
Более того, я даже НЕ МОГУ написать сигнатуру условного выхода.
def conditional_exit(condition: bool) -> ?:
if not condition:
return False
os.exit(0)
Какой тип у нас после вызова os.exit(0)? None
? Но это не правда, os.exit не возвращает None
. Где мой '!
'? (AKA ⊥
)?
Ага, а какой тогда у нас возвращаемый тип у функции? Union[bool, NoReturn]
? Но это же не правда! Типом должен быть bool, а NoReturn - это его subtype. Стоп. А питон поддерживает subtyping?
Стоп. А питон поддерживает subtyping?
А почему бы не взять и не проверить?
def conditional_exit(condition: bool) -> bool:
if not condition:
return False
sys.exit(0)
Success: no issues found in 1 source file
почему вообще должен быть здесь NoReturn ? Эта функция может вернуть только bool, NoReturn нужен только для функций которые безусловно завершаются чтобы проверить что мы не забираем значение и вообще не ожидаем что дальше может быть что-то кроме Exception.Oh wait а как же мы декорируем исключения ? :)
os._exit, пардон. Алсо, я посмотрел help на них обоих (sys.exit и os._exit) и ни один из них не имеет типосигнатуры (Python 3.10.6)
Вот это интересно.
То есть в самой функции этого нет, информация о типах где-то в другом месте. Или питон не показывает сигнатуры в справке? Ещё чудеснее.
В стандартной библиотеке аннотаций типов нет, они в сторонней либе typeshed
Вообще, это сейчас стандартная практика костылять аннотации отдельно (и зачастую неофициально), для того же несчастного Django есть django-stubs например
Тайп хинтс нужны, чтобы понять, что вообще куда можно передавать и что имел в виду разработчик. Да, контракт на минималках. Я сам обожаю nim, но там до коммерческой разработки как до луны, а rust меня победил
Не так уж много языков, в которых явно есть bottom type. В java или C++ в аналогичном случае conditional_exit будет возвращать bool и всё.
И, насколько я понимаю, bottom type | bool == bool, так что это вполне корректно.
Очень слабо подтвержденный наброс.
Ты можешь написать условный выход используя typing.overload.
Типы и контракты в типах в коде поддерживает анализатор, который встроен в CI.
Как ты вообще притянули сюда раст не очень понятно. "Большая Ржавая Компилятора" связи с типизацией в питоне имеет не больше чем с любым другим языком
Прекрасная статья! В свое время заменила бы мне первый год разработки на питоне)
Крутая статья, спасибо :)
Выскажусь про "длинные методы". Тут очень деликатный вопрос как по мне. В погоне за короткими методами можно получить ещё более трудночитаемый код:
Часто единый флоу нарезают по методам, и чтобы понять, что делает паблик функция (или, вернее, как конкретно работает), нужно попрыгать по всем вызываемым методам. И это не решается правильными названиями, поскольку реально алгоритм/флоу сложный. И искусственное деление его по 7 строк никому лучше не сделает.
В погоне за правилом "не более N строк", разработчики будут просто пихать больше кода на одну строку
def get_orientation(obj: Ufo) -> tuple[float, float, float]: # WAT? это что такое?
UserId = int
FooId = int
BarId = int
def return_something() -> Tuple[UserId, FooId, BarId]:
...
Частенько выручает. Но не в этом случае.
За suppress/tenacity отдельное спасибо :)
Я, вообще, не разработчик на питоне, я пишу на плюсах, но иногда приходится читать питон-код и он всегда ужасен. Каждый раз, когда я спрашиваю у интернета "лучший способ сделать что-то", интернет мне отвечает: "Смотри! Есть отличный способ! Давай засунем все в одну строку! Это самый лучший путь!".
Когда начал читать статью - был приятно удивлен. А потом дошел до пункта о nested if и погрустнел.
Почему бы в этом чудесном примере не использовать описанный немного позже прием "Early quit"?
if min_val is not None and max_val is not None:
if max_val < min_val:
raise ValueError("max_val is greater than min_val") # тут уровень отступов == 2
# ...вжух!
if min_val is not None and max_val is not None and max_val < min_val:
raise ValueError("max_val is greater than min_val") # тут уровень отступов == 1
# ...вжух! ...вжух-вжух!
if min_val is None or max_val is None:
raise InvalidValue("max_val or/and min_val are None!")
if minval > maxval: # в условном операторе только логика, никакой дополнительной нагрузки
raise ValueError() # все еще уровень отступов == 1
Возможно, я слишком избалован best-practices на привычном мне языке и тяну оттуда вещи, которые невалидны в других местах, но если нужно всунуть множество разных проверок в один if - то лучше этого не делать. Разбейте на функции, вынесите в отдельные переменные, разбейте на несколько if-ов - это немного увеличит код. Но это немного упростит чтение. Кажется, игра стоит свеч.
Еще из примеров, с которыми я категорически не согласен в заданном виде, это об использовании условного тернарного оператора. Возможно, это я недостаточно умен, но имхо, если там что-то сложнее, чем то, что указано в примере - лучше вынести это в отдельную функцию (которую вы потом отдельно протестируете), чем нагружать одну строку тонной логики. Терпеть не могу что-то в духе такого:
if abs(a) > abs(b):
abs_diff = abs(a) - abs(b)
else:
abs_diff = abs(b) - abs(a)
# ...вжух! ...???????
abs_diff = abs(a) - abs(b) if abs(a) > abs(b) else abs(b) - abs(a)
# Конечно, совсем корректным было бы сказать abs(abs(a)-abs(b)), но в качестве иллюстрации сойдет...
Длинная строка. Длинная. С кучей слов. Где тут вызовы функций, где начинается одно выражение, где заканчивается другое, в каком порядке все это исполняется? На все эти вопросы можно найти ответ. Но взгляд спотыкается. (Где-то также, как на ужасной практике в С++ писать if (CONST_VALUE == variable) вместо if(variable == CONST_VALUE)), которая, слава умным людям, уже отходит в прошлое).
Конечно, условный тернарный оператор - это хорошо, если у вас тривиальный код. Но если там есть хоть что-то сложнее, чем "одна проверка, одно присваивание" - то пожалуйста, сделайте это функцией. Это проще читать. Это проще тестировать. Это проще.
Вещь, которую (насколько я разобрался) понял автор - код пишут намного реже, чем читают. Наверняка в статье есть еще вещи, к которым можно придраться. Есть вещи, которые можно сделать еще проще. Но мне очень нравится, что хоть где-то, хоть кто-то согласился с тем, что код на питоне - непомерно-сложный для чтения. Теперь я чувствую, что я не один такой.
Автору спасибо.
С совмещением условий в один if также не согласен с автором, условие стало сложнее и текст набрал плотности
В питоне есть сахар, который я встречаю очень редко по упрощению вложенных if, но я практически никогда не видел его в коде. Пример будет очень условным, но тем не менее:
data = 54
data_is_not_none = data is not None
data_bigger_then_50 = data > 50
if data_is_not_none and data_bigger_then_50:
print("Yeah!")
А где собственно сахар? Вполне себе базовые конструкции языка.
Линтеры в общем подталкивают к такому подходу, там сильно с вложенными ифами не забалуешь.
Использование типа bool - это синтаксический сахар?
Само собой, сложные условия лучше вынести в переменную, или даже в отдельную функцию. В данном случае это обычный контракт, при неисполнении которого надо бросать исключение.
Отличная статья; во многом советы универсальны и не зависят от языка программирования. Не являясь специалистом Python (и поэтому буду стараться писать в общих терминах), все же отмечу моменты, с которыми не согласен:
1) Redundant else - нарушается блочная структура. Есть старое доброе правило - один вход, один выход. Вполне допуская оправданность конструкций вида:
if check_1: return
if check_2: return
...
main code
отмечу, что при такой структуре идет разрушение кода функции как одного блока, который можно во что-нибудь обернуть. Нередко встречал ситуации, когда в начале нужно вызвать какой-нибудь обработчик (логирование?) и в конце тоже нужно вызвать какой-нибудь обработчик (профилирование?). Оптимальным здесь считаю goto в конец, сразу перед return. Здесь можно возразить, что тогда следует написать функцию-обертку, но все же, все же... вариант с goto более "модульный" какой-то, что ли.
2) "Заменяйте ненужные переменные на underscore " - не понял смысл этого. Потом логика изменится, понадобится сослаться на эту переменную цикла, и придется изменить с "_" на "i"? Идея менять имя чего-то только потому, что оно вдруг понадобилось, выглядит довольно странной.
Но по большому счету, это мелочи.
Как раз хотел сказать: напишите враппер :) Но вообще да, вы правы, и когда нужно что-то сделать "после" в любом из вариантов выхода, то предложенный подход не катит.
Да, если придётся сослаться, то надо будет заменить на
i
. Это как с мёртвым или закоментированным кодом: от него нет толку, но он занимает место.i
тоже занимает место в голове, не неся смысловой нагрузки, в то время как_
глаз проскакивает, не обращая внимания. В питоне_
для неиспользуемых переменных вроде как типа стандарт, поправьте меня, если я не прав. Ну и сравните:
roll, pitch, yaw, speed = get_many_params(object)
# vs
_, pitch, _, speed = get_many_params(object)
Имхо, второй вариант несёт меньше нагрузки.
Имхо, второй вариант несёт меньше нагрузки.
Почему-то мое имхо с точностью до наоборот :)
Один из важнейших принципов читаемости, который я вывел за много лет кодинга (на pl/sql) - чтение кода должно вызывать как можно меньше вопросов. Вот я читаю первый вариант
roll, pitch, yaw, speed = get_many_params(object)
И тут нет особых вопросов. Также здесь соответствие принципу
Легко читать > Самодокументация > Названия переменных
Если меня вдруг заинтересует, где именно используется roll, и как именно - я либо воспользуюсь поиском, либо выделю слово, чтобы редактор его подсветил во всех остальных местах. Учитывая то, что функции у нас не километровые (в соответствии с другими вышеоговоренными принципами), проблем с этим нет.
Читая же
_, pitch, _, speed = get_many_params(object)
У меня сразу в голове возникают вопросы - а что именно скрывается за этими "_" - и будьте уверены, я пойду ковырять этот get_many_params
в попытке это понять. А потом изменится алгоритм, бизнес-процессы, понадобится мне тот же roll, и придется после полугода забытья опять ковырять get_many_params, чтобы понять, где именно этот roll возвращается. В случае же первого варианта я сразу воспользуюсь этим roll. Здесь также нарушается принцип понятных имен. То есть принцип понятных имен входит в конфликт с принципом "неиспользуемое называем "_" ".
Исходя из всего вышесказанного, я бы предпочел сразу явно видеть, что к чему. Мне претит мысль, что из-за дизайна вызываемых функций надо вмешиваться в имена объектов вызывающих функций - это какое-то нарушение независимости получается.
Насчет стандартов ничего не могу сказать, ибо не специалист в python. Поэтому сужу с точки зрения "в целом", используя опыт кодинга на pl/sql :)
Я думаю, у питонистов таких вопросов не возникает, к подчеркиваниям все привыкли и им понятно. И мне кажется, тут всё-таки yagni важнее :P Ну каждому своё ?♂️, сверху уже писали, что Джанго, например, зарезервировали "_" под локализацию текста
roll, pitch, yaw, speed = get_many_params(object)
КМК, ужасная практика (все так делают, да). Потом функция стала возвращать не 4 параметра, а 5 - и всё сломалось. Или при вызове функции программист их неправильно сосчитал. Или, чтобы не ошибиться с подсчетом, скопипастил правильный вызов, но не до конца исправил.
Потом функция стала возвращать не 4 параметра, а 5 - и всё сломалось
В python такое считается нормальным? Ну, в смысле, когда кто-то меняет api функции и при этом не просматривает все места, откуда она вызывается? Дескать, не мои проблемы? :) При работе с компилируемым ЯП я бы только рад был, если бы такое ломалось и уже на этапе компиляции валилось с ошибкой.
Или при вызове функции программист их неправильно сосчитал. Или, чтобы не ошибиться с подсчетом, скопипастил правильный вызов, но не до конца исправил
А вот это уже манипулятивные аргументы :) Потому что это вообще здесь не при чем. Программист может ошибиться в чем угодно и где угодно. Чтобы не ошибаться при копипасте, надо не копипастить. А чтобы не ошибаться в написании кода, надо его не писать. Приведу пример из мира баз данных:
можно писать везде
select * from tbl
вместо
select fld_1, fld_2, ..., fld_n from tbl
дескать, если еще одно поле добавится, его не надо прописывать лиший раз. Но это может не только негативно сказаться на производительности, но и по сути означает потерю контроля над кодом. В этом месте нужно только эти поля, в этом - только вот эти. Читая везде все скопом сразу - по сути создается помойка, где непонятно, что именно нам нужно, ради мимолетного удобства программиста (не нужен копи-паст имени поля в те места, где оно нужно). Возможность ошибиться исключается, но возможностей ошибиться вообще бесконечно много, чтобы ради этого поступать в ущерб другим, важным областям (читабельность, производительность, контролируемость кода разработчиком, контроль зависимости объектов).
У меня сразу в голове возникают вопросы - а что именно скрывается за этими "_" - и будьте уверены, я пойду ковырять этот
get_many_params
в попытке это понять.
А для других это возможность срезать и не читать лишние переменные.
Go вообще запрещает объявлять неиспользуемые переменные на уровне компилятора. Но есть один способ обойти это https://go.dev/doc/effective_go#blank
Как я уже где-то писал, сам я в питоне разбираюсь не очень, но в условных плюсах в таких случаях есть, на мой вкус, очень хорошая практика (конечно, есть еще с десяток плохих практик, о которых мы умолчим). В терминах питона я бы ее описал как-то так:
# Суперская фича. В плюсах это была бы struct, но у этих dataclass есть возможность добавить тонну полезностей почти без кода.
@dataclass
class PhysicalAttributes:
roll: int = 0
pitch: int = 0
yaw: int = 0
speed: int = 0
def get_many_params(object) -> PhysicalAttributes:
roll = GetRoll(object)
pitch = GetPitch(object)
yaw = GetYaw(object)
speed = GetSpeed(object)
return PhysicalAttributes(roll, pitch, yaw, speed)
roll, pitch, yaw, speed = get_many_params(object)
# vs
_, pitch, _, speed = get_many_params(object)
# vs
attr = get_many_params(object)
# Дальше используем то, что нам нужно
Теперь, если вы добавляете возвращаемых параметров - это не обязательно поддерживать во всем коде, где вызывается функция (ну, скажем, вы добавили имя - большей части кода на это будет все равно). А если вы что-то удаляете - то поддерживать нужно только там, где это ранее использовалось, что правильно.
А я делал как-то так:
@dataclass
class BaseClass:
def set(self, field, val):
setattr(self, field, val)
def get(self, field):
return getattr(self, field)
.....
new_fields = [('x', int), ('y', int)]
cls = BaseClass()
cls.__class__ = make_dataclass("Example", fields=new_fields, bases=(BaseClass,))
В new_fields
в итоге у нас полное описание полей, и можно просто делать cls.set()
/ cls.get()
где нужно. Плюс BaseClass
можно использовать для тайп-хинтинга.
Груг видит сложность, груг бояться
Так, а какую проблему это решает?
Я попытался понять, как подобный BaseClass использовать для вышеуказанной проблемы - для того, чтобы сжать много возвращаемых аргументов в один, и не смог. Правда, я не питонист, так что это вообще ни о чем не говорит (по крайней мере, о решении).
Еще из минусов отмечу, что, в такие классы лично мне не очень приятно добавлять методы. Да, в dataclass в принципе плохо добавлять логику, но разные простенькие проверки, имхо, вполне допустимы.
new_fields = [('x', int),
('y', int),
('z', int)]
cls = BaseClass()
cls.__class__ = make_dataclass("Example",
fields=new_fields,
bases=(BaseClass,),
namespace={'sum': lambda self: self.x + self.y + self.z})
И это откровенно паршиво, на мой вкус. А главное - ради чего?
Так, а какую проблему это решает?
Удобная структура для хранения разнородной информации.
Вместо сабжевого roll, pitch, yaw, speed = ...
используется одна переменная.
Вот как пример — https://github.com/isdn/tg_bot_Fh9q/blob/master/sensors.py#L101
Я не настоящий питонист (в смысле мне за это не платят деньги), делал чисто for fun.
Еще из минусов отмечу, что, в такие классы лично мне не очень приятно добавлять методы. Да, в dataclass в принципе плохо добавлять логику, но разные простенькие проверки, имхо, вполне допустимы.
Вот именно, это датакласс. Если нужна логика — то либо через лямбды как в вашем примере, либо в определении самого класса, как обычный метод (в make_dataclass(), к слову, в bases можно передавать base classes).
Вместо сабжевого
roll, pitch, yaw, speed = ...
используется одна переменная.
some_tuple = ...
Ну это можно и без датаклассов делать. Просто не распаковывать tuple в переменные.
Tuple не умеет именованные значения. В принципе это норм если элементов 2-3. Больше — уже не очень норм.
Плюс датаклассы дают больше гибкости.
Я имел в виду, какую проблему решает это усложненное решение с прослойкой в виде BaseClass, по сравнению с использованием голого dataclass, как я описал выше? Что разработчик получает, повышая когнитивную нагрузку?
В приведенном примере используется вообще не так, как мы тут себе представляем. Этот dataclass нужен в том числе для того, чтобы пользователь функции мог, не заморачиваясь, узнать, что ему возвращают. Я знаю, что type-hinting в питоне - это чисто для разработчика, а не для интерпретатор, но я имею в виду вот такого рода запись:
def get_many_params(object) -> PhysicalAttributes:
...
По примеру выходит, что возвращаемый BaseSensors может включать или не включать в себя какой угодно набор параметров.
Что разработчик получает, повышая когнитивную нагрузку?
Наоборот снижает. Как раз для тайп-хинтинга.
Этот тайп-хинтинг чем-то превосходит банальный : int, используемый в обычном dataclass?
Вы добавили прослойку с какими-то методами. В ней надо разбираться. Ее создание (я про cls.__class__ = make_dataclass("Example",...
Выглядит довольно пугающе (что подтверждает комментатор в соседней ветке). При этом в приведенном примере его использование не решает изначально поставленных перед этим датакласом вопросов. И в чем тогда преимущество над обычным @dataclass?
Если есть два метода, которые возвращают BaseClass - это вовсе не означает, что они возвращают одинаковый набор аргументов. Разве это... Не сложно?
Этот тайп-хинтинг чем-то превосходит банальный: int, используемый в обычном dataclass?
Я не понял вопрос. Вы спрашиваете чем отличается теплое от мягкого?
Выглядит довольно пугающе
Разве это… Не сложно?
А, скажем, list comprehension с guard для вас тоже пугающе и слишком сложно?
При этом в приведенном примере его использование не решает изначально поставленных перед этим датакласом вопросов. И в чем тогда преимущество над обычным @dataclass?
Решает проблему о которой я писал выше.
Если есть два метода, которые возвращают BaseClass — это вовсе не означает, что они возвращают одинаковый набор аргументов.
Ну и чем это отличается от, скажем, dict[str, str] | bool
? Тоже "вовсе не означает, что они возвращают одинаковый набор аргументов". Но зато определяет контракт.
Вы спрашиваете чем отличается теплое от мягкого?
Возможно. Я поговорил с документацией питона, и так и не понял, чем отличается ваш тайп-хинтинг от тайп-хинтинга в обычном @dataclass. Можете отправить ссылку, где об этом можно подробно прочесть в личку, если так будет проще. Я буду признателен и с удовольствием ознакомлюсь.
list comprehension с guard для вас тоже пугающе и слишком сложно?
Если речь идет о чем-то вроде этого:
vowels = 'aeiou'
consonants = [c for c in string.ascii_lowercase if c not in vowels]
То все в порядке. Собственно, если сложность цикла и сложность guard достаточно низкие - это вполне удобный и лаконичный синтаксический сахар.
В вашем случае... Ну ладно, выглядит страшным только издалека и в темноте, пожалуй.
Решает проблему о которой я писал выше.
Простите, я не уловил. Вот эта?
Удобная структура для хранения разнородной информации.
Если да, то, ну, более линейный PhysicalAttributes тоже ее решает. Ваше решение, несомненно, лучше чем десять возвращаемых параметров.
Ну и чем это отличается от, скажем,
dict[str, str] | bool
?
Да, в общем-то, ничем (и в данном случае обертка, имхо, даже лучше). Но я же не предлагаю все впилить в набор возвращаемых значений. Контракт - это хорошо. Но в вашем случае, он существует только для писателя функции. Я же предлагаю вынести его в отдельный интерфейс. Тогда чтобы понять, что именно возвращает функция - не нужно смотреть в ее реализацию, достаточно увидеть структуру, которую она возвращает.
Я поговорил с документацией питона, и так и не понял, чем отличается ваш тайп-хинтинг от тайп-хинтинга в обычном @dataclass.
А что значит " тайп-хинтинг в обычном @dataclass"? У меня какой-то необычный? :)
В вашем случае… Ну ладно, выглядит страшным только издалека и в темноте, пожалуй.
Оно вообще не выглядит страшным. Вот через namespace и lambda — тут, пожалуй, можно сделать страшно.
Простите, я не уловил. Вот эта?
Если да, то, ну, более линейный PhysicalAttributes тоже ее решает. Ваше решение, несомненно, лучше чем десять возвращаемых параметров.
Более линейный — это какой?
Но в вашем случае, он существует только для писателя функции. Я же предлагаю вынести его в отдельный интерфейс.
В питоне нет интерфейсов в каноничном смысле. В моем примере "эмуляция" интерфейса/абстрактного класса — это как раз BaseClass.
Тогда чтобы понять, что именно возвращает функция — не нужно смотреть в ее реализацию, достаточно увидеть структуру, которую она возвращает.
Достаточно посмотреть на определение функции и увидеть что она возвращает. Я честно не понимаю проблемы о которой вы пишете.
Есть BaseClass, он определяет методы класса. Затем мы в рантайме создаем его поля и наполняем их данными.
Имея BaseClass в тайп-хинте в аргументах или возврате функции мы сразу понимаем, что можно с этим делать.
BaseClass в принципе может быть даже обычной оберткой над структурой типа dict, например (но тут мы потеряем некоторые фичи).
В данном примере тайпхинтинг позволяет только работать с методами BaseClass.
А что там внутри для тайпхинтинга не важно. По большому счёту BaseClass это просто коллекция типа dict. Тип не знает что у него внутри, но знает как к этим внутренностям дять доступ снаружи.
Все очень хорошо, но...
* В статье на хабре я могу приводить примеры плохого кода. Например: вот тут копипаста, а вот таким изящным способом я могу ее избежать.
* Я могу ссылаться на чужой плохой код, который мне не нравится. Разные причины могут заставить меня вставить чужую работу, например, на задачу FizzBuzz по-сеньорски уже пяток ответок прилетело.
* В демонстративных целях я могу писать код, отличающийся от того, который я напишу в рабочих проектах. Например, в рабочих проектах я никогда не поставлю магическую константу, а в примере на хабре я не буду занимать лишнюю строчку под переменную. Я же писал, почему здесь 42 в предыдущем абзаце, не думаю, что вы забыли.
И вот я написал действительно полезную статью, вместо чтения ее проверили на соответствие формальным параметрам, и оценили как "негодную".
Выдайте, пожалуйста, список формальных параметров, и я натренирую нейросеть писать вам годные статьи. Вам же их потом читать.
Попробовал затащить к себе плагины. Что-то даже нашли. Поживу с ними посмотрю как будет работать.
В документации к flake8-commas==2.1.0
рекомендуют использовать black вместо него.
flake8-annotations-coverage==0.0.6
и flake8-annotations==2.9.1
конфликтуют. Первый плагин считает покрытие и не имеет настроек, а второй требует 100% для включённых опций. В flake8-annotations
у меня отключена часть требований:
ANN101 Missing type annotation for self in method
ANN204 Missing return type annotation for special method
Поэтому в flake8-annotations-coverage
для себя не вижу смысла.
Я flake8 через pre-commit запускаю, поэтому не опубликованные в pip плагины не захотели работать, например git+https://github.com/c0ntribut0r/flake8-grug
https://flakeheaven.readthedocs.io/en/latest/
Обвязка вокруг flake для красоты с возможностью отключения отдельных правил у отдельных плагинов
def fn(items: list[int | float | None])
В python, если в сигнатуре метода указано, что аргумент принимает float, то он принимает и int (минутка саморекламы).
Я достаточно душный?) Если нет, то print вместо логирования, да и отсутствие type hints в статьях, которые по умолчанию являются учебным кодом можно простить, так как они служат дополнительным отвлекающим фактором, и влекут дополнительный import, а вот в продакшене да — лучше с ними.
"НЕ ГОРОДИТЬ ОГОРОД КЛАССОВ", там, где это не требуется, жирным текстом, пожалуйста))
Слишком абстрактно, где граница?
Я использую классы для dependency injection, так как без этого сложно писать юнит-тесты. Если выкинуть тесты, то эти классы можно убрать.
O в SOLID, пропагандирует содавать новый класс вместо изменения существующего. В результате код который создан в процессе эволюции продукта содержит больше классов, чем если бы его написали сразу.
Колоссальный труд. Мой вам книксен.
У нас в компании полный фарш, майпай, флейкхелл, аннотация типов обязательна, и пока линтер полностью не удовлетворишь - pr не замержишь. Я скорее "за", меня не бесит :) Но, возможно, стоит упростить набор правил, так как иногда линтер уж слишком жестит, приходится noqa-ть, а кто-то злоупотребляет ими на каждой строчке, что сводит на нет весь смысл. Спасибо за аддоны и конфиг.
Попробуйте сделать обучение на работе, где расскажите как в случае ворнинга правильно менять код. Когда есть инструкция что-делать, людям будет проще делать нормально.
Главное чтобы в noqa код ошибки добавляли, если их пустыми ставить, то всё плохо будет.
В настройках flake8 есть per-file-ignore, я например для тестов убираю часть проверок.
Простите за глупый вопрос, пытался сделать как на картинке, что б линтер не только подсвечивал проблемное место, но и рядом писал описание проблемы.
IDE - PyCharm
Если кто напишет в двух словах, буду сильно благодарен
Чтоб было прям как на картинке, нужен vscode и error lens. Про pycharm не в курсе
https://plugins.jetbrains.com/plugin/17302-inlineerror
https://plugins.jetbrains.com/plugin/19678-inspection-lens
Раньше дела не имел, поискал-поставил сегодня ради интереса, наблюдения: первый (InlineError) достаточно тормозной, второй (по словам автора, развитие InlineError), работает вроде пошустрее.
Груг против сложности. Я пролинтил все посты на Хабре про Python, и вот что я нашёл