⚖️ Старый и современный подход

⚡ Различия в двух словах

Старый: декораторы без @functools.wraps, ручные фабрики, классы без аннотаций типов и прямое изменение полей.

Новый: @functools.wraps для сохранения метаданных, functools.partial для простых фабрик, аннотации типов, dataclasses и @property для контроля доступа.

Вывод: современный код проще читать, безопаснее поддерживать и лучше интегрируется с инструментами.

📜 Старый подход: декоратор без @functools.wraps

def debug(func):
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__}")
        return func(*args, **kwargs)
    return wrapper


@debug
def add(a, b):
    """Сложение двух чисел."""
    return a + b


print(add.__name__)  # wrapper
print(add.__doc__)   # None

Что здесь происходит: декоратор работает, но теряет имя и документацию оригинальной функции. Это мешает отладке и документации.

✅ Современный вариант

import functools


def debug(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__}")
        return func(*args, **kwargs)
    return wrapper


@debug
def add(a: int, b: int) -> int:
    """Сложение двух чисел."""
    return a + b


print(add.__name__)  # add
print(add.__doc__)   # Сложение двух чисел.

Что улучшилось: метаданные сохранены, добавлены аннотации типов. Редакторы могут подсказывать аргументы, а документация не ломается.

📜 Ручная фабрика для фиксации аргументов

def make_adder(value):
    def adder(x):
        return x + value
    return adder


add_five = make_adder(5)
print(add_five(3))

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

✅ Современный вариант: functools.partial

from functools import partial


def add(a: int, b: int) -> int:
    return a + b


add_five = partial(add, 5)
print(add_five(3))

Что улучшилось: functools.partial делает намерение очевидным и не требует вложенных функций для простых сценариев.

Проверить по документации: functools.partial создаёт вызываемый объект, а не обычную функцию. Подробнее — в документации functools.

📜 Класс без аннотаций типов

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    def get_description(self):
        return f"{self.title} — {self.author}"


book = Book("1984", "George Orwell")
print(book.get_description())

Что здесь происходит: код работает, но непонятно, какие типы ожидает конструктор и что возвращает метод.

✅ Современный вариант: аннотации типов

class Book:
    def __init__(self, title: str, author: str) -> None:
        self.title = title
        self.author = author

    def get_description(self) -> str:
        return f"{self.title} — {self.author}"


book = Book("1984", "George Orwell")
print(book.get_description())

Что улучшилось: контракт класса очевиден, IDE подсказывают ошибки, код легче поддерживать в команде.

Проверить по документации: руководство по аннотациям типов — в модуле typing и PEP 484.

✅ Современный вариант: dataclasses для простых моделей

from dataclasses import dataclass


@dataclass
class Book:
    title: str
    author: str

    def get_description(self) -> str:
        return f"{self.title} — {self.author}"


book = Book("1984", "George Orwell")
print(book.get_description())

Что улучшилось: декоратор @dataclass автоматически генерирует __init__, __repr__ и другие методы. Меньше шаблонного кода для классов-контейнеров данных.

Проверить по документации: возможности dataclasses — в документации dataclasses.

✅ Современный вариант: @property для контроля доступа

class Circle:
    def __init__(self, radius: float) -> None:
        self._radius = radius

    @property
    def radius(self) -> float:
        return self._radius

    @radius.setter
    def radius(self, value: float) -> None:
        if value < 0:
            raise ValueError("Радиус не может быть отрицательным")
        self._radius = value

    def get_area(self) -> float:
        from math import pi
        return pi * self.radius ** 2


circle = Circle(5)
print(circle.get_area())
circle.radius = 8
print(circle.get_area())

Что улучшилось: вместо прямого изменения поля используется свойство, которое может проверять значения и скрывать детали реализации.

Проверить по документации: механизм свойств — в документации property.

✅ Современный стиль: PEP 8

PEP 8 — руководство по стилю кода Python. Основные рекомендации:

  • Имена функций и переменных — snake_case.
  • Имена классов — CamelCase.
  • Имена констант — UPPER_SNAKE_CASE.
  • Отступы — 4 пробела.
  • Длина строки — не более 79–88 символов.
  • Импорты группируются: стандартная библиотека, сторонние модули, локальные модули.
Проверить по документации: полный текст руководства — в PEP 8.

🕰️ Когда старый подход ещё можно встретить

Учебные материалы и legacy-код могут не использовать аннотации типов, dataclasses или @property. Это нормально, но в новых проектах рекомендуется использовать современный стиль. Аннотации str | None и синтаксис list[str] требуют Python 3.10+.