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

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

C# заставляет программиста давать классам имена, соответствующие именам файлов, в которых находится код этих классов.

Не заставляет. Да и как бы он мог заставить, если в одном файле может быть несколько классов?

Ещё один человек даёт советы, как организовать код динамического языка таким образом, чтобы сделать бледное подобие статического.

Когда бы вы ни создавали точку входа в приложение — обязательно добавьте в код проверку на то, что этот код выполняется, а не импортируется

Я бы советовал автору для начала лучше разобраться с семантикой языка Python. Импорт – это и есть выполнение кода, def – это обычный исполняемый оператор, присваивающий лямбда-свойству переменной прописанное в нём лямбда-выражение. Не более и не менее. В модуле могут присваиваться и другие свойства, не только операторами def, и это один из вариантов рекомендуемого стиля. От недостаточного понимания семантики языка при написании кода и рождаются уродливые искусственные конструкции, подобные описанным в статье.

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

А как тогда правильно организовывать код динамического языка? Поделитесь плиз хорошими статьями на эту тему

Ну вот, например, обстоятельная книжка на английском по сходной теме. Но я вообще считаю все эти coding guidelines довольно условной вещью. Не в этом счастье.

Вот примеры удачных имён:

plane = Plane()

Не назвал бы удачным этот пример, где одновременно используются две переменные, различающиеся только регистром первого символа.

Дальше кто-нибудь по ошибке напишет Plane = Plane()и будет долго удивляться при попытке следующего после этого обращения к Plane().

Или Plane = None вместо plane = None.

Это не C++ и не Java, где экземпляр объекта синтаксически отличается от своего класса.

Ага, а ещё начинающий разработчик может случайно переопределить встроенные (англ. built-in) имена:

len = MyLengthCalcHandler()
print = MyOutputHandler()

И случайно импортировать всё содержимое модуля с такой подлянкой в другом модуле:

from myapp.myhandlers import *

Чтобы потом долго удивляться, почему встроенные функции len() и print() в одних местах неожиданно изменили своё поведение, а в других нет.

Поставить IDE с плагинами для Python? Установить линтер и вызывать его из консоли? Наконец, просто вникнуть в основы языка? Нет, это всё путь для слабаков. Настоящие мужики идут на Хабр Q&A и пишут вопрос "Почему мой кот не работает? Памагите!" =))

Ну в питоне никакое IDE не позволит отличить в данном случае plane от Plane. Так как это просто две одинаковые переменные, различающиеся только присвоенными им значениями.

Вы можете использовать аннотации типов. Тогда и развитые IDE, и type checker`ы помогут Вам быстро найти многие банальные ошибки.

Думаю, для более-менее сложной кодовой базы, аннотации типов обязательны. Иначе Вы всегда рискуете что-то упустить и словить в production какой-нибудь TypeError: 'NoneType' object is not callable.

В production такая ситуация должна обрабатываться автоматически в динамике. Иначе вообще зачем вам динамический язык?

Type checker в принципе указанную проблему не решает, а просто снижает вероятность ошибки методом отсечения некоторых самых простых. Эти все костыли проходили ещё в 1980 году, когда придумывали Аду.

В сколь-либо сложной кодовой базе ошибки всё равно происходят, просто они парируются обработкой и на функциональности это не сказывается. Если, например, почитать системный журнал ОС с таких позиций, так можно в ужас прийти. Там этих null pointer'ов преизрядно.

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

Думаю для начала тут нужно сравнение двух языков программирования.

JavaScript создавался для маленьких скриптов, что не должны ломать работу других скриптов на веб-странице. Поэтому там реализовали динамическую и слабую типизация. Язык сам пытается хоть как-то привести типы, прощая разработчику даже очень грубые ошибки.

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

Далее. В маленькой кодовой базе, эта особенность Python легко компенсируется внимательностью и хотя бы небольшим опытом разработчика. Если же проект превысил условные пять тысяч строк кода и зависит от дюжины сторонних библиотек, разработчик обязательно что-то упустит. И хорошо если это упущение когда-нибудь приведёт к падению сервиса, что отразится в логах внятной ошибкой и трассировкой стэка вызовов. Так, для веб-сервисов принято делать автоматический перезапуск в случае падения, а один необработанный запрос пользователя обычно некритичен.

Хуже, когда разработчик одной части программы считал очевидным, что деньги надо считать только в типе Decimal (с повышенной точностью дробной части), а потому решил не добавлять ни приведение аргументов к типу Decimal, ни проверку типу аргументов. А разработчик другой части программы был не в курсе и решил везде передавать деньги типом float, что славится своими ошибками округления. И ведь падать тут нечему, оба типа поддерживают арифметические операции. Вот если бы первый разработчик добавил аннотации типов, то все места с данной ошибкой можно было бы найти с помощью type checker`а (например, с помощью MyPy). Особенно, если проверки type checker`ом автоматизированы для Git репозитория с проектом.

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

Смысл статической проверки типов в том, чтобы ограничить проверки времени исполнения преимущественно в местах ввода данных. Если проверки времени исполнения недостаточны или некорректны, проверка type checker`ом может вывести, что такие-то функции и методы могут получить аргументы недопустимых типов или значений. Доработали проверки в местах ввода, type checker ничего не нашёл -- отлично, теперь можно переходить к написанию юнит-тестов =))

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

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

