Это правила, по которым я пишу код. В основе лежит книга Роберта Мартина “Чистый код” и PEP 8 для Python. Документ живой, я дополняю его по мере того, как набиваю шишки.
Главная мысль одна: код читают намного чаще, чем пишут. Поэтому я пишу его для человека, который откроет файл через полгода. Часто этот человек я сам.
0. Без ТЗ результат ХЗ#
Любая работа начинается с требований, а не с кода.
- Сначала фиксирую, какую проблему решаем и как поймем, что она решена.
- Потом архитектура: компоненты, данные, границы между ними.
- Только после этого код. Если в процессе требования меняются, сначала правлю ТЗ, потом код.
1. Стиль кода: PEP 8 + мои правила#
PEP 8 это базовый стандарт. Мои правила работают поверх него, как каскад в CSS: все, что я не переопределил, наследуется от PEP 8 как есть. Переопределение одно.
Табуляция вместо пробелов. Один уровень вложенности это один символ. Каждый видит отступ той ширины, которую выставил у себя в редакторе, а в файле при этом ничего не меняется. Пробелы и табы в одном файле не смешиваю никогда, Python 3 это и не позволит.
Все остальное по PEP 8: длина строки, пустые строки между функциями и классами, порядок импортов, пробелы вокруг операторов.
Чтобы не держать это в голове, стиль проверяет и правит инструмент. Я использую ruff:
# pyproject.toml
[tool.ruff]
line-length = 100
[tool.ruff.format]
indent-style = "tab"
[tool.ruff.lint]
select = ["E", "F", "I", "N", "B", "UP"]
ignore = ["W191"] # W191 ругается на табы, у нас это осознанное решениеIDE: JetBrains (PyCharm). В настройках проекта включены табы и запуск ruff при сохранении.
2. Имена#
Имя должно отвечать на три вопроса: зачем это существует, что делает и как используется. Если для имени нужен комментарий, значит имя плохое.
- Переменные и функции в
snake_case, классы вPascalCase, константы вUPPER_CASE. - Функции называю глаголом:
fetch_page,parse_price,send_report. Классы и переменные существительным:Browser,proxy_pool. - Без сокращений и однобуквенных имен. Исключение:
iв коротком цикле иeдля исключения. - Одно понятие, одно слово. Если в проекте есть
fetch, то не появляются рядомget,loadиretrieveдля того же самого. - Без кодирования типа в имени:
users, а неusers_listилиlst_users. - Булевы переменные читаются как вопрос:
is_ready,has_proxy,can_retry.
3. Функции#
- Маленькие. Функцию должно быть видно целиком на экране без прокрутки. Если не видно, ее можно разбить.
- Делают одно дело. Если функцию нельзя описать одним предложением без слова “и”, это две функции.
- Один уровень абстракции. Функция либо управляет процессом и вызывает другие функции, либо работает с деталями. Не одновременно.
- Мало аргументов. Идеально ноль-два. Три и больше это повод собрать их в dataclass.
- Без флагов в аргументах.
render(page, True)непонятно читается. Лучше две функции:render_fullиrender_preview. - Без скрытых побочных эффектов. Если функция называется
check_session, она не должна по дороге создавать новую сессию. - Команда или запрос. Функция либо меняет состояние, либо возвращает данные. Не то и другое сразу.
4. Комментарии#
Лучший комментарий это тот, который не понадобился, потому что код и так понятен.
Пишу комментарий, когда нужно объяснить почему, а не что:
# Cloudflare отдает 403 на первый запрос без cookie, поэтому всегда делаем два
response = session.get(url)Не пишу:
- комментарии, которые пересказывают код
- закомментированный код. Для истории есть git
- журнал изменений и авторство в шапке файла. Это тоже работа git
Docstring обязателен для публичных функций и классов модуля. Для внутренних по ситуации.
5. Обработка ошибок#
- Исключения вместо кодов возврата. Функция не возвращает
Noneили-1, если что-то пошло не так, она бросает исключение. - Ловлю конкретные исключения, никогда голый
except:илиexcept Exception: pass. - Свои исключения для своей предметной области:
ProxyBannedError,CaptchaError. По ним понятно, что случилось, без чтения стектрейса. - Не возвращаю
Noneтам, где ждут коллекцию. Пустой список лучше, чем проверка наNoneв каждом месте вызова. - Ошибку обрабатываю там, где знаю, что с ней делать. Если не знаю, пропускаю выше.
6. Классы и модули#
- Маленькие классы с одной ответственностью. У класса должна быть одна причина для изменения. Если класс и ходит в сеть, и парсит HTML, и пишет в базу, это три класса.
- Высокая связность. Методы класса работают с его полями. Если метод не трогает
self, ему, скорее всего, не место в этом классе. - Зависимости снаружи. Класс получает браузер, сессию или клиент базы в конструкторе, а не создает их сам. Так его легко тестировать и подменять.
- Закон Деметры. Общаюсь только с ближайшими объектами.
order.customer.address.cityэто знак, что нужен метод. - Границы с чужим кодом. Сторонние библиотеки оборачиваю в свой тонкий слой. Если библиотеку придется заменить, правка будет в одном месте.
7. Тесты#
- Тесты это тоже код и к ним те же требования к чистоте.
- Тест проверяет одну вещь, и это видно по его имени:
test_returns_empty_list_when_page_has_no_items. - Структура каждого теста: подготовка, действие, проверка.
- Тесты быстрые, независимые друг от друга и дают одинаковый результат при каждом запуске. Никаких походов в реальный интернет в юнит-тестах.
- Баг сначала воспроизвожу тестом, потом чиню.
8. Правило бойскаута#
Оставляю код чище, чем он был до меня. Не нужно переписывать все сразу. Достаточно переименовать одну непонятную переменную или вынести один кусок в функцию каждый раз, когда трогаю файл.
9. Git#
- Один коммит, одно логическое изменение.
- Сообщение коммита в формате conventional commits:
feat:,fix:,refactor:,docs:,chore:. - В основную ветку не попадает код, который не проходит линтер и тесты.
- Секреты, ключи и
.envникогда не попадают в репозиторий.
