Использование современных инструментов для выявления дефектов на ранних этапах жизненного цикла разработки.
Python — это один из тех языков, который делает вас продуктивными почти сразу.
Это большая часть причины его популярности. Путь от идеи к рабочему коду может быть очень быстрым. Вам не нужна большая подготовка только для тестирования идеи. Немного парсинга ввода, несколько функций, всё это вместе — и часто у вас получится что-то полезное за считанные минуты.
Обратная сторона в том, что Python может быть очень снисходительным в местах, где иногда вы бы предпочли, чтобы он это не делал.
Он с удовольствием предположит, что ключ словаря существует, когда его нет. Он позволит вам передавать структуры данных слегка разных форм, пока одна из них наконец не сломается во время выполнения. Он позволит опечатке прожить дольше, чем она должна. И, хитро, он позволит коду быть «правильным», оставаясь при этом слишком медленным для реального использования.
Именно поэтому я стал больше интересоваться workflow-ами разработки кода в целом, чем каким-либо отдельным методом тестирования.
Когда люди говорят о качестве кода, разговор обычно идёт прямо к тестам. Тесты важны, и я их использую постоянно, но я не думаю, что они должны нести всё бремя. Было бы лучше, если бы большинство ошибок были поймано до того, как код вообще запустится. Возможно, некоторые проблемы должны быть пойманы как только вы сохраните файл кода. Другие — когда вы коммитите изменения в GitHub. И если они пройдут хорошо, возможно, вы захотите запустить серию тестов для проверки того, что код ведёт себя правильно и работает достаточно быстро, чтобы выдержать реальный контакт.
В этой статье я хочу рассмотреть набор инструментов, которые вы можете использовать для построения Python-workflow с целью автоматизации задач, упомянутых выше. Не гигантская корпоративная установка и не сложная DevOps-платформа. Просто практичная, относительно простая цепочка инструментов, которая помогает ловить ошибки в вашем коде перед развёртыванием в production.
Чтобы сделать это конкретным, я буду использовать небольшой, но реалистичный пример. Представьте, что я строю Python-модуль, который обрабатывает заказы, вычисляет итоги и генерирует сводки недавних заказов. Вот намеренно грубый первый вариант.
from datetime import datetime
import json
def normalize_order(order):
created = datetime.fromisoformat(order["created_at"])
return {
"id": order["id"],
"customer_email": order.get("customer_email"),
"items": order["items"],
"created_at": created,
"discount_code": order.get("discount_code"),
}
def calculate_total(order):
total = 0
discount = None
for item in order["items"]:
total += item["price"] * item["quantity"]
if order.get("discount_code"):
discount = 0.1
total *= 0.9
return round(total, 2)
def build_order_summary(order): normalized = normalize_order(order); total = calculate_total(order)
return {
"id": normalized["id"],
"email": normalized["customer_email"].lower(),
"created_at": normalized["created_at"].isoformat(),
"total": total,
"item_count": len(normalized["items"]),
}
def recent_order_totals(orders):
summaries = []
for order in orders:
summaries.append(build_order_summary(order))
summaries.sort(key=lambda x: x["created_at"], reverse=True)
return summaries[:10]
Код такого рода имеет много положительного, когда вы «движетесь быстро и ломаете всё». Он короткий и читаемый, и вероятно даже работает с первой парой примеров вводных данных, которые вы попробуете.
Но есть и несколько ошибок или проблем с дизайном, ждущих в крыльях. Если customer_email отсутствует, например, метод .lower() вызовет AttributeError. Есть также предположение, что переменная items всегда содержит ожидаемые ключи. Есть неиспользуемый импорт и оставшаяся переменная из того, что кажется незавершённым рефакторингом. И в финальной функции весь набор результатов сортируется, хотя нужны только 10 самых свежих элементов. Последний момент важен, потому что мы хотим, чтобы наш код был максимально эффективным. Если нам нужны только первые десять, мы должны избежать полной сортировки набора данных при возможности.
Код такого рода — это где хороший workflow начинает окупать себя.
Учитывая всё это, давайте посмотрим на некоторые инструменты, которые вы можете использовать в вашем pipeline разработки кода, которые обеспечат вашему коду наилучший возможный шанс быть правильным, поддерживаемым и производительным. Все инструменты, которые я обсужу, бесплатны для скачивания, установки и использования.
Отметим, что некоторые из упомянутых мною инструментов многоцелевые. Например, некоторое форматирование, которое может делать утилита black, также может быть сделано инструментом ruff. Часто это просто вопрос личного предпочтения, какие из них вы используете.
Инструмент №1: Читаемый код без шума форматирования
Первый инструмент, который я обычно устанавливаю, называется Black. Black — это форматер кода Python. Его задача очень проста: он берёт ваш исходный код и автоматически применяет последовательный стиль и формат.
Установка и использование
Установите его с помощью pip или вашего предпочитаемого менеджера пакетов Python. После этого вы можете запустить его вот так:
$ black your_python_file.py
или
$ python -m black your_python_file
Black требует Python версии 3.10 или более позднюю для работы.
Использование форматера кода может показаться косметическим, но я думаю, что форматеры более важны, чем люди иногда признают. Вы не хотите тратить умственную энергию на решение того, как обёртывается вызов функции, где должен идти разрыв строки или форматирована ли вами словарь «достаточно хорошо». Ваш код должен быть последовательным, чтобы вы могли сосредоточиться на логике, а не на представлении.
Предположим, вы написали эту функцию в спешке.
def build_order_summary(order): normalized=normalize_order(order); total=calculate_total(order)
return {"id":normalized["id"],"email":normalized["customer_email"].lower(),"created_at":normalized["created_at"].isoformat(),"total":total,"item_count":len(normalized["items"])}
Это грязно, но Black превращает это в вот это.
def build_order_summary(order):
normalized = normalize_order(order)
total = calculate_total(order)
return {
"id": normalized["id"],
"email": normalized["customer_email"].lower(),
"created_at": normalized["created_at"].isoformat(),
"total": total,
"item_count": len(normalized["items"]),
}
Black не исправил здесь никакую бизнес-логику. Но это сделало что-то чрезвычайно полезное: это сделало код проще для инспекции. Когда форматирование исчезает как источник трения, любые реальные проблемы с кодированием становятся намного легче видны.
Black конфигурируется множеством различных способов, о которых вы можете прочитать в его официальной документации.
Инструмент №2: Ловля маленьких подозрительных ошибок
После форматирования я обычно добавляю Ruff в pipeline. Ruff — это линтер Python, написанный на Rust. Ruff быстрый, эффективный и очень хорош в том, что он делает.
Установка и использование
Как и Black, Ruff можно установить с помощью любого менеджера пакетов Python.
$ pip install ruff
$ # И используется вот так
$ ruff check your_python_code.py
Линтинг полезен, потому что многие баги начинаются с маленьких подозрительных деталей. Не глубокие логические недостатки или хитрые граничные случаи. Просто слегка неправильный код.
Например, допустим, у нас есть следующий простой код. В нашем примере модуля, например, есть пара неиспользуемых импортов и переменная, которой присваивается значение, но на самом деле она не используется:
from datetime import datetime
import json
def calculate_total(order):
total = 0
discount = 0
for item in order["items"]:
total += item["price"] * item["quantity"]
if order.get("discount_code"):
total *= 0.9
return round(total, 2)
Ruff может ловить это немедленно:
$ ruff check test1.py
F401 [*] `datetime.datetime` imported but unused
--> test1.py:1:22
|
1 | from datetime import datetime
| ^^^^^^^^
2 | import json
|
help: Remove unused import: `datetime.datetime`
F401 [*] `json` imported but unused
--> test1.py:2:8
|
1 | from datetime import datetime
2 | import json
| ^^^^
3 |
4 | def calculate_total(order):
|
help: Remove unused import: `json`
F841 Local variable `discount` is assigned to but never used
--> test1.py:6:5
|
4 | def calculate_total(order):
5 | total = 0
6 | discount = 0
| ^^^^^^^^
7 |
8 | for item in order["items"]:
|
help: Remove assignment to unused variable `discount`
Found 3 errors.
[*] 2 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
Инструмент №3: Python начинает чувствоваться намного безопаснее
Форматирование и линтинг помогают, но ни один из них не решает источник большей части проблем в Python: предположения о данных.
Это где приходит mypy. Mypy — это статический проверяющий типы инструмент для Python.