Ну и распространённость Python как языка программирования обусловлена далеко не столько самой динамической типизацией, сколько механизмами, конструкциями и синтаксическим сахаром во многом выстроенными поверх динамической типизации. Например, из динамической типизации вырастают функции высшего порядка, а из функций высшего порядка -- декораторы.

Типы параметров можно в питоне продекламировать без всякого type checker'а. Только это всё не панацея от ошибок.

Один разработчик может передавать данные в метрах, а другой ждать в километрах, ну и дальше что? Эти вопросы типизацией не вывезти.

Вещи, о которых вы пишете, мы читали у Вирта и его единомышленников (которые в типичном случае сами никогда не занимались промышленной разработкой) ещё несколько десятков лет назад. Механизмы для сильной типизации с тех пор развились преизрядно, а ошибки в программах никуда не делись. И даже ракета Ariane как-то упала, откинув type cast exception.

Не всё тут так просто и нуждается в принятии на веру. Я тоже в 20 лет эти мантры воспринимал некритически.

А в реальности что? Главным образом, работают десятилетиями чудовищно написанные, но хорошо оттестированные и отлаженные программы. Вам или мне это может не нравиться, но это так. Одновременно с этим, программисты на условном С++ тратят своё рабочее время, обеспечивая формальную интерфейсную совместимость кода, большинство из которого они через полчаса выкинут.

Типы параметров можно в питоне продекламировать без всякого type checker'а.

