📖 Теория: декораторы в практикуме

⚡ Кратко

Декоратор — функция, которая оборачивает другую функцию. Параметризованный декоратор — фабрика декораторов. Состояние декоратора хранится в замыкании. Для кэширования используют словарь, для логирования — модуль logging, для проверки типов — isinstance.

Базовый декоратор

Декоратор принимает функцию, добавляет поведение и возвращает новую функцию — обёртку. Внутри используется вложенная функция и замыкание.

def simple_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before call")
        result = func(*args, **kwargs)
        print("After call")
        return result
    return wrapper


@simple_decorator
def greet(name: str) -> str:
    return f"Hello, {name}!"


print(greet("Alice"))

Параметризованный декоратор

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

def repeat(times: int):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for _ in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator


@repeat(times=3)
def say_hi():
    print("Hi")


say_hi()

Состояние в замыкании

Декоратор может хранить данные между вызовами: кэш результатов, счётчики обращений, журналы. Изменяемые объекты (словари, множества, списки) удобно держать в замыкании внешней функции.

def call_counter(func):
    counts: dict[str, int] = {}

    def wrapper(*args, **kwargs):
        name = func.__name__
        counts[name] = counts.get(name, 0) + 1
        print(f"{name} called {counts[name]} times")
        return func(*args, **kwargs)

    return wrapper

Логирование через logging

Модуль logging предоставляет гибкую систему записи событий. Базовую настройку задают через logging.basicConfig: имя файла, уровень, формат сообщения, кодировка.

import logging

logging.basicConfig(
    filename="app.log",
    level=logging.INFO,
    format="%(message)s",
    encoding="utf-8",
)

Проверка типов аргументов

Функция isinstance(obj, type_) проверяет, принадлежит ли объект указанному типу. В отличие от сравнения type(obj) is type_, она учитывает наследование.

def accepts_int(func):
    def wrapper(value):
        if not isinstance(value, int):
            raise TypeError(f"Expected int, got {type(value).__name__}")
        return func(value)
    return wrapper

Современные замечания

  • PEP 8: имена функций и переменных — snake_case, имена декораторов тоже.
  • f-strings: предпочтительнее конкатенации и str.format для читаемости.
  • Аннотации типов: помогают IDE и статическим анализаторам, но не влияют на выполнение.
  • Guard clauses: ранняя проверка и выход из функции упрощает вложенность.
  • functools.wraps: сохраняет __name__, __doc__ и другие атрибуты исходной функции.