Требования к коду на 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 = [...]): правила по умолчанию меняются от версии к версии, и вruff0.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
Комментарий «среднее первых ста отсчётов» к той же строке повторял бы код. Если же без комментария непонятно, что делает фрагмент, его проще переписать: вынести в функцию с говорящим именем, заменить число именованной константой. Предупреждение «не менять» надёжнее заменить тестом, а закомментированный код удаляют: сохранность прежних версий обеспечивает история системы контроля версий.
Задание. Применить этот список к чужому коду и оставить замечания: «Ревью чужого кода».