Мне удобно и приятно пользоваться MyPy. Чувства -- это первичный источник информации. Даже если Вы скажете, что без type checker`а удобнее -- это не переубедит меня, так как не совпадает с моим многолетним опытом программирования на Python.

Только это всё не панацея от ошибок.

С одной стороны, мир -- это иерархия систем. С другой стороны, мир -- это динамический хаос, порождающий и растворяющий системы. С третьей стороны, системы не имеют чётких границ и зон ответственности. Поэтому у Вас нет и не будет ни измерительных приборов, ни вычислительных мощностей для абсолютно точного предсказания поведения реальных систем, их взаимодействия и вытекающих из него побочных эффектов. Единственный возможный подход -- это исходить из того, что вероятность любого события строго больше 0% и строго меньше 100%.

В этом смысле, я не понимаю с чего вдруг в данной физической реальности возможно построение рукотворных информационных систем на 100% защищённых от ошибок? Если аппаратное обеспечение по определению не защищено от ошибок, если люди по определению не защищены от ошибок, то с чего вдруг возможна разработка совершенного программного обеспечения? Сама постановка такой задачи звучит безумно.

Куда реалистичнее выглядит задача повышения надёжности программного обеспечения. Каждый известный метод решения этой задачи снижает вероятность ошибки. Но комбинируя различные методы, мы можем складывать их достоинства и вычитать их недостатки. То есть, комплексным подходом мы можем добиться куда большей надёжности систем, чем если бы полагались на какую-то одну "серебряную пулю".

По моему опыту, аннотации типов (mypy) в сочетании с unit тестами (pytest) могут сильно облегчить отладку, сопровождение, рефакторинг и развитие даже небольших Python проектов от нескольких сотен строк. Особенно, если Вы будете возвращаться к проекту раз в полгода.

Главным образом, работают десятилетиями чудовищно написанные, но хорошо оттестированные и отлаженные программы.

Многие стремятся занять новую нишу раньше конкурентов, а потому неизбежно жертвуют качеством кодовой базы. И это по-своему оправдано, так как занятая ниша может стать источником ресурсов для дальнейшей разработки, рефакторинга кодовой базы или даже переписывания "с нуля".

Повышение надёжности программы и отсутствие в ней ошибок - разные вещи. Коррелированные в некоторой степени, но не более того.

Только пассивным снижением вероятности ошибок вы надёжность сильно не повысите. Зато трудоёмкость можно сильно повысить. Поэтому всё хорошо в меру.

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

Даже если речь о написании программы на Rust без unsafe блоков и с глубоким пониманием Domain Driven Design и Clean Architecture.

Например, слишком медленный темп и слишком высокие трудозатраты на разработку -- это тоже ошибки.

Хорошо то, что оптимально в соответствующем контексте.

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

А я разве где-то утверждал обратное?

Программу без ошибок написать практически невозможно, но вот стопроцентно надёжную – вполне осуществимо в ряде случаев.

Если поддержать поднятый вами философский контекст, то это возможно за счёт такого свойства сложных систем, как самоорганизация.

В деле написания программ без ошибок больших достижений достиг Дональд Кнут, но реальные пути повышения надёжности совсем другие.

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

Программу без ошибок написать практически невозможно, но вот стопроцентно надёжную – вполне осуществимо в ряде случаев.

Если поддержать поднятый вами философский контекст, то это возможно за счёт такого свойства сложных систем, как самоорганизация.

Все вымершие виды животных были самоорганизующимися системами с несколькими уровнями организации (как минимум, уровни клеток, особей и популяций). В этом смысле, я бы не рискнул говорить о 100% надёжности самоорганизующихся систем.

Как правило, чем дольше среда находится в стабильном состоянии, тем больше самоорганизующаяся система адаптирует свой гомеостаз исключительно под имеющуюся среду, утрачивая "экономически нецелесообразные" качества.

Так маврикийский дронт и родригесский дронт как виды успешно избавились от "экономически нецелесообразной" способности к полёту и успешно повышали стабильность своего гомеостаза... Но ровно до эпохи географических открытий.

Ещё примечательны социальные насекомые. Их гомеостаз потрясающе стабилен. Особи отвечающие за размножение большую часть времени хорошо защищены от опасностей окружающей среды сотнями, тысячами, а порой и миллионами других особей. Однако, такое разделение ответственностей привело социальных насекомых к резкому ослаблению естественного отбора и замедлению эволюции.

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

В этом смысле, наиболее "надёжна" та программа, что постоянно копирует себя со случайными ошибками, придумывает себе самые безумные сценарии использования, тестирует свои копии на соответствие этим сценариям, а затем отправляет в /dev/null наименее удачные копии. Вот только я не готов признать подобную программу на 100% надёжной.

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

Кажется Конрад Лоренц как-то сказал "червяк может извиваться как угодно, но не может стоять". То есть, отсутствие структуры может лишать качественно новых свобод, но обретение структуры может лишить базовых свобод. В этом смысле, нужно взвешивать, что нам важнее -- возможность извиваться как угодно или, скажем, способность к полёту?

Я не сказал бы, что аннотации типов в Python (тем более с Any, TypeVar, Generic и Protocol) заметно влияют на адаптивность кодовой базы. Тем более, если type checker оказался в корне неправ, можно явно указать действительный тип объекта с помощью cast() или игнорировать определённые места с помощью комментария # type: ignore .

Аннотации типов можно вообще не использовать или использовать выборочно. Можно использовать для статических проверок или для построения декларативных API, в том числе для (полу)автоматической и параметризуемой генерации производного кода.

Да, в том же Django, без всяких аннотаций типов, многое можно генерировать на основе моделей Django ORM. Но, по ряду причин, я бы предпочёл декларативные API, использующие аннотации типов вместо операции присвоения. Во-первых, ради совместимости с type checker`ами. Во-вторых, было бы странно использовать модели веб-фреймворка Django для генерации форм ввода в графических приложениях на Kivy, Python GTK+3 или PySide 2.

