5.02. Рекомендации по разработке на Python
Рекомендации по разработке на Python
Введение в культуру кода
Разработка на Python предполагает соблюдение определённых принципов, которые делают код понятным, поддерживаемым и расширяемым. Язык предоставляет гибкость, но именно дисциплина в оформлении и проектировании определяет качество программного продукта. Культура кода на Python формируется вокруг нескольких ключевых идей: читаемость превыше всего, простота лучше сложности, явное лучше неявного. Эти принципы воплощены в официальных руководствах по стилю и поддержаны сообществом разработчиков.
Процесс разработки начинается с получения задачи и анализа её требований. Перед написанием первой строки кода необходимо убедиться в полноте понимания поставленной цели, выявить потенциальные риски и ограничения. Даже для небольших задач полезно составить краткий план реализации — определить основные сущности, их взаимодействие и последовательность действий. Такой подход помогает избежать переделок на поздних этапах и снижает количество ошибок.
Кодирование представляет собой центральный этап разработки. Здесь важно соблюдать принятые в проекте соглашения, следить за читаемостью и структурой. Хороший код легко читается другими разработчиками, содержит минимальное количество неочевидных решений и следует принципам проектирования. После завершения написания кода обязательна его проверка: тестирование собственными силами, анализ на наличие ошибок, проверка соответствия требованиям задачи. Только после полной уверенности в корректности работы код попадает в систему контроля версий и проходит код-ревью.
Соглашения об именовании
Имена в коде служат основным средством коммуникации между разработчиками. Правильно подобранные имена передают смысл сущности без дополнительных комментариев и упрощают понимание логики программы.
Основные правила именования
Все идентификаторы в Python используют стиль snake_case — слова разделяются символом подчёркивания, все буквы строчные. Этот стиль применяется для переменных, функций, методов и модулей. Константы объявляются в стиле UPPER_CASE — все буквы заглавные, слова разделены подчёркиванием. Классы именуются в стиле PascalCase — каждое слово начинается с заглавной буквы, разделители отсутствуют.
Примеры корректных имён:
# Переменные и функции
user_count = 42
max_retries = 3
process_payment = lambda amount: f"Processed {amount}"
# Константы
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30.0
API_VERSION = "v2.1"
# Классы
class PaymentProcessor:
pass
class DatabaseConnection:
pass
Имена должны передавать назначение сущности максимально точно. Избегайте однобуквенных имён за исключением счётчиков в циклах или математических выражений, где контекст очевиден. Предпочитайте полные слова сокращениям, если сокращение не является общепринятым в предметной области. Например, user_id предпочтительнее uid, а config допустимо как общепринятое сокращение для configuration.
Для булевых переменных и методов используйте префиксы is_, has_, can_, should_, которые делают намерение очевидным:
is_active = True
has_permissions = False
can_edit = user.role == "admin"
should_retry = attempt < max_attempts
Имена функций и методов начинаются с глагола, описывающего действие:
def calculate_total(items):
pass
def validate_user_credentials(username, password):
pass
def send_notification(user, message):
pass
Имена классов представляют собой существительные, описывающие сущность или концепцию:
class ShoppingCart:
pass
class PaymentGateway:
pass
class UserSession:
pass
Имена модулей должны быть короткими, полностью строчными, без подчёркиваний, если это возможно. Для пакетов применяются те же правила. Избегайте имён, совпадающих со стандартными библиотеками или широко используемыми сторонними пакетами.
Форматирование и оформление кода
Читаемость кода напрямую влияет на скорость его понимания и количество ошибок при модификации. Единообразное форматирование создаёт предсказуемую структуру, которую легко воспринимать визуально.
Отступы и пробелы
Для отступов используются четыре пробела. Табуляция запрещена. Отступы определяют структуру блоков кода в Python, поэтому их единообразие критически важно. После двоеточия, открывающего блок, следует новая строка с увеличенным отступом. Закрывающая строка блока возвращается на предыдущий уровень отступа.
Операторы окружены пробелами с обеих сторон:
x = a + b
threshold = value * 0.85
result = (x + y) / (z - w)
После запятых ставится один пробел, перед запятой пробел не ставится:
items = [1, 2, 3, 4, 5]
user = {"name": "Alice", "age": 30, "city": "Moscow"}
process_data(first_item, second_item, third_item)
В вызовах функций и индексации пробелы не ставятся между именем функции и открывающей скобкой, а также между именем последовательности и открывающей квадратной скобкой:
value = calculate_sum(items)
first = items[0]
Пустые строки разделяют логические блоки кода внутри функции, а также разделяют функции и классы на верхнем уровне модуля. Одна пустая строка используется между методами внутри класса. Две пустые строки — между определениями функций и классов на уровне модуля.
Длина строк и переносы
Максимальная длина строки составляет 88 символов. Это значение рекомендовано инструментом Black и обеспечивает комфортное чтение на различных устройствах. При необходимости переноса выражения на несколько строк применяются следующие подходы:
Для арифметических выражений и логических операций перенос выполняется перед оператором:
total = (first_component
+ second_component
+ third_component
- adjustment)
Для вызовов функций с большим количеством аргументов каждый аргумент размещается на отдельной строке с выравниванием:
result = process_transaction(
user_id=current_user.id,
amount=transaction_amount,
currency=transaction_currency,
timestamp=datetime.now(),
metadata=additional_info
)
Для сложных условий в операторе if каждый элемент условия размещается на отдельной строке:
if (user.is_active
and user.has_valid_subscription
and transaction.amount <= user.daily_limit
and not is_fraudulent(transaction)):
process_payment(transaction)
Оформление многострочных конструкций
Словари и списки с большим количеством элементов оформляются вертикально. Каждый элемент размещается на отдельной строке с отступом. Закрывающая скобка располагается на отдельной строке на уровне начала конструкции:
configuration = {
"database": {
"host": "localhost",
"port": 5432,
"name": "production_db",
"user": "app_user",
"password": "secure_password"
},
"cache": {
"type": "redis",
"host": "cache.example.com",
"port": 6379,
"ttl": 300
},
"logging": {
"level": "INFO",
"format": "%(asctime)s %(levelname)s %(name)s %(message)s"
}
}
Для кортежей с одним элементом обязательно указывается запятая после элемента:
single_element = ("only_item",)
Структура проекта и организация файлов
Правильная организация файлов и директорий упрощает навигацию по проекту, облегчает тестирование и развёртывание. Структура проекта на Python следует определённым шаблонам, которые стали стандартом де-факто в сообществе.
Базовая структура приложения
Типичная структура проекта включает следующие элементы:
my_project/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ ├── services.py
│ │ └── repositories.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── endpoints.py
│ │ └── schemas.py
│ └── utils/
│ ├── __init__.py
│ ├── helpers.py
│ └── validators.py
├── tests/
│ ├── __init__.py
│ ├── unit/
│ │ ├── test_models.py
│ │ └── test_services.py
│ └── integration/
│ └── test_api.py
├── docs/
│ └── index.md
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
├── .env.example
├── .gitignore
├── pyproject.toml
├── README.md
└── setup.py
Корневая директория проекта содержит конфигурационные файлы и метаданные. Директория src содержит исходный код приложения, организованный в пакеты. Разделение на core, api, utils отражает архитектурные слои приложения. Директория tests зеркалирует структуру исходного кода, что упрощает навигацию между реализацией и тестами.
Организация модулей
Каждый модуль должен иметь чёткую ответственность. Модуль models.py содержит определения данных и бизнес-логику, связанную с ними. Модуль services.py реализует операции над данными, координирует взаимодействие между репозиториями и другими компонентами. Модуль repositories.py отвечает за доступ к данным — запросы к базе данных, кэширование, внешние API.
Избегайте создания монолитных модулей с тысячами строк кода. При достижении размера модуля 500–800 строк рассмотрите возможность его декомпозиции на несколько специализированных модулей. Например, модуль services.py можно разделить на user_services.py, payment_services.py, notification_services.py в зависимости от предметной области.
Файл __init__.py в каждом пакете определяет его публичный интерфейс. В нём можно импортировать ключевые классы и функции, чтобы обеспечить удобный доступ извне пакета:
# my_package/core/__init__.py
from .models import User, Order, Product
from .services import UserService, OrderService
from .repositories import UserRepository, OrderRepository
__all__ = [
"User",
"Order",
"Product",
"UserService",
"OrderService",
"UserRepository",
"OrderRepository",
]
Такой подход позволяет импортировать сущности напрямую из пакета:
from my_package.core import User, UserService
Управление зависимостями
Зависимости проекта описываются в файлах requirements/*.txt или в секции [project.optional-dependencies] файла pyproject.toml. Базовые зависимости размещаются в base.txt, зависимости для разработки — в dev.txt, для продакшена — в prod.txt. Файл dev.txt включает base.txt и добавляет инструменты для тестирования, линтинга, форматирования.
Все зависимости фиксируются с точными версиями в файле requirements.txt для продакшена. Для разработки допустимо использование диапазонов версий, но с осторожностью — чтобы избежать неожиданных изменений поведения при обновлении зависимостей.
Проектирование классов и функций
Хорошее проектирование снижает сложность системы, упрощает тестирование и делает код устойчивым к изменениям. Принципы проектирования применимы ко всем уровням — от отдельных функций до архитектуры приложения в целом.
Принцип единственной ответственности
Каждый класс и функция должны решать одну задачу. Класс User отвечает за представление пользователя и его атрибутов. Класс UserService отвечает за операции с пользователями — создание, обновление, поиск. Класс UserRepository отвечает за сохранение и загрузку пользователей из хранилища данных. Такое разделение позволяет изменять один аспект системы без влияния на другие.
Функции должны быть короткими и сфокусированными. Идеальная функция выполняет одну операцию и занимает не более 20–30 строк. Если функция растёт в размерах, её следует декомпозировать на несколько вспомогательных функций с понятными именами. Это улучшает читаемость и позволяет повторно использовать логику.
Пример декомпозиции:
# До декомпозиции
def process_order(order_data):
# Валидация данных
if not order_data.get("user_id"):
raise ValueError("User ID required")
if not order_data.get("items"):
raise ValueError("Items required")
if len(order_data["items"]) == 0:
raise ValueError("At least one item required")
# Расчёт суммы
total = 0
for item in order_data["items"]:
if item["quantity"] <= 0:
raise ValueError("Quantity must be positive")
total += item["price"] * item["quantity"]
# Применение скидки
if total > 1000:
discount = total * 0.1
total -= discount
else:
discount = 0
# Сохранение заказа
order = {
"user_id": order_data["user_id"],
"items": order_data["items"],
"total": total,
"discount": discount,
"status": "pending",
"created_at": datetime.now()
}
database.save_order(order)
# Отправка уведомления
send_email(order_data["user_id"], f"Order created: {order['id']}")
return order
# После декомпозиции
def process_order(order_data):
validate_order_data(order_data)
total, discount = calculate_order_total(order_data["items"])
order = create_order_record(order_data, total, discount)
save_order_to_database(order)
notify_user_about_order(order)
return order
def validate_order_data(order_data):
if not order_data.get("user_id"):
raise ValueError("User ID required")
if not order_data.get("items"):
raise ValueError("Items required")
if len(order_data["items"]) == 0:
raise ValueError("At least one item required")
for item in order_data["items"]:
if item["quantity"] <= 0:
raise ValueError("Quantity must be positive")
def calculate_order_total(items):
total = sum(item["price"] * item["quantity"] for item in items)
discount = total * 0.1 if total > 1000 else 0
return total - discount, discount
def create_order_record(order_data, total, discount):
return {
"user_id": order_data["user_id"],
"items": order_data["items"],
"total": total,
"discount": discount,
"status": "pending",
"created_at": datetime.now()
}
Инкапсуляция и сокрытие реализации
Внутренние детали реализации класса должны быть скрыты от внешнего мира. Публичный интерфейс класса определяет контракт взаимодействия, а детали реализации могут изменяться без влияния на клиентский код. В Python для обозначения внутренних атрибутов и методов используется префикс подчёркивания _.
class PaymentProcessor:
def __init__(self, api_key):
self._api_key = api_key
self._session = self._create_session()
def process_payment(self, amount, currency):
payload = self._prepare_payload(amount, currency)
response = self._send_request(payload)
return self._parse_response(response)
def _create_session(self):
return requests.Session()
def _prepare_payload(self, amount, currency):
return {
"amount": amount,
"currency": currency,
"timestamp": int(time.time()),
"signature": self._generate_signature(amount, currency)
}
def _generate_signature(self, amount, currency):
data = f"{amount}{currency}{self._api_key}"
return hashlib.sha256(data.encode()).hexdigest()
def _send_request(self, payload):
return self._session.post("https://api.payment.com/charge", json=payload)
def _parse_response(self, response):
if response.status_code != 200:
raise PaymentError(f"Payment failed: {response.text}")
return response.json()
Клиентский код взаимодействует только с методом process_payment, не заботясь о деталях подготовки запроса, генерации подписи или обработки ответа. При изменении внутренней реализации — например, переходе на другую платежную систему — публичный интерфейс остаётся неизменным.
Композиция вместо наследования
Предпочитайте композицию наследованию при проектировании классов. Композиция обеспечивает большую гибкость и снижает связанность между компонентами. Вместо создания иерархии классов через наследование, собирайте поведение из независимых компонентов.
# Подход с наследованием
class NotificationService:
def send(self, user, message):
raise NotImplementedError()
class EmailNotificationService(NotificationService):
def send(self, user, message):
send_email(user.email, message)
class SMSNotificationService(NotificationService):
def send(self, user, message):
send_sms(user.phone, message)
class UserNotifier:
def __init__(self, notification_service):
self.notification_service = notification_service
def notify_user(self, user, message):
self.notification_service.send(user, message)
# Подход с композицией
class EmailSender:
def send(self, recipient, message):
send_email(recipient, message)
class SMSSender:
def send(self, recipient, message):
send_sms(recipient, message)
class UserNotifier:
def __init__(self, email_sender, sms_sender):
self.email_sender = email_sender
self.sms_sender = sms_sender
def notify_by_email(self, user, message):
self.email_sender.send(user.email, message)
def notify_by_sms(self, user, message):
self.sms_sender.send(user.phone, message)
Композиция позволяет комбинировать компоненты различными способами, заменять их реализации без изменения клиентского кода и упрощает тестирование через внедрение заглушек.
Обработка ошибок и исключений
Корректная обработка ошибок повышает надёжность приложения и упрощает диагностику проблем. Исключения в Python следует использовать для обработки исключительных ситуаций, а не для управления нормальным потоком выполнения программы.
Создание пользовательских исключений
Для предметной области проекта создаются собственные классы исключений, наследуемые от базовых классов Exception или более специфичных исключений стандартной библиотеки. Пользовательские исключения группируются в иерархию, отражающую структуру ошибок в системе.
class ApplicationError(Exception):
"""Базовый класс для всех ошибок приложения"""
pass
class ValidationError(ApplicationError):
"""Ошибка валидации входных данных"""
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"Validation error in field '{field}': {message}")
class PaymentError(ApplicationError):
"""Ошибка обработки платежа"""
def __init__(self, transaction_id, reason):
self.transaction_id = transaction_id
self.reason = reason
super().__init__(f"Payment failed for transaction {transaction_id}: {reason}")
class ResourceNotFoundError(ApplicationError):
"""Запрашиваемый ресурс не найден"""
def __init__(self, resource_type, resource_id):
self.resource_type = resource_type
self.resource_id = resource_id
super().__init__(f"{resource_type} with ID {resource_id} not found")
Использование специфичных исключений позволяет точно определить тип ошибки и обработать её соответствующим образом:
try:
order = order_service.create_order(order_data)
except ValidationError as e:
return {"error": "validation_failed", "field": e.field, "message": e.message}, 400
except PaymentError as e:
return {"error": "payment_failed", "transaction_id": e.transaction_id}, 402
except ResourceNotFoundError as e:
return {"error": "resource_not_found", "type": e.resource_type, "id": e.resource_id}, 404
Логирование ошибок
Каждое исключение, достигающее верхнего уровня обработки, должно быть записано в лог с полным контекстом: тип исключения, сообщение, стек вызовов, идентификаторы запроса и пользователя. Это обеспечивает возможность диагностики проблем в продакшене.
import logging
import traceback
logger = logging.getLogger(__name__)
def handle_request(request):
try:
return process_request(request)
except Exception as e:
request_id = getattr(request, "id", "unknown")
user_id = getattr(request, "user_id", "anonymous")
logger.error(
"Unhandled exception during request processing",
extra={
"request_id": request_id,
"user_id": user_id,
"exception_type": type(e).__name__,
"exception_message": str(e),
"traceback": traceback.format_exc()
}
)
raise
Избегайте пустых блоков except. Если исключение перехватывается без обработки, это скрывает реальную проблему и затрудняет отладку. Даже в случаях, когда ошибка игнорируется намеренно, добавляйте комментарий с объяснением причины.
Комментирование и документирование
Комментарии дополняют код, объясняя решения, которые неочевидны из самого кода. Хороший код самодокументирован через имена и структуру, но комментарии необходимы для объяснения «почему» принято то или иное решение.
Документирование модулей и функций
Каждый модуль начинается со строк документации, описывающих его назначение и основные компоненты. Каждая публичная функция и метод сопровождается строкой документации в формате Google Style или NumPy Style. Строка документации содержит краткое описание, параметры, возвращаемое значение и возможные исключения.
"""Модуль обработки платежей
Содержит компоненты для взаимодействия с платежными шлюзами,
валидации платежных данных и обработки результатов транзакций.
"""
def calculate_discount(amount: float, user_tier: str) -> float:
"""Рассчитывает размер скидки для пользователя на основе суммы заказа и уровня лояльности.
Скидка применяется по следующим правилам:
- Для сумм до 1000 рублей скидка не предоставляется
- Для сумм от 1000 до 5000 рублей предоставляется скидка 5%
- Для сумм свыше 5000 рублей:
* Бронзовый уровень: 7%
* Серебряный уровень: 10%
* Золотой уровень: 15%
Args:
amount: Сумма заказа в рублях.
user_tier: Уровень лояльности пользователя (bronze, silver, gold).
Returns:
Размер скидки в рублях.
Raises:
ValueError: Если сумма отрицательная или уровень лояльности некорректный.
"""
if amount < 0:
raise ValueError("Amount cannot be negative")
if amount < 1000:
return 0.0
if amount < 5000:
return amount * 0.05
tier_discounts = {
"bronze": 0.07,
"silver": 0.10,
"gold": 0.15
}
if user_tier not in tier_discounts:
raise ValueError(f"Invalid user tier: {user_tier}")
return amount * tier_discounts[user_tier]
Комментарии в коде
Комментарии в теле функции используются для пояснения нетривиальных алгоритмов, объяснения причин обхода ограничений или описания временных решений. Избегайте комментариев, дублирующих код:
# Плохо: комментарий дублирует очевидное действие
# Увеличиваем счётчик на единицу
counter += 1
# Хорошо: комментарий объясняет нетривиальное решение
# Используем экспоненциальную задержку для предотвращения шторма запросов
# при частых временных сбоях внешнего сервиса
delay = min(base_delay * (2 ** attempt), max_delay)
time.sleep(delay)
Для временных решений или известных ограничений используйте маркеры TODO, FIXME, HACK с указанием причины и плана устранения:
# TODO: Заменить хардкод значения на конфигурационный параметр после релиза v2.0
MAX_RETRIES = 3
# FIXME: Обход ошибки в библиотеке requests версии 2.28.0
# Удалить после обновления до версии 2.29.0
if sys.version_info < (3, 11):
ssl_context = ssl.create_default_context()
else:
ssl_context = None
Тестирование
Тесты обеспечивают уверенность в корректности работы кода и защищают от регрессий при внесении изменений. Структура тестов отражает структуру тестируемого кода, что упрощает навигацию и поддержку.
Структура тестов
Тесты организуются в директории tests с поддиректориями unit для модульных тестов и integration для интеграционных. Каждый тестовый файл соответствует тестируемому модулю с префиксом test_. Каждый тестовый метод начинается с префикса test_ и описывает проверяемое поведение.
# tests/unit/test_payment_service.py
import pytest
from unittest.mock import Mock
from src.my_package.core.services import PaymentService
from src.my_package.core.repositories import PaymentRepository
from src.my_package.core.models import Payment, PaymentStatus
class TestPaymentService:
@pytest.fixture
def payment_repository(self):
return Mock(spec=PaymentRepository)
@pytest.fixture
def payment_service(self, payment_repository):
return PaymentService(payment_repository)
def test_process_payment_success(self, payment_service, payment_repository):
payment_repository.save.return_value = Payment(
id="pay_123",
amount=1000.0,
currency="RUB",
status=PaymentStatus.SUCCEEDED
)
result = payment_service.process_payment(
amount=1000.0,
currency="RUB",
payment_method="card_4242"
)
assert result.status == PaymentStatus.SUCCEEDED
payment_repository.save.assert_called_once()
def test_process_payment_failure(self, payment_service, payment_repository):
payment_repository.save.side_effect = PaymentError("Insufficient funds")
with pytest.raises(PaymentError):
payment_service.process_payment(
amount=1000.0,
currency="RUB",
payment_method="card_declined"
)
Принципы написания тестов
Каждый тест должен проверять одно конкретное поведение. Тест должен быть изолированным — не зависеть от состояния других тестов или внешних систем. Для внешних зависимостей используются моки и заглушки. Тест должен быть быстрым — выполняться за доли секунды. Медленные тесты выносятся в отдельную категорию интеграционных тестов.
Имена тестовых методов описывают проверяемое поведение в формате «действие_условие_результат»:
def test_calculate_total_with_discount_applied():
pass
def test_validate_email_rejects_invalid_format():
pass
def test_process_order_creates_audit_log_entry():
pass
Тесты покрывают не только сценарии успешного выполнения, но и обработку ошибок, граничные условия, пустые входные данные. Для каждого публичного метода должны существовать тесты, проверяющие его поведение во всех ожидаемых сценариях.
Инструменты автоматизации и проверки кода
Автоматизация проверки кода обеспечивает соблюдение стандартов без ручного контроля. Инструменты интегрируются в процесс разработки и предотвращают попадание некачественного кода в основную ветку.
Статический анализ
Инструменты статического анализа проверяют код на соответствие стилю, выявляют потенциальные ошибки и проблемные конструкции. Для Python используются:
- ruff — быстрый линтер и форматировщик, заменяющий комбинацию flake8, isort и pyupgrade
- mypy — статический анализатор типов, проверяющий корректность аннотаций
- bandit — анализатор безопасности, выявляющий уязвимости в коде
Конфигурация инструментов размещается в файле pyproject.toml:
[tool.ruff]
line-length = 88
target-version = "py310"
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
]
ignore = [
"E501", # line too long (handled by formatter)
]
[tool.ruff.isort]
known-first-party = ["my_package"]
Автоматическое форматирование
Форматирование кода выполняется автоматически инструментом Black. Black применяет единый стиль без настроек, что устраняет споры о форматировании. Интеграция с прекоммит-хуками обеспечивает применение форматирования перед каждым коммитом.
Конфигурация Black в pyproject.toml:
[tool.black]
line-length = 88
target-version = ["py310"]
Типизация
Аннотации типов повышают надёжность кода и улучшают поддержку в редакторах. Все публичные функции и методы должны содержать аннотации параметров и возвращаемого значения. Для сложных типов используются возможности модуля typing:
from typing import List, Dict, Optional, Union, Callable
from datetime import datetime
def process_orders(
orders: List[Dict[str, Union[str, float, int]]],
callback: Optional[Callable[[str], None]] = None
) -> Dict[str, Union[int, float, datetime]]:
pass
Для Python 3.9 и новее предпочтительны встроенные обобщённые типы вместо импортов из typing:
# Python 3.9+
def get_users() -> list[dict[str, str]]:
pass
# Вместо (для более старых версий)
from typing import List, Dict
def get_users() -> List[Dict[str, str]]:
pass
Проверка типов выполняется инструментом mypy как часть процесса сборки. Ошибки типизации рассматриваются как критические и должны исправляться до слияния изменений.
Практические рекомендации для повседневной разработки
Разработка программного обеспечения включает множество рутинных операций, для которых существуют проверенные подходы. Следование этим подходам экономит время и снижает количество ошибок.
Работа с конфигурацией
Конфигурационные параметры извлекаются из переменных окружения с помощью библиотеки pydantic-settings или аналогичных решений. Все параметры имеют значения по умолчанию для локальной разработки. Конфигурация валидируется при запуске приложения.
from pydantic_settings import BaseSettings
from pydantic import Field
class Settings(BaseSettings):
database_url: str = Field(
default="postgresql://localhost:5432/myapp",
description="URL подключения к базе данных"
)
redis_url: str = Field(
default="redis://localhost:6379/0",
description="URL подключения к Redis"
)
debug: bool = Field(
default=False,
description="Режим отладки"
)
secret_key: str = Field(
default="insecure-default-key-for-dev",
description="Секретный ключ для подписи токенов"
)
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
Никогда не коммитьте файл .env с секретными данными в систему контроля версий. В репозитории размещается только пример .env.example с описанием необходимых переменных.
Работа с внешними сервисами
Все обращения к внешним сервисам выполняются через абстракции — интерфейсы или базовые классы. Это позволяет заменять реализации для тестирования и упрощает миграцию на другие сервисы.
from abc import ABC, abstractmethod
from typing import List
class EmailService(ABC):
@abstractmethod
def send_email(self, to: str, subject: str, body: str) -> bool:
pass
class SESMailService(EmailService):
def __init__(self, aws_region: str):
self.client = boto3.client("ses", region_name=aws_region)
def send_email(self, to: str, subject: str, body: str) -> bool:
try:
self.client.send_email(
Source="noreply@example.com",
Destination={"ToAddresses": [to]},
Message={
"Subject": {"Data": subject},
"Body": {"Text": {"Data": body}}
}
)
return True
except Exception as e:
logger.error(f"Failed to send email to {to}: {e}")
return False
class DebugEmailService(EmailService):
def send_email(self, to: str, subject: str, body: str) -> bool:
logger.debug(f"DEBUG EMAIL to {to}\nSubject: {subject}\n\n{body}")
return True
В продакшене используется SESMailService, в разработке и тестах — DebugEmailService. Переключение выполняется через конфигурацию без изменения клиентского кода.
Логирование
Логирование выполняется через стандартный модуль logging с настройкой форматтеров и хэндлеров. Каждый модуль получает собственный логгер с именем модуля. Уровень логирования устанавливается централизованно через конфигурацию.
import logging
logger = logging.getLogger(__name__)
def process_transaction(transaction_id: str):
logger.info("Starting transaction processing", extra={"transaction_id": transaction_id})
try:
# Логика обработки
result = _perform_operations(transaction_id)
logger.info("Transaction processed successfully", extra={
"transaction_id": transaction_id,
"result": result
})
return result
except Exception as e:
logger.exception("Transaction processing failed", extra={
"transaction_id": transaction_id,
"error_type": type(e).__name__
})
raise
Для структурированного логирования в продакшене используется формат JSON с дополнительными полями контекста — идентификаторами запроса, пользователя, транзакции. Это упрощает анализ логов с помощью инструментов вроде ELK Stack или Splunk.
Управление состоянием и мутабельностью
Предпочитайте неизменяемые структуры данных там, где это возможно. Для представления данных используйте dataclass с параметром frozen=True или NamedTuple. Неизменяемость упрощает рассуждение о коде, предотвращает побочные эффекты и упрощает тестирование.
from dataclasses import dataclass
from typing import List
@dataclass(frozen=True)
class OrderItem:
product_id: str
quantity: int
price: float
@dataclass(frozen=True)
class Order:
id: str
user_id: str
items: List[OrderItem]
status: str
@property
def total(self) -> float:
return sum(item.quantity * item.price for item in self.items)
При необходимости изменения состояния создавайте новый объект с обновлёнными значениями вместо модификации существующего. Для сложных объектов используйте методы-помощники, возвращающие копии с изменениями.