С помощью чего можно добиться качественного и чистого кода?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Ответ: Инструменты и практики для чистого кода
Качественный код это не магия и не талант, это система и дисциплина. Расскажу как я это делаю на практике.
1. Линтеры — первая линия защиты
Линтер анализирует код и находит ошибки до runtime.
Ruff — самый быстрый
# Установка
pip install ruff
# Проверка
ruff check .
# Автоматическое исправление
ruff check --fix .
Конфиг (pyproject.toml):
[tool.ruff]
line-length = 79
target-version = "py311"
[tool.ruff.lint]
select = [
"E", # pycodestyle ошибки
"W", # pycodestyle предупреждения
"F", # Pyflakes
"I", # isort (импорты)
"C4", # flake8-comprehensions
"B", # flake8-bugbear
"D", # pydocstyle
]
ignore = [
"D100", # Missing docstring in public module
"D104", # Missing docstring in public package
]
[tool.ruff.lint.pydocstyle]
convention = "google"
Pylint — углублённый анализ
pip install pylint
pylint mymodule.py
Находит:
- Неиспользуемые переменные
- Нарушения SOLID принципов
- Сложность циклов
- Логические ошибки
2. Форматер кода — Zero friction
Форматер автоматически переписывает код в правильном стиле. Не думаешь о форматировании, просто пишешь.
Black — бескомпромиссный
pip install black
black .
Конфиг:
[tool.black]
line-length = 79
target-version = ['py311']
Пример:
# ДО (мой код)
result=calculate_total_price(user_id=123,items=[1,2,3],apply_discount=True,tax_rate=0.2)
# ПОСЛЕ (Black)
result = calculate_total_price(
user_id=123,
items=[1, 2, 3],
apply_discount=True,
tax_rate=0.2,
)
3. Type checking — ловим ошибки типов
Python динамический, но можно добавить типы и проверять их статически.
mypy
pip install mypy
mypy .
Конфиг:
[tool.mypy]
python_version = "3.11"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true # строгая типизация
Пример:
from typing import Optional
def get_user_age(user_id: int) -> Optional[int]:
"""Get user age or None."""
user = db.query(User).get(user_id)
if user is None:
return None
return user.age
# mypy проверит что это правильно
age = get_user_age(123)
if age is not None:
print(age + 1) # OK
# mypy ошибка: age может быть None
print(age + 1) # Error: unsupported operand type(s)
4. Тесты — коллега который смотрит за тобой
Хороший набор тестов гарантирует что код работает правильно и не сломается при изменениях.
pytest
pip install pytest pytest-cov
pytest
pytest --cov=app tests/ # с покрытием
Пример теста:
import pytest
from app.services import calculate_total_price
def test_calculate_total_price_no_discount():
result = calculate_total_price(
base_price=100,
quantity=2,
tax_rate=0.2,
discount=0
)
assert result == 240 # 100 * 2 * 1.2
def test_calculate_total_price_with_discount():
result = calculate_total_price(
base_price=100,
quantity=2,
tax_rate=0.2,
discount=0.1
)
assert result == 216 # 100 * 2 * 1.2 * 0.9
def test_invalid_quantity():
with pytest.raises(ValueError):
calculate_total_price(
base_price=100,
quantity=-1, # ошибка!
tax_rate=0.2,
discount=0
)
Цели по coverage:
- Минимум 80% покрытие
- 90%+ для критического кода
- 100% для business logic
5. Code Review и CI/CD
GitHub Actions pipeline
name: Code Quality
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: pip install ruff black mypy pytest
- run: ruff check .
- run: black --check .
- run: mypy .
- run: pytest --cov=app
На pull request запускается:
- Линтер (ruff)
- Форматер (black)
- Type checker (mypy)
- Тесты (pytest)
Если что-то не прошло — PR не мержится.
6. SOLID принципы — архитектура
S — Single Responsibility (одна ответственность)
# ❌ Плохо: UserService делает слишком много
class UserService:
def create_user(self, name, email):
user = User(name=name, email=email)
db.add(user)
db.commit()
# Отправляет email
send_email(email, "Welcome!")
# Логирует
logger.info(f"User {name} created")
# Обновляет кэш
cache.set(f"user:{user.id}", user)
return user
# ✓ Хорошо: разделили ответственность
class UserRepository:
def create(self, name: str, email: str) -> User:
user = User(name=name, email=email)
db.add(user)
db.commit()
return user
class NotificationService:
def send_welcome_email(self, user: User) -> None:
send_email(user.email, "Welcome!")
class UserService:
def __init__(self, repo: UserRepository, notifier: NotificationService):
self.repo = repo
self.notifier = notifier
def create_user(self, name: str, email: str) -> User:
user = self.repo.create(name, email)
self.notifier.send_welcome_email(user)
return user
D — Dependency Inversion (зависимости от интерфейсов, не реализации)
# ❌ Плохо: зависит от конкретной БД
class UserService:
def __init__(self):
self.db = PostgreSQL() # жёсткая зависимость
def get_user(self, user_id: int):
return self.db.query(User).get(user_id)
# ✓ Хорошо: зависит от интерфейса
from abc import ABC, abstractmethod
class IUserRepository(ABC):
@abstractmethod
def get(self, user_id: int) -> User:
pass
class UserService:
def __init__(self, repository: IUserRepository):
self.repository = repository # зависимость от интерфейса
def get_user(self, user_id: int) -> User:
return self.repository.get(user_id)
# Можно использовать любую реализацию
postgres_repo = PostgresUserRepository()
mongo_repo = MongoUserRepository()
memory_repo = MemoryUserRepository() # для тестов!
service = UserService(memory_repo) # легко подменить
7. DRY (Don't Repeat Yourself)
# ❌ Плохо: код повторяется
def get_user_page(page_num):
users = db.query(User).filter(User.is_active == True)
total = users.count()
offset = (page_num - 1) * 10
items = users.offset(offset).limit(10).all()
return {"items": items, "total": total, "page": page_num}
def get_posts_page(page_num):
posts = db.query(Post).filter(Post.is_published == True)
total = posts.count()
offset = (page_num - 1) * 10
items = posts.offset(offset).limit(10).all()
return {"items": items, "total": total, "page": page_num}
# ✓ Хорошо: извлекли общий паттерн
def paginate(query, page_num, page_size=10):
total = query.count()
offset = (page_num - 1) * page_size
items = query.offset(offset).limit(page_size).all()
return {"items": items, "total": total, "page": page_num}
def get_user_page(page_num):
query = db.query(User).filter(User.is_active == True)
return paginate(query, page_num)
def get_posts_page(page_num):
query = db.query(Post).filter(Post.is_published == True)
return paginate(query, page_num)
8. KISS (Keep It Simple, Stupid)
# ❌ Плохо: переусложнено
class UserFilterBuilder:
def __init__(self):
self.filters = []
def add_name_filter(self, name):
self.filters.append(lambda u: u.name == name)
return self
def add_age_filter(self, min_age, max_age):
self.filters.append(lambda u: min_age <= u.age <= max_age)
return self
def build(self):
def combined_filter(user):
return all(f(user) for f in self.filters)
return combined_filter
filter = UserFilterBuilder()
filter.add_name_filter("John")
filter.add_age_filter(25, 35)
results = [u for u in users if filter.build()(u)]
# ✓ Хорошо: просто
results = [
u for u in users
if u.name == "John" and 25 <= u.age <= 35
]
Мой production stack
# pyproject.toml dependencies
[tool.poetry.group.dev.dependencies]
ruff = "^0.1.0"
black = "^23.0.0"
mypy = "^1.0.0"
pytest = "^7.0.0"
pytest-cov = "^4.0.0"
pytest-asyncio = "^0.21.0"
# Скрипты
[tool.poetry.scripts]
lint = "ruff check . && black --check ."
format = "black . && ruff check --fix ."
type-check = "mypy ."
test = "pytest --cov=app"
Workflow перед коммитом:
make format # черный форматер
make lint # ruff проверка
make type-check # mypy
make test # pytest
Итог
Чистый код это не талант, это система:
- Линтеры (ruff) — находят ошибки
- Форматеры (black) — унифицируют стиль
- Type checking (mypy) — ловят ошибки типов
- Тесты (pytest) — гарантируют корректность
- Code review — второй взгляд
- CI/CD — автоматическая проверка
- SOLID — архитектура
- DRY/KISS — простота
Главное: эти инструменты работают, только если их использовать в pipeline'е. Один ruff не поможет, нужна вся система. Я использую их все, каждый день на работе.