P.S. Впрочем, и завязываться на Pydantic как зависимость я бы не рискнул. Хотя бы потому, что сопровождающий проекта позволил себе неглядя закрыть несколько сотен накопившихся issues от пользователей. Мол, нет времени всё читать, если что-то важное -- откройте новый issue.

Один разработчик может передавать данные в метрах, а другой ждать в километрах, ну и дальше что? Эти вопросы типизацией не вывезти.

Вообще-то как раз это и можно. Например.

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

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

Думаю для начала тут нужно сравнение двух языков программирования.

JavaScript создавался для маленьких скриптов, что не должны ломать работу других скриптов на веб-странице. Поэтому там реализовали динамическую и слабую типизация. Язык сам пытается хоть как-то привести типы, прощая разработчику даже очень грубые ошибки.

Я не очень понял, причём тут JavaScript, но исторически первым языком со слабой типизацией при крайне мощной системе типов (то есть с фактически универсальным приведением типов по умолчанию) был PL/I, который задумывался, как язык вообще для всего, и на котором было понаписано этого всего неимоверно много, причём именно в сегменте High Availability, на мейнфреймах. Вышел он из употребления по причине сложности изучения языка и написания компиляторов, а не потому, что были какие-то претензии к надёжности программ.

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

Что бы скомпилировать вызов метода в машинном коде, нужно знать его адрес. Как узнать адрес метода, если его нет?

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

Можете считать, что с точки зрения компилятора у смоллтоковского класса Object, наследниками которого являются все объекты, есть один метод высшего порядка вызватьМетод (имя, аргументы...)

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

В этом и есть разница между языками. Смоллток затратит на этот вызов сотни тактов процессора, а может и тысячи с учетом обращений к памяти и ее ожидания. А С++ - всего два такта на команду call. Потому что Смолтолк построен "от человека", а С++ - "от машины". Либо программируем сложнее, но код будет эффективен, либо проще, но с потерей эффективности. Выбор за вами.

(Кстати, там нет таблицы адресов, если это не виртуальный метод. Будет просто машинная команда call по фиксированному адресу. Ну да, положит в стек адрес объекта, еще пара тактов).

Сейчас считать такты неуместно, при чудовищной неэффективности общеупотребительных фреймворков. Кто думает о тактах, те программируют на чистом Си или ассемблере.

С этим я согласен. Тогда можно вернуться к вопросу о критике языков, которые не могут скомпилировать не существующий метод - зачем эта критика? Эти языки используют те, кто считает такты. А кто не считает - используют Смолтолк или тот же Питон :)

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

Это поверхностное объяснение. Люди, которые считают такты, вообще по ряду причин не используют ООП.

Я написал, что лично мне больше нравится объектный подход смоллтока, чем подход C++. Более того, только познакомившись со смоллтоком, я оценил достоинства ООП вообще.

На самом деле, популярность C++ связана с взаимоусиливающими друг друга силой традиции и количеством библиотек и прочего легаси.

"Считать такты" - это утрировано. Кто их действительно считает, наверное ООП действительно не нужно.

Но я допускаю, что существуют применения, где нужен эффективный в плане ресурсов и быстродействия язык с ООП. Правда, я не уверен, что С++ еще в этой нише. Мне нравился С++, когда он еще был "Си с классами", это 90-е годы. Что он представляет собой сейчас - тут ничего не могу сказать, у меня опыт в этом языке только тех лет.

Сейчас бы я выбрал просто Си, если нужен эффективный по быстродействию код, а для остального - Питон. Ну это мои личные предпочтения.

Правило №3: давайте модулям имена, представляющие собой существительные во множественном числе

Довольно спорный момент, как и в вопросе именования таблиц в базах данных.

Например, если Вы используете множественное число, то список модулей пакета (или список таблиц БД) в алфавитном порядке может выглядеть неправильно упорядоченным:

mypackage
├── myproject
    ├── companies.py  # компании
    ├── companyemployees.py # сотрудники компаний
    ├── companyprofiles.py # профили компаний
    ├── userprofiles.py # профили пользователей
    ├── users.py # пользователи
    ├── userwallets.py # кошельки пользователей

