Лучшие практики разработки на Python

Цель этой статьи  —  поделиться лучшими практиками разработки на Python. Вы узнаете, как настроить и использовать репозиторий Github. Я познакомлю вас с полезными инструментами для поддержания чистоты и правильности кода, покажу, как настроить репозиторий и включить в него ранее представленные инструменты для автоматизированной проверки CI (непрерывной интеграции).

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

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

Используемые инструменты

В этом разделе перечислим инструменты, необходимые для разработки Python-репозитория.

poetry

Poetry  —  это удобный инструмент для управления версиями и зависимостями Python. С его помощью легко контролировать и корректировать версии, а также централизованно управлять зависимостями. Из всех способов сделать это рекомендую poetry. Теперь кратко о том, как использовать этот инструмент.

Основой управления зависимостями в poetry является файл pyproject.toml. В нашем проекте он начинается следующим образом:

[tool.poetry]
name = "Sample Python Project"
version = "0.1.0"
description = "Sample Python repository"
authors = ["hermanmichaels <[email protected]>"]

[tool.poetry.dependencies]
python = "3.10"
matplotlib = "3.5.1"
mypy = "0.910"
numpy = "1.22.3"
pytest = "7.1.2"
black = "22.3.0"
flake8 = "4.0.1"
isort = "^5.10.1"

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

Нужно просто выполнить poetry install в терминале, и poetry автоматически создаст среду Python со всеми установленными зависимостями. Затем можно войти в него через poetry shell.

После добавления новой зависимости нужно запустить poetry update. Это создаст или обновит файл poetry.lock, который можно представить как двоичное представление вышеуказанных зависимостей. Его также нужно будет добавить в репозиторий, и описанный выше процесс установки требований использует этот файл.

isort

PEP 8, руководство по стилю для Python, определяет, как упорядочить импорты. Рекомендуется создавать следующие группы.

  1. Импорт стандартных библиотек (например, os и sys).
  2. Импорт сторонних производителей (например, numpy).
  3. Локальный, специфический для проекта импорт (например, различные файлы одного проекта).

Внутри этих групп импортированные инструменты должны быть отсортированы в алфавитном порядке.

isort  —  это инструмент, который позволяет не запоминать и не выполнять все это самостоятельно. Удобно то, что isort и большинство инструментов, представленных в следующих разделах, отлично работают с poetry. Можно даже задать их настройки в файле pyproject.toml. Для нашего случая установим следующее:

[tool.isort]
profile = "black"
py_version = 310
multi_line_output = 3

В дополнение к версии Python сообщаем isort, что будем работать с форматером black (см. следующий раздел), и определяем, как будут переформатироваться импорты, которые слишком длинны для одной строки.

black

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

Настройками также управляет poetry, а мы просто задаем:

[tool.black]
line-length = 80
target_version = ["py310"]

Т.е. указываем максимальную длину строки (80) и целевую версию Python.

flake8

flake8  —  это линтер кода. Линтеры и форматеры кода близки по своему назначению, однако линтеры проверяют соблюдение определенных стилей и рекомендаций, но не форматируют код. flake8 выполняет несколько функций, одна из которых  —  проверка на соответствие ранее упомянутому стандарту PEP 8.

mypy

mypy  —  это статическая проверка типов Python. Как вы (наверняка) знаете, Python  —  динамически типизированный язык, то есть типы переменных определяются во время выполнения (в отличие от, например, C++). Эта гибкость, которую мы все ценим, имеет свои недостатки, такие как большая вероятность совершения ошибок без компилятора или аналогичного средства, выступающего в качестве первой линии обороны.

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

Можно аннотировать аргументы функций и возвращаемые типы следующим образом:

def foo(x: int, y: int) -> int:
return x + y

Тогда mypy будет сигнализировать о нарушении, если вы попытаетесь вызвать функцию с неправильными аргументами, например:

foo("a", "b")

Управлять настройками mypy будем в отдельном файле mypy.ini. Это необходимо главным образом потому, что некоторые внешние зависимости не могут быть проверены на тип, и нужно исключить их из проверки (хотя какие-то из них можно настроить).

pytest

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

Модульное тестирование помогает отлавливать ошибки и тем самым поддерживать качество кода на высоком уровне.

Github Actions

Github Actions позволяет автоматизировать и запускать определенные шаги в репозитории  —  в духе непрерывной интеграции. С помощью этого инструмента можно создавать рабочие процессы, которые будут выполняться для определенных событий, таких как запросы на включение изменений (PR).

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

Настройка репозитория

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

Собственно говоря, нужно установить лишь одну настройку  —  защиту основной ветки. Чтобы кто-то не попал в нее без проверок, необходимо обеспечить выполнение ваших требований, в частности одобрения другими разработчиками и прохождения установленных вами CI-тестов. Для этого зайдите в репозиторий и выберите “Settings” (“Настройки”), а затем “Branches” (“Ветки”):

Скриншот автора

Потом добавьте правила защиты основной ветки.

  • Требовать запрос на добавление изменений перед слиянием.
  • Требовать одобрения (можно выбрать количество необходимых одобрений).
  • Требовать прохождения проверки состояния перед слиянием.

Соберем все вместе

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

Образец проекта

Проект будет иметь папку utils, содержащую файл math_utils.py и связанный с ним файл модульного теста (math_utils_test.py). В math_utils повторно реализуем функцию экспоненциализации в демонстрационных целях:

import numpy.typing as npt

def exponentiate(base: int, exponent: npt.NDArray) -> npt.NDArray:
return base**exponent

Таким образом, exponentiate(2, [1, 2, 3]) вернет [2, 4, 8].

Проверим корректность функции в тестовом файле:

import numpy as np
import numpy.typing as npt
import pytest

from utils.math_utils import exponentiate


@pytest.mark.parametrize(
"base, exponent, expected",
[
(2, np.zeros(3), np.ones(3)),
(2, np.linspace(1, 4, 4), np.asarray([2, 4, 8, 16])),
],
)
def test_exponentiate(base: int, exponent: npt.NDArray, expected: npt.NDArray) -> None:
assert np.allclose(exponentiate(base, exponent), expected)

В основном файле (main.py) будем использовать эту функцию для генерации первых 10 значений степени 2 и построения графика с помощью matplotlib:

import matplotlib.pyplot as plt
import numpy as np

from utils.math_utils import exponentiate


def main() -> None:
x = np.linspace(0, 10, 10)
y = exponentiate(2, x)
plt.plot(x, y, "ro")
plt.savefig("plot.png")

if __name__ == "__main__":
main()

Файл pyproject.toml для этого проекта выглядит следующим образом:

[tool.poetry]
name = "Sample Python Project"
version = "0.1.0"
description = "Sample Python repository"
authors = ["hermanmichaels <[email protected]>"]

[tool.poetry.dependencies]
python = "3.10"
matplotlib = "3.5.1"
mypy = "0.910"
numpy = "1.22.3"
pytest = "7.1.2"
black = "22.3.0"
flake8 = "4.0.1"
isort = "^5.10.1"

[tool.poetry.dev-dependencies]

[tool.black]
line-length = 80
target_version = ["py310"]

[tool.isort]
profile = "black"
py_version = 310
multi_line_output = 3

Кроме того, исключаем matplotlib из проверки mypy для предотвращения ошибок, создав следующий файл mypy.ini:

[mypy]
python_version = 3.10

[mypy-matplotlib.*]
ignore_missing_imports = True
ignore_errors = True

Рабочий процесс Github

Теперь определим следующий рабочий процесс Github Actions:

name: Sample CI Check

on:
pull_request:
branches: [main]
push:
branches: [main]

permissions:
contents: read

jobs:
build:
runs-on: ubuntu-20.04

steps:
- uses: actions/checkout@v3

- name: Set up Python 3.10.0
uses: actions/setup-python@v3
with:
python-version: "3.10.0"

- name: Install poetry dependencies
run: |
curl -sSL https://install.python-poetry.org | python3 -
poetry install

- name: Sort imports with isort
run: poetry run python -m isort .

- name: Format with black
run: poetry run python -m black .

- name: Lint with flake8
run: poetry run python -m flake8 .

- name: Check types with mypy
run: poetry run python -m mypy .

- name: Run unit tests
run: poetry run python -m py.test

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

Он состоит из следующих шагов.

  • Проверка репозитория.
  • Установка Python 3.10.
  • Установка poetry и зависимостей.
  • Запуск всех установленных проверок (обратите внимание, что poetry run X идентичен входу в среду poetry через poetry shell и последующему выполнению X). В частности, осуществляется запуск сортировки импорта с помощью isort, форматирования кода с помощью black, линтинга с помощью flake8, проверки типов с помощью mypy и модульного тестирования с помощью pytest.

Локальный рабочий процесс разработчика

Теперь опишем рабочий процесс, который каждый разработчик должен выполнять время от времени, особенно перед тем, как генерировать PR. В предыдущем разделе выражение “рабочий процесс” обозначало концепцию Github по группировке шагов в рабочем процессе, а здесь оно просто описывает шаги, которые должен выполнить разработчик.

Не стоит полагаться на CI и нагружать его поиском всех ошибок. Необходимо отправлять PR как можно более “чистыми”. Это означает, что следует проделывать все шаги, выполняемые на CI, самостоятельно локально перед отправкой. Это достигается следующим образом:

  • запуск isort для сортировки импорта: isort;
  • запуск black для форматирования кода: black;
  • запуск flake8 для проверки кода: python -m flake8;
  • запуск mypy для проверки типов: mypy (в первый раз это может занять довольно много времени);
  • запуск всех модульных тестов: python -m pytest.

Заключение

В этой статье описаны полезные инструменты, помогающие организовывать и поддерживать код Python в хорошем состоянии в соответствии с профессиональными стандартами. Мы также показали, как создать Git-репозиторий для версионирования и обмена этим кодом и, в частности, как использовать ранее представленные инструменты в CI (выполнение определенных проверок для предотвращения любых нечистых и ошибочных коммитов в основную ветку). Наконец, мы показали, как запустить все эти инструменты локально, чтобы минимизировать риск сбоя CI.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Oliver S: Best Practices for Python Development

Предыдущая статья13 чит-кодов к жизни программиста
Следующая статья7 методов оптимизации производительности React