Требования к коду на Python

Требования к коду на Python, действующие в этой книге, рекомендуется соблюдать и в собственных проектах.

Слайды к главе. Требования к коду и их автоматическая проверка (линтер, форматтер, хук pre-commit) изложены также в лекции «Жизненный цикл ПО» с демонстрациями в терминале; слайды лекции доступны на сайте книги и в PDF.

Требования следующие:

  • Строгое соответствие PEP 8.
  • Длина строки — 79 символов.
  • Импорты корректно отсортированы, неиспользуемых импортов нет.
  • Отступы делаются в 4 пробела.
  • Переносы строк выполнены с правильными отступами.
  • Обратные слеши для переносов не используются.
  • Консистентность (одинаковые кавычки, одинаковые способы решения одинаковых задач).
  • Отсутствие закомментированного кода и стандартных комментариев-заглушек (# Create your views here. и т. п.).
  • Комментарии к функциям оформлены как docstring в соответствии с Docstring Conventions, то есть начинаются с заглавной буквы, заканчиваются точкой и содержат описание того, что делает функция.
  • Комментарии к коду краткие и информативные.
  • Длинные куски кода логически разделены пустыми строками, как абзацы в тексте.
  • Нет лишних операций.
  • Нет лишних else (если в if происходит return/raise); используется guard block.
  • В репозитории нет лишних файлов наподобие __pycache__ и .vscode, попадающих туда по невнимательности.
  • Исполняемый код в .py-файлах закрыт конструкцией if __name__ == "__main__":.
  • Для неизменяемых последовательностей данных предпочтительны кортежи, а не списки.
  • В f-строках используется только подстановка переменных, без логических и арифметических операций, вызовов функций и прочей динамики, мешающей читать шаблон.
  • Имена переменных, выбранные по смыслу, записаны по-английски; однобуквенных имён нет (кроме счётчиков в коротких циклах и общепринятых математических обозначений).
  • Функции и методы названы глаголами или глагольными фразами (get_data, compute_energy), а классы существительными (Particle, FieldSolver); функция-формула, возвращающая величину, допускает имя этой величины, как mean и exp в NumPy.
  • Магические числа вынесены в именованные константы.

Большинство перечисленных пунктов проверяются автоматически линтерами и форматтерами ruff, flake8, black, isort, работающими в редакторе. Если они настроены в редакторе и в CI, следить за этими правилами вручную почти не требуется. black и ruff format по умолчанию форматируют по 88 символов, а не по 79, поэтому длину необходимо задать явно в pyproject.toml ([tool.black] line-length = 79, [tool.ruff] line-length = 79), иначе форматтер и линтер будут предъявлять к коду противоречащие требования. Набор правил линтера также задают явно ([tool.ruff.lint] select = [...]): правила по умолчанию меняются от версии к версии, и в ruff 0.16 из них исключены, например, проверки сравнения с None и импорта со звёздочкой. Проверки удобно запускать хуком pre-commit перед каждым коммитом, а в CI — на каждый push.

Принципы проектирования

Перечисленные требования касаются формы кода, и большую их часть проверяет линтер. Устройство кода — какие фрагменты выделены в функции и где хранится общее для них знание — линтер не оценивает: его обсуждают на ревью, опираясь на принципы, сложившиеся в инженерной практике; каждый из них представляет собой эвристику с границами применимости, а не механическое правило. Принципы проектирования классов и модулей, известные под аббревиатурой SOLID, подробно изложены в книге Р. Мартина [13]; ниже рассмотрены принципы, применимые к любому коду, включая скрипты обработки данных.

Принцип DRY

Принцип DRY (Don't Repeat Yourself, «не повторяйся») сформулирован Э. Хантом и Д. Томасом в книге «Программист-прагматик» (1999): каждое знание должно иметь в системе единственное, однозначное и авторитетное представление. Знанием здесь являются не строки кода, а факты о задаче: коэффициенты калибровки, формула, формат файла. Рассмотрим типичный случай. Калибровка энергетической шкалы спектрометра скопирована в скрипт построения спектра и в скрипт подгонки пиков; после перекалибровки коэффициенты исправляют только в одном из них, и для одного и того же пика скрипты дают разные энергии, что обнаруживается, когда перестают сходиться рисунок и таблица статьи. Функция channel_to_energy в отдельном модуле, который импортируют оба скрипта, исключает такое расхождение и позволяет вносить новую калибровку в одном месте.

Граница принципа определяется тем же понятием знания. Одинаковый код может выражать разные знания, например окно сглаживания, совпадающее сегодня у двух детекторов разной конструкции. Объединив такой код, получим связь, которой нет в задаче: при замене одного детектора правка общего кода изменит обработку данных другого. Р. Мартин называет такие совпадения случайным дублированием [13]. Вторую опасность представляет преждевременная абстракция: общая функция, написанная по одному-двум случаям, обрастает флагами и ветвлениями при первом же случае, который в неё не укладывается, а исправить неудачную абстракцию труднее, чем позднее объединить одинаковые фрагменты. Практическую меру задаёт правило трёх, которое М. Фаулер приводит в книге «Рефакторинг» (1999) со слов Д. Робертса: однократное повторение допускается, а при третьем появлении фрагмента общий код выносится в функцию, когда уже видно, что у случаев общего и чем они различаются.

Принципы KISS и YAGNI

Принцип KISS (Keep it simple, stupid; аббревиатуру связывают с авиаконструктором К. Джонсоном) требует выбирать самое простое из решений, удовлетворяющих задаче. Каждая лишняя ветвь кода нуждается в тестах и сопровождении, а ошибки в редко исполняемых ветвях обнаруживаются позже всего. Характерным примером является «универсальный» загрузчик, распознающий по содержимому файлы HDF5, ROOT, FITS и несколько видов текстовых таблиц, тогда как установка пишет файлы одного формата. Загрузчик единственного формата занимает несколько строк, проверяется тестом на одном файле-образце и сообщает о неожиданном файле ошибкой, а не пытается прочитать его иначе.

Принцип YAGNI (You Aren't Gonna Need It, «это не понадобится») происходит из экстремального программирования: возможность реализуется, когда она потребовалась, а не когда её появление только предполагается. Скрипту для однократной обработки одного сеанса измерений не требуется система конфигурации с иерархией файлов YAML, переопределением через переменные окружения и проверкой схемы: достаточно трёх констант в начале файла. Конфигурацию вводят, когда скрипт становится инструментом группы (глава «От скрипта к приложению») и уже известно, какие параметры действительно меняются; тот же довод лежит в основе минимально жизнеспособного продукта (глава «Жизненный цикл ПО»). Границу принципа М. Фаулер (2015) проводит так: YAGNI относится к предполагаемым возможностям, а не к усилиям, облегчающим изменение кода, поэтому тесты, ясные имена и деление на функции работой «на будущее» не являются.

Противоположную крайность называют BDUF (Big Design Up Front): систему проектируют целиком и во всех подробностях до начала разработки. Экстремальное программирование такой подход отвергает, однако, как показывает М. Фаулер в статье «Is Design Dead?» (2000), речь идёт о равновесии предварительного проектирования и рефакторинга: деление на части и интерфейсы обсуждают с коллегами заранее (разделы «Проектирование» и «Design-review» главы «Жизненный цикл ПО»), а подробности уточняют по ходу работы. К тому же ряду относится правило о преждевременной оптимизации (глава «Причины низкой скорости Python»).

Комментарии в коде

Что делает код, сообщают имена и сама запись; комментарий объясняет, почему код написан именно так: зачем формула записана в необычном виде, какую ошибку сторонней библиотеки обходит фрагмент, какое ограничение прибора он учитывает. Комментарий, пересказывающий строку, дублирует знание, уже выраженное кодом: при правке кода его забывают исправить, и он начинает противоречить коду, а такой комментарий, согласно PEP 8, хуже отсутствующего. В следующем фрагменте комментарий сообщает причину решения, которую из кода не восстановить:

import numpy as np

PRETRIGGER_SAMPLES = 100


def subtract_baseline(waveform):
    """Вычитает из осциллограммы базовую линию."""
    # Базовая линия АЦП дрейфует с температурой, поэтому она оценивается
    # в каждом событии по отсчётам до триггера, а не берётся из калибровки.
    baseline = np.mean(waveform[:PRETRIGGER_SAMPLES])
    return waveform - baseline

Комментарий «среднее первых ста отсчётов» к той же строке повторял бы код. Если же без комментария непонятно, что делает фрагмент, его проще переписать: вынести в функцию с говорящим именем, заменить число именованной константой. Предупреждение «не менять» надёжнее заменить тестом, а закомментированный код удаляют: сохранность прежних версий обеспечивает история системы контроля версий.

Задание. Применить этот список к чужому коду и оставить замечания: «Ревью чужого кода».