При этом, с единственным числом такой проблемы нет:

mypackage
├── myproject
    ├── company.py  # тип компания
    ├── companyemployee.py # тип сотрудник компании
    ├── companyprofile.py # тип профиль компании
    ├── user.py # тип пользователь
    ├── userprofile.py # тип профиль пользователя
    ├── userwallet.py # тип кошелёк пользователя

Классы, представляющие нечто из бизнес-среды, должны называться в
соответствии с названиями связанных с ними сущностей (и имена должны
быть существительными!)

Именование классов существительными, а методов и функций глаголами я бы не воспринимал как строгое правило. Глаголы указывают на активности и процессы, а активности и процессы могут иметь как параметры инициализации, так и хранимое состояние (например, время начала и время завершения). И, скажем, имя класса CompanyProfileUpdate выглядит чуть менее громоздким, чем CompanyProfileUpdateActivity.

На сайте W3C есть пример целой системы типов, что изобилует глаголами. Однако, объекты таких типов имеют атрибуты, изменяемое состояние и могут храниться в СУБД.

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

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

Переходить к классам имеет смысл там, где логика работы программы начинает зависеть от хранимого состояния (настроек, параметров инициализации, динамически переключаемых флагов и пр.).

Например, если у Вас логика работы двух функций зависит от значения глобальной константы DEBUG_MODE -- возможно Вам нужен класс App с двумя методами и атрибутом debub_mode, чьё значение передаётся в __init__(self, debug_mode: bool).

НЛО прилетело и опубликовало эту надпись здесь

Единственный минус такого подхода заключается в том, что, без дополнительных усилий, не получится воспользоваться в своём коде командой вида import module_a. Для этого потребуется кое-что сделать. <…>

Проблема хорошо решается вызовом pip install -e . в окружении, в котором вы работаете (pyenv/conda/etc). В этом случае можно просто импортировать тестируемый пакет в тесты.

Не совсем понял, зачем внутри src находятся тесты. Они сами по себе не то чтобы являются исходным кодом пакета. Плюс, если использовать пример setup.py из https://github.com/pypa/sampleproject, где есть строчка packages=find_packages(where="src"), то при упаковке пакета папка test попадёт внутрь. Нужна ли она пользователю пакета? Мне так не кажется. Хотя при более тонкой настройке packages, естественно, она в пакет не попадёт.

В остальном приличный гайд. Советы полезные.

Имя src мне не нравится, это что-то техническое. Я предпочитаю использовать название проекта. То есть вместо src/gmaps_crawler/ просто gmaps_crawler/.

Есть еще и личная неприязнь к этому имени в контексте Питона, src ассоциируется с Джавой.
Я пишу и на Питоне и на Джаве, но попытки притащить парадигмы из одного языка в другой мне не нравятся.

Это не попытки притащить парадигмы из одного языка в другой. src-layout – это довольно распространенный паттерн Python проектов, который призван решать некоторые проблемы при разработке приложений. Подробней об этом можно узнать здесь:

  1. https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#choosing-a-test-layout-import-rules

  2. https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure

Первый вариант который рекомендует pytest, мне привычнее, и кажется проще для разработки. Я локально гоняю только юниты, а всякие инсталляции можно сбросить на CI.

Вторая статья еще использует setup.py, но с тех пор появились новые инструменты например flit и poetry. Надо посмотреть, как они с такими структурами работают.


Весьма естественным кажется такой импорт классов и функций:

from gmaps_crawler.storages import get_storage
from gmaps_crawler.entities import Place
from gmaps_crawler.exceptions import CantEmitPlace

При такой структуре проекта в файле __main__.py и модулях возможен импорт только вида:

from main import main

if __name__ == "__main__":
	main()

(в файле main.py есть функция main)

А в вашем импорте возникнет исключение вида:

ModuleNotFoundError: No module named '...'

Из примечательного: при импорте

from .main import main

Будет исключение само-импорта, т.к. при выполнении модуля запускаемый файл выполняется, как если бы он был в корне проекта (независимо от его расположения в проекте).

ImportError: attempted relative import with no known parent package

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