Каждая минута, потраченная на организацию своей деятельности, экономит вам целый час.
Бенджамин Франклин

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

Python — это самый гибкий язык программирования из тех, с которыми мне приходилось сталкиваться. А когда имеешь дело с чем-то «слишком гибким» — возрастает вероятность принятия неправильных решений.

  • Хотите держать все классы проекта в единственном файле main.py? Да, это возможно.

  • Надо читать переменную окружения? Берите и читайте там, где это нужно.

  • Требуется модифицировать поведение функции? Почему бы не прибегнуть к декоратору!?

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

Но, если вы точно знаете о том, что делаете, последствия гибкости Python не обязательно окажутся плохими.

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

Структура Python-проекта

Сначала обратим внимание на структуру директорий проекта, на именование файлов и организацию модулей.

Рекомендую держать все файлы модулей в директории src, а тесты — в поддиректории tests этой директории:

<project>
├── src
│   ├── <module>/*
│   │    ├── __init__.py
│   │    └── many_files.py
│   │
│   └── tests/*
│        └── many_tests.py
│
├── .gitignore
├── pyproject.toml
└── README.md

Здесь <module> — это главный модуль проекта. Если вы не знаете точно — какой именно модуль у вас главный — подумайте о том, что пользователи проекта будут устанавливать командой pip install, и о том, как, по вашему мнению, должна выглядеть команда import для вашего модуля.

Часто имя главного модуля совпадает с именем всего проекта. Но это — не некое жёсткое правило.

Аргументы в пользу директории src

Я видел множество проектов, устроенных по-другому.

Например, в проекте может отсутствовать директория src, а все модули будут просто лежать в его корневой директории:

non_recommended_project
├── <module_a>/*
│     ├── __init__.py
│     └── many_files.py
│
├── .gitignore
│
├── tests/*
│    └── many_tests.py
│
├── pyproject.toml
│
├── <module_b>/*
│     ├── __init__.py
│     └── many_files.py
│
└── README.md

Уныло смотрится проект, в структуре которого нет никакого порядка из-за того, что его папки и файлы просто расположены по алфавиту, в соответствии с правилами сортировки объектов в IDE.

Главная причина, по которой рекомендуется пользоваться папкой src, заключается в том, чтобы активный код проекта был бы собран в одной директории, а настройки, параметры CI/CD, метаданные проекта находились бы за пределами этой директории.

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

Именование файлов

Правило №1: тут нет файлов

Во-первых — в Python нет таких сущностей, как «файлы», и я заметил, что это — главный источник путаницы для новичков.

Если вы находитесь в директории, содержащей файл __init__.py, то это — директория, включающая в себя модули, а не файлы.

Рассматривайте каждый модуль, как пространство имён.

Я говорю о «пространстве имён», так как нельзя сказать с уверенностью — имеется ли в модуле множество функций и классов, или только константы. В нём может присутствовать практически всё что угодно, или лишь несколько сущностей пары видов.

Правило №2: если нужно — держите сущности в одном месте

Совершенно нормально, когда в одном модуле имеется несколько классов. Так и стоит организовывать код (но, конечно, только если классы связаны с модулем).

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

Часто встречается мнение, что это — пример неудачного приёма работы. Те, кто так считают, находятся под влиянием опыта, полученного после использования других языков программирования, которые принуждают к другим решениям (например — это Java и C#).

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

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

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

Реальный пример именования модулей

Вот мой проект — Google Maps Crawler, созданный в качестве примера.

Этот проект направлен на сбор данных из Google Maps с использованием Selenium и на их представление в виде, удобном для дальнейшей обработки (тут, если интересно, можно об этом почитать).

Вот текущее состояние дерева проекта (тут выделены исключения из правила №3):

gmaps_crawler
├── src
│   └── gmaps_crawler
│        ├── __init__.py
│        ├── config.py (форма единственного числа)
│        ├── drivers.py
│        ├── entities.py
│        ├── exceptions.py
│        ├── facades.py
│        ├── main.py (форма единственного числа)
│        └── storages.py
│
├── .gitignore
├── pyproject.toml
└── README.md

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

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

Можно понять, что в exceptions может иметься как один, так и множество классов исключений.

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

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

  • Их, если нужно, в любой момент можно разбить на более мелкие модули.

  • «Множественные» имена дают программисту сильное ощущение того, что он знает о том, что может быть внутри соответствующих модулей.

Именование классов, функций и переменных

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

Имена функций и методов должны быть глаголами

Функции и методы представляют собой действия, или нечто, выполняющее действия.

Функция или метод — это не просто нечто «существующее». Это — нечто «действующее».

Действия чётко определяются глаголами.

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

def get_orders():
    ...
def acknowledge_event():
    ...
def get_delivery_information():
    ...
def publish():
    ...

А вот — несколько неудачных примеров:

def email_send():
    ...
def api_call():
   ...
def specific_stuff():
   ...

Тут не очень ясно — возвращают ли функции объект, позволяющий выполнить обращение к API, или они сами выполняют какие-то действия, например — отправку письма.

Я могу представить себе такой сценарий использования функции с неудачным именем:

email_send.title = "title"
email_send.dispatch()

У рассмотренного правила есть и некоторые исключения:

  • Создание функции main(), которую вызовут в главной точке входа приложения — это хороший повод нарушить это правило.

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

Имена переменных и констант должны быть существительными

Имена переменных и констант всегда должны быть существительными и никогда — глаголами (это позволяет чётко отделить их от функций).

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

plane = Plane()
customer_id = 5
KEY_COMPARISON = "abc"

Вот — неудачные имена:

fly = Plane()
get_customer_id = 5
COMPARE_KEY = "abc"

А если переменная или константа представляют собой список или коллекцию — им подойдёт имя, представленное существительным во множественном числе:

planes: list[Plane] = [Plane()] # Даже если содержит всего один элемент
customer_ids: set[int] = {5, 12, 22}
KEY_MAP: dict[str, str] = {"123": "abc"} # Имена словарей остаются существительными в единственном числе

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

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

Всегда давайте классам имена в единственном, а не во множественном числе. Имена во множественном числе напоминают имена коллекций элементов (например — если я вижу имя orders, то я полагаю, что это — список или итерируемый объект). Поэтому, выбирая имя класса, напоминайте себе, что после создания экземпляра класса в нашем распоряжении оказывается единственный объект.

Классы представляют собой некие сущности

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

Пример использования суффиксов

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

Кто-то может решить, что он может олицетворять некую сущность:

email = Email() # Предполагаемый пример использования
email.title = "Title"
email.body = create_body()
email.send_to = "guilatrova.dev"

send_email(email)

Такой класс следует назвать EmailSender или EmailService.

Соглашения по именованию сущностей

Следуйте этим соглашениям по именованию сущностей:

Тип

Общедоступный

Внутренний

Пакеты (директории)

lower_with_under

Модули (файлы)

lower_with_under.py

Классы

CapWords

Функции и методы

lower_with_under()

_lower_with_under()

Константы

ALL_CAPS_UNDER

_ALL_CAPS_UNDER

Отступление о «приватных» методах

Если имя метода выглядит как __method(self) (любой метод, имя которого начинается с двух символов подчёркивания), то Python не позволит внешним классам/методам вызывать этот метод обычным образом. Некоторые, узнавая об этом, считают, что это нормально. 

Тем, кто, вроде меня, пришёл в Python из C#, может показаться странным то, что (пользуясь вышеприведённым руководством) метод класса нельзя защитить.

Но у Гвидо ван Россума есть достойная причина считать, что на это есть веские основания: «Мы все тут взрослые, ответственные люди».

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

В конце концов, если вы и правда решите вызвать некий приватный метод, то вы для этого сделаете что-то неординарное (в C# это называется Reflection).

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

Когда создавать функцию, а когда — класс?

Мне несколько раз задавали вопрос, вынесенный в заголовок этого раздела.

Если вы следуете рекомендациям, приведённым выше, то ваши модули будут понятными, а понятные модули — это эффективный способ организации функций:

from gmaps_crawler import storages

storages.get_storage()  # Похоже на класс, но экземпляр не создаётся, а имя - это существительное во множественном числе
storages.save_to_storage()  # Так может называться функция, хранящаяся в модуле

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

Пример группировки подмножества функций

Предположим, имеется уже встречавшийся нам модуль storages с 4 функциями:

def format_for_debug(some_data):
    ...

def save_debug(some_data):
    """Выводит данные на экран"""
    formatted_data = format_for_debug(some_data)
    print(formatted_data)


def create_s3(bucket):
    """Создаёт бакет s3, если он не существует"""
    ...

def save_s3(some_data):
    s3 = create_s3("bucket_name")
    ...

S3 — это облачное хранилище Amazon (AWS), подходящее для хранения любых данных. Это — нечто вроде Google Drive для программ.

Проанализировав этот код, мы можем сказать следующее:

  • Разработчик может сохранять данные в режиме отладки (save_debug) (они просто выводятся на экран), или в S3 (save_s3) (они попадают в облако).

  • Функция save_debug использует функцию format_for_debug.

  • Функция save_s3 использует функцию create_s3.

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

class DebugStorage:
    def format_for_debug(self, some_data):
        ...

    def save_debug(self, some_data):
        """Выводит данные на экран"""
        formatted_data = self.format_for_debug(some_data)
        print(formatted_data)


class S3Storage:
    def create_s3(self, bucket):
        """Создаёт бакет s3, если он не существует"""
        ...

    def save_s3(self, some_data):
        s3 = self.create_s3("bucket_name")
        ...

Вот эмпирическое правило, помогающее решить вопрос о функциях и классах:

  • Всегда начинайте с функций.

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

Создание модулей и точки входа в приложение

У каждого приложения есть точка входа.

То есть — имеется единственный модуль (другими словами — файл), который запускает приложение. Это может быть как отдельный скрипт, так и большой модуль.

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

def execute_main():
    ...


if __name__ == "__main__":  # Добавьте это условие
    execute_main()

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

Файл __main__.py

Вы, возможно, заметили, что некоторые Python-пакеты можно вызывать, пользуясь ключом -m:

python -m pytest
python -m tryceratops
python -m faust
python -m flake8
python -m black

Система относится к таким пакетам почти как к обычным утилитам командной строки, так как запускать их ещё можно так:

pytest
tryceratops
faust
flake8
black

Для того чтобы оснастить ваш проект такой возможностью — нужно добавить файл __main.py__ в главный модуль:

<project>
├── src
│   ├── example_module Главный модуль
│   │    ├── __init__.py
│   │    ├── __main__.py Добавьте сюда этот файл
│   │    └── many_files.py
│   │
│   └── tests/*
│        └── many_tests.py
│
├── .gitignore
├── pyproject.toml
└── README.md

И не забудьте, что и тут, в файле __main__.py, понадобится проверка __name__ == "__main__".

Когда вы установите свой модуль — вы сможете запускать его командой вида python -m example_module.

О, а приходите к нам работать? ???? ????

Мы в wunderfund.io занимаемся высокочастотной алготорговлей с 2014 года. Высокочастотная торговля — это непрерывное соревнование лучших программистов и математиков всего мира. Присоединившись к нам, вы станете частью этой увлекательной схватки.

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

Сейчас мы ищем плюсовиков, питонистов, дата-инженеров и мл-рисерчеров.

Присоединяйтесь к нашей команде.

Комментарии (30)


  1. Alek_roebuck
    25.07.2022 12:18
    +2

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

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


    1. sintez
      25.07.2022 23:12
      +1

      Автор видимо перепутал с Java


  1. vadimr
    25.07.2022 12:25

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

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

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

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


    1. v131v
      25.07.2022 16:46
      +6

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


      1. vadimr
        25.07.2022 23:28
        -1

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


  1. vadimr
    25.07.2022 14:42
    -4

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

    plane = Plane()

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

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

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

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


    1. ri_gilfanov
      25.07.2022 18:43
      +1

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

      len = MyLengthCalcHandler()
      print = MyOutputHandler()

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

      from myapp.myhandlers import *

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

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


      1. vadimr
        25.07.2022 23:37

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


        1. ri_gilfanov
          25.07.2022 23:51
          +1

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

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


          1. vadimr
            26.07.2022 00:18
            -3

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

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

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

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


            1. ri_gilfanov
              26.07.2022 07:24
              +2

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

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

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

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

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

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

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

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

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

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


              1. vadimr
                26.07.2022 09:21

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

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

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

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

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


                1. ri_gilfanov
                  26.07.2022 18:40

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

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

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

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

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

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

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

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

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


                  1. vadimr
                    26.07.2022 19:08

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

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


                    1. ri_gilfanov
                      26.07.2022 20:16

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

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

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

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


                      1. vadimr
                        26.07.2022 20:19

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

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

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

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

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

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


                      1. ri_gilfanov
                        27.07.2022 03:24

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

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

                        Все вымершие виды животных были самоорганизующимися системами с несколькими уровнями организации (как минимум, уровни клеток, особей и популяций). В этом смысле, я бы не рискнул говорить о 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.


                1. AnthonyMikh
                  27.07.2022 14:31

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

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


                  1. vadimr
                    27.07.2022 14:34

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

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


              1. vadimr
                26.07.2022 13:19

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

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

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


            1. kuza2000
              27.07.2022 18:23

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

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


              1. vadimr
                27.07.2022 18:32

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

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

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


                1. kuza2000
                  27.07.2022 20:14

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

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


                  1. vadimr
                    27.07.2022 20:49

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


                    1. kuza2000
                      27.07.2022 21:52

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

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


                      1. vadimr
                        27.07.2022 22:02

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

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

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


                      1. kuza2000
                        27.07.2022 22:39

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

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

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


  1. ri_gilfanov
    25.07.2022 21:12
    +1

    Правило №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).


  1. Yuribtr
    26.07.2022 09:51
    +2

    Кто-то может решить, что он может олицетворять некую сущность:

    email = Email() # Предполагаемый пример использования
    email.title = "Title"
    email.body = create_body()
    email.send_to = "guilatrova.dev"
    send_email(email)

    Такой класс следует назвать EmailSender или EmailService

    Вот тут я завис. EmailSender как видно из названия, сам отправляет письма. А в коде мы видим какой то DTO объект, который затем передаётся в функцию send_email. Если бы у этого класса был реализован свой метод send_email - я бы согласился. А так непонятно почему автор решил что EmailSender лучше чем Email


  1. looogle
    26.07.2022 17:34

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

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

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

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