Декораторы в Python: как работают и где применяются

Полный разбор декораторов в Python: синтаксис @, functools.wraps, декораторы с аргументами, классы-декораторы, стекирование, lru_cache, retry-логика и типичные ошибки на собеседовании.

02 марта 2026 г.20 минLexicon Team

Декораторы — одна из тех тем, которые на первый взгляд выглядят как «просто синтаксический сахар», но за которыми скрывается значительная часть Python-экосистемы. Весь Django покрыт декораторами (@login_required, @permission_required, @csrf_exempt). FastAPI построен на них (@app.get, @app.post). В стандартной библиотеке они везде: @property, @staticmethod, @classmethod, @functools.lru_cache. Понять декораторы — значит понять, как Python устроен изнутри.

На собеседованиях декораторы спрашивают практически всегда, начиная с уровня junior. Поверхностного «синтаксис @ — это обёртка» недостаточно. Хочется услышать: как работает functools.wraps и зачем он нужен, как сделать декоратор с аргументами, чем класс-декоратор отличается от функции, в каком порядке применяются стекированные декораторы. Именно это мы и разберём — от первых принципов до реальных паттернов из продакшн-кода.

Для понимания этой статьи полезно уже знать основы asyncio — там декораторы используются очень активно. А подборку 50 вопросов по Python можно использовать как чеклист после прочтения.

Python на собеседовании — без стресса

50 вопросов с разбором: декораторы, GIL, asyncio и паттерны.

Начать тренировку

1. Функции — объекты первого класса

Понять декораторы без понимания модели функций в Python невозможно. В Python функции — это объекты первого класса. Это не метафора и не маркетинг: функция — такой же объект, как число или строка. У неё есть тип (function), она хранится в памяти, имеет атрибуты, и с ней можно делать всё то же самое, что с любым другим объектом.

Это означает три вещи, важных для декораторов.

Функцию можно присвоить переменной:

def greet(name):
    return f"Привет, {name}!"

say_hello = greet          # say_hello — ещё одно имя для того же объекта-функции
print(say_hello("Алиса"))  # Привет, Алиса!
print(greet is say_hello)  # True — один и тот же объект

Функцию можно передать в другую функцию как аргумент:

def apply(func, value):
    return func(value)

def double(x):
    return x * 2

def square(x):
    return x ** 2

print(apply(double, 5))   # 10
print(apply(square, 5))   # 25
print(apply(str, 42))     # '42'

Функция может возвращать другую функцию:

def make_multiplier(n):
    def multiplier(x):
        return x * n   # n захвачена из внешней области видимости (closure)
    return multiplier  # возвращаем функцию, не результат её вызова

triple = make_multiplier(3)
print(triple(10))   # 30
print(triple(7))    # 21

# Каждый вызов make_multiplier создаёт новую функцию с новым n
double = make_multiplier(2)
print(double(10))   # 20

Функция multiplier внутри make_multiplier образует замыкание (closure): она «помнит» переменную n из внешней области видимости даже после того, как make_multiplier вернула управление. Это фундаментальный механизм, на котором строятся декораторы.

Убедиться в существовании замыкания можно через атрибут __closure__:

triple = make_multiplier(3)
print(triple.__closure__)           # (<cell at 0x...>,)
print(triple.__closure__[0].cell_contents)  # 3

2. Декоратор без синтаксиса @

Теперь, когда понятно, что функцию можно передавать и возвращать, декоратор — это просто функция высшего порядка: принимает функцию, возвращает (обычно) функцию.

Напишем первый декоратор вручную, без всякого @:

def log_call(func):
    """Декоратор: логирует вызовы функции."""
    def wrapper(*args, **kwargs):
        print(f"Вызов {func.__name__} с аргументами {args}, {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} вернула {result!r}")
        return result
    return wrapper

def add(a, b):
    return a + b

# Применяем декоратор вручную: переприсваиваем имя
add = log_call(add)

add(2, 3)
# Вызов add с аргументами (2, 3), {}
# add вернула 5

Что здесь происходит: log_call(add) получает объект функции add, создаёт новую функцию wrapper, которая вызывает оригинальный add внутри себя (через замыкание), и возвращает wrapper. Затем мы переприсваиваем имя add этой новой функции. Теперь когда кто-то вызывает add(2, 3), на самом деле вызывается wrapper(2, 3), которая логирует и делегирует.

Именно это и делает синтаксис @ — автоматически.

3. Синтаксис @decorator

@decorator перед определением функции — это синтаксический сахар, введённый в Python 2.4. Компилятор разворачивает его в точности так:

@decorator
def func():
    ...

# Эквивалентно:
def func():
    ...
func = decorator(func)

Ничего магического: интерпретатор вычисляет выражение после @ (это может быть любое выражение, не только имя), определяет функцию, а потом вызывает результат вычисления с функцией как аргументом.

Применим наш log_call уже через @:

def log_call(func):
    def wrapper(*args, **kwargs):
        print(f"→ {func.__name__}({args}, {kwargs})")
        result = func(*args, **kwargs)
        print(f"← {func.__name__} = {result!r}")
        return result
    return wrapper

@log_call
def multiply(a, b):
    return a * b

@log_call
def greet(name):
    return f"Привет, {name}!"

multiply(3, 4)
# → multiply((3, 4), {})
# ← multiply = 12

greet("Боря")
# → greet(('Боря',), {})
# ← greet = 'Привет, Боря!'

Использование *args и **kwargs в wrapper — стандарт: декоратор не знает заранее, какую сигнатуру имеет декорируемая функция. *args, **kwargs принимает любые аргументы и передаёт их дальше как есть.

4. functools.wraps — почему нельзя без него

У декоратора из предыдущего раздела есть незаметная, но серьёзная проблема. Посмотрим на метаданные функции после декорирования:

def log_call(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log_call
def multiply(a, b):
    """Перемножает два числа."""
    return a * b

print(multiply.__name__)   # wrapper   ← должно быть multiply
print(multiply.__doc__)    # None      ← должно быть "Перемножает два числа."
print(multiply.__module__) # __main__  ← это совпало случайно

Декорированная функция потеряла своё имя и документацию. Для инструментов интроспекции, логирования, отладчиков, Sphinx и pytest это катастрофа: все будут видеть wrapper вместо реального имени функции.

functools.wraps решает это одной строкой:

import functools

def log_call(func):
    @functools.wraps(func)   # копирует метаданные из func в wrapper
    def wrapper(*args, **kwargs):
        print(f"→ {func.__name__}")
        result = func(*args, **kwargs)
        print(f"← {result!r}")
        return result
    return wrapper

@log_call
def multiply(a, b):
    """Перемножает два числа."""
    return a * b

print(multiply.__name__)   # multiply  ✓
print(multiply.__doc__)    # Перемножает два числа.  ✓

functools.wraps(func) — это сам декоратор, который применяется к wrapper. Под капотом он использует functools.update_wrapper, который копирует атрибуты: __name__, __qualname__, __doc__, __dict__, __module__, __annotations__, __wrapped__. Последний особенно полезен: multiply.__wrapped__ — это оригинальная функция, что позволяет получить доступ к исходному коду обходя декоратор.

# __wrapped__ позволяет тестировать оригинальную функцию без декоратора
original_multiply = multiply.__wrapped__
print(original_multiply(3, 4))   # 12, без логирования

Правило: functools.wraps должен быть в каждом декораторе. Без него декоратор неполноценен.

5. Декораторы с аргументами

Иногда декоратору нужны параметры. Например, @repeat(n=3) — повторить вызов три раза. Логика такова: @repeat(n=3) должен сначала вернуть декоратор, который уже принимает функцию.

Это три уровня вложенности:

import functools

def repeat(n=1):
    """Декоратор-фабрика: возвращает декоратор, который вызывает функцию n раз."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = None
            for _ in range(n):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(n=3)
def say(message):
    print(message)

say("Привет")
# Привет
# Привет
# Привет

Развернём @repeat(n=3) вручную, чтобы убедиться в механике:

# @repeat(n=3) эквивалентно:
decorator = repeat(n=3)  # шаг 1: вызываем фабрику, получаем декоратор
say = decorator(say)     # шаг 2: декоратор применяется к функции

Это ключевое отличие от декораторов без аргументов: @log_call сразу применяет декоратор к функции, а @repeat(n=3) сначала вызывает repeat(n=3) и только потом применяет результат.

Частая ошибка на собеседовании: написать @repeat вместо @repeat() и удивляться ошибке.

Можно также сделать аргументы необязательными через functools.wraps и небольшой трюк:

import functools

def repeat(_func=None, *, n=1):
    """Работает и как @repeat, и как @repeat(n=3)."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(n):
                func(*args, **kwargs)
        return wrapper

    if _func is not None:
        # Вызов без аргументов: @repeat
        return decorator(_func)
    # Вызов с аргументами: @repeat(n=3)
    return decorator

@repeat
def once():
    print("раз")

@repeat(n=2)
def twice():
    print("два")

once()    # раз
twice()   # два  два

6. Классы как декораторы

Декоратором может быть не только функция, но и класс. Для этого класс должен реализовать __init__ (получает декорируемую функцию) и __call__ (вызывается вместо декорированной функции):

import functools

class CallCounter:
    """Декоратор-класс: считает количество вызовов функции."""

    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.call_count = 0

    def __call__(self, *args, **kwargs):
        self.call_count += 1
        print(f"[{self.func.__name__}] вызов #{self.call_count}")
        return self.func(*args, **kwargs)

@CallCounter
def process(x):
    return x * 2

process(5)   # [process] вызов #1
process(10)  # [process] вызов #2
process(15)  # [process] вызов #3

print(process.call_count)  # 3 — доступ к атрибуту декоратора

Заметим: functools.update_wrapper(self, func) делает то же, что @functools.wraps, только применяется в __init__ к экземпляру класса.

Класс-декоратор удобен когда:

  • нужно хранить состояние между вызовами (счётчики, кэш, накопленные данные);
  • нужно предоставить дополнительный интерфейс через атрибуты или методы;
  • хочется наследоваться от базового декоратора.

Функция-декоратор удобнее в большинстве остальных случаев — меньше кода, проще читается.

Сравним реализацию кэша через класс и через замыкание:

# Через класс — явное состояние
class memoize:
    def __init__(self, func):
        functools.update_wrapper(self, func)
        self.func = func
        self.cache = {}

    def __call__(self, *args):
        if args not in self.cache:
            self.cache[args] = self.func(*args)
        return self.cache[args]

    def clear_cache(self):
        self.cache.clear()

@memoize
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)

print(fib(50))           # 12586269025 — быстро благодаря кэшу
fib.clear_cache()        # дополнительный метод — удобство класса

7. Стекирование декораторов

На одну функцию можно навесить несколько декораторов. Они применяются снизу вверх — ближайший к определению функции применяется первым:

import functools

def bold(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"**{func(*args, **kwargs)}**"
    return wrapper

def italic(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return f"_{func(*args, **kwargs)}_"
    return wrapper

def upper(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs).upper()
    return wrapper

@bold
@italic
@upper
def greet(name):
    return f"привет, {name}"

print(greet("алиса"))
# **_ПРИВЕТ, АЛИСА_**

Разворачивается это так:

# Порядок применения: снизу вверх
temp = upper(greet)      # 1. upper применяется первым
temp = italic(temp)      # 2. italic — к результату
greet = bold(temp)       # 3. bold — к результату

# При вызове greet("алиса"):
# bold.wrapper вызывает italic.wrapper
# italic.wrapper вызывает upper.wrapper
# upper.wrapper вызывает оригинальный greet
# Результаты оборачиваются в обратном порядке

Типичная ошибка — неправильная интуиция о порядке. Многие думают, что @bold применяется первым, потому что стоит выше. Верно обратное: @bold идёт снаружи, потому что применяется последним.

Ещё одна ловушка — декоратор с побочными эффектами при применении (не при вызове). Если декоратор что-то делает в теле (не в wrapper), это происходит в момент применения:

def register(func):
    print(f"Регистрируем {func.__name__}")  # выполняется при @register
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@register  # ← прямо здесь напечатает "Регистрируем handler"
def handler():
    pass

Python-вопросы в Telegram

Ежедневные разборы GIL, asyncio и паттернов.

Подписаться

8. Практические паттерны

Декораторы — не академическая абстракция. Разберём паттерны, которые реально встречаются в продакшн-коде.

8.1 Кэширование: @lru_cache и @cache

functools.lru_cache — встроенный декоратор для мемоизации с ограничением размера кэша (LRU — Least Recently Used):

from functools import lru_cache, cache
import time

@lru_cache(maxsize=128)
def fibonacci(n):
    if n < 2:
        return n
    return fibonacci(n - 1) + fibonacci(n - 2)

start = time.perf_counter()
print(fibonacci(100))   # 354224848179261915075
elapsed = time.perf_counter() - start
print(f"{elapsed:.6f}с")   # ~0.000050с — мгновенно благодаря кэшу

# Статистика кэша
print(fibonacci.cache_info())
# CacheInfo(hits=98, misses=101, maxsize=128, currsize=101)

# Очистка кэша
fibonacci.cache_clear()

В Python 3.9+ появился @cache — эквивалент @lru_cache(maxsize=None), кэш без ограничений. Быстрее за счёт отсутствия логики LRU, но память не ограничена:

from functools import cache

@cache
def expensive_computation(n):
    # Например, запрос к внешнему API или тяжёлые вычисления
    time.sleep(0.1)
    return n ** 2

print(expensive_computation(10))  # 0.1с
print(expensive_computation(10))  # мгновенно — из кэша

Важно: lru_cache работает только с хэшируемыми аргументами. Списки, словари и другие изменяемые типы не подходят — передавайте tuple вместо list.

8.2 Измерение времени выполнения

Классический декоратор для профилирования — один из первых, что пишут при знакомстве с декораторами:

import functools
import time

def timeit(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            result = func(*args, **kwargs)
            return result
        finally:
            elapsed = time.perf_counter() - start
            print(f"{func.__name__} выполнилась за {elapsed:.4f}с")
    return wrapper

@timeit
def slow_sort(data):
    return sorted(data)

import random
data = random.sample(range(1_000_000), 100_000)
slow_sort(data)
# slow_sort выполнилась за 0.0421с

finally гарантирует, что время будет напечатано даже если функция бросит исключение.

8.3 Retry-логика

Декоратор для повторных попыток при исключениях — незаменим при работе с нестабильными сетевыми ресурсами:

import functools
import time
import random

def retry(max_attempts=3, delay=1.0, exceptions=(Exception,)):
    """
    Повторяет вызов функции при исключении.

    :param max_attempts: максимальное число попыток
    :param delay: задержка между попытками в секундах
    :param exceptions: кортеж типов исключений для перехвата
    """
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            last_exception = None
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    last_exception = e
                    if attempt < max_attempts:
                        print(f"Попытка {attempt}/{max_attempts} неудачна: {e}. "
                              f"Повтор через {delay}с...")
                        time.sleep(delay)
                    else:
                        print(f"Все {max_attempts} попыток исчерпаны.")
            raise last_exception
        return wrapper
    return decorator

@retry(max_attempts=3, delay=0.5, exceptions=(ConnectionError, TimeoutError))
def fetch_data(url):
    # Симулируем нестабильное соединение
    if random.random() < 0.7:
        raise ConnectionError(f"Не удалось подключиться к {url}")
    return {"data": "ok"}

try:
    result = fetch_data("https://api.example.com/data")
    print(result)
except ConnectionError as e:
    print(f"Финальная ошибка: {e}")

8.4 Авторизация / проверка прав

Декораторы — идиоматический способ добавить проверку прав доступа. Django использует именно этот паттерн:

import functools

# Симуляция контекста текущего пользователя
current_user = {"name": "Алиса", "roles": ["user"]}

def require_role(*required_roles):
    """Разрешает вызов только если у пользователя есть нужная роль."""
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            user_roles = set(current_user.get("roles", []))
            if not user_roles.intersection(required_roles):
                raise PermissionError(
                    f"Требуется одна из ролей: {required_roles}. "
                    f"У пользователя: {user_roles}"
                )
            return func(*args, **kwargs)
        return wrapper
    return decorator

@require_role("admin")
def delete_user(user_id):
    print(f"Пользователь {user_id} удалён")

@require_role("admin", "moderator")
def ban_user(user_id):
    print(f"Пользователь {user_id} заблокирован")

try:
    delete_user(42)
except PermissionError as e:
    print(e)
# Требуется одна из ролей: ('admin',). У пользователя: {'user'}

current_user["roles"].append("admin")
delete_user(42)   # Пользователь 42 удалён ✓

8.5 Логирование и трейсинг

Декоратор для структурированного логирования — полезный инструмент для трассировки выполнения в сложных системах:

import functools
import logging
import json
from datetime import datetime

logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger(__name__)

def trace(func):
    """Логирует вход, выход и исключения с контекстом."""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        call_id = f"{func.__name__}_{id(wrapper)}"
        entry = {
            "event": "call",
            "function": func.__name__,
            "args": repr(args),
            "kwargs": repr(kwargs),
            "timestamp": datetime.utcnow().isoformat(),
        }
        logger.info(json.dumps(entry, ensure_ascii=False))

        try:
            result = func(*args, **kwargs)
            exit_entry = {
                "event": "return",
                "function": func.__name__,
                "result": repr(result),
            }
            logger.info(json.dumps(exit_entry, ensure_ascii=False))
            return result
        except Exception as e:
            error_entry = {
                "event": "error",
                "function": func.__name__,
                "error": str(e),
                "error_type": type(e).__name__,
            }
            logger.error(json.dumps(error_entry, ensure_ascii=False))
            raise
    return wrapper

@trace
def calculate(a, b):
    return a / b

calculate(10, 2)
calculate(10, 0)   # ← исключение, но залогируется

9. Декораторы в популярных библиотеках

Понимая механику декораторов, легче читать и использовать фреймворки.

@dataclass

Один из самых мощных встроенных декораторов. @dataclass анализирует аннотации класса и автоматически генерирует __init__, __repr__, __eq__ и другие методы:

from dataclasses import dataclass, field

@dataclass(order=True, frozen=True)
class Point:
    x: float
    y: float
    label: str = field(default="", compare=False)

p1 = Point(1.0, 2.0, "A")
p2 = Point(3.0, 4.0, "B")
p3 = Point(1.0, 2.0, "C")   # label=C, но compare=False

print(p1)           # Point(x=1.0, y=2.0, label='A')
print(p1 == p3)     # True — label игнорируется при сравнении
print(p1 < p2)      # True — order=True добавляет __lt__, __le__, etc.
# p1.x = 5.0  # AttributeError — frozen=True запрещает изменение

@property, @staticmethod, @classmethod

Три встроенных дескриптор-декоратора, с которыми сталкивается каждый Python-разработчик:

class Temperature:
    def __init__(self, celsius: float):
        self._celsius = celsius

    @property
    def celsius(self) -> float:
        """Геттер — доступ как к атрибуту."""
        return self._celsius

    @celsius.setter
    def celsius(self, value: float):
        if value < -273.15:
            raise ValueError("Ниже абсолютного нуля невозможно")
        self._celsius = value

    @property
    def fahrenheit(self) -> float:
        return self._celsius * 9 / 5 + 32

    @classmethod
    def from_fahrenheit(cls, f: float) -> "Temperature":
        """Альтернативный конструктор — получает доступ к классу."""
        return cls((f - 32) * 5 / 9)

    @staticmethod
    def is_valid(value: float) -> bool:
        """Утилитарная функция — не нужен ни self, ни cls."""
        return value >= -273.15

t = Temperature(100)
print(t.celsius)      # 100
print(t.fahrenheit)   # 212.0
t.celsius = 0
print(t.celsius)      # 0

t2 = Temperature.from_fahrenheit(98.6)
print(f"{t2.celsius:.1f}°C")   # 37.0°C

print(Temperature.is_valid(-300))  # False

@app.route / @router.get

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

# Flask
from flask import Flask
app = Flask(__name__)

@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id: int):
    return {"id": user_id, "name": "Алиса"}

# FastAPI
from fastapi import FastAPI, Depends
app = FastAPI()

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {"id": user_id}

@app.route("/users/<int:user_id>") — это декоратор с аргументами. app.route(...) возвращает декоратор, который принимает get_user и регистрирует его в таблице маршрутов Flask.

@login_required и @permission_required в Django

from django.contrib.auth.decorators import login_required, permission_required
from django.views import View

@login_required
def profile_view(request):
    return render(request, "profile.html", {"user": request.user})

@permission_required("app.can_publish", raise_exception=True)
def publish_article(request, article_id):
    ...

@validator / @field_validator в Pydantic

# Pydantic v1
from pydantic import BaseModel, validator

class User(BaseModel):
    name: str
    age: int

    @validator("age")
    def age_must_be_positive(cls, v):
        if v <= 0:
            raise ValueError("Возраст должен быть положительным")
        return v

# Pydantic v2
from pydantic import BaseModel, field_validator

class User(BaseModel):
    name: str
    age: int

    @field_validator("age")
    @classmethod
    def age_must_be_positive(cls, v: int) -> int:
        if v <= 0:
            raise ValueError("Возраст должен быть положительным")
        return v

10. Типичные ошибки

Забыть functools.wraps

Самая распространённая. Без @functools.wraps декорированная функция теряет имя, документацию и модуль. Это ломает help(), логирование, pytest, Sphinx и многое другое. Правило простое: @functools.wraps — в каждом декораторе.

Декоратор с () vs без ()

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

def retry(max_attempts=3):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for _ in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    pass
        return wrapper
    return decorator

@retry()     # ← правильно: retry() вернёт decorator
def fetch(): ...

@retry       # ← неправильно: retry — функция, она применится к fetch напрямую
def fetch(): ...
# TypeError: decorator() missing 1 required positional argument: 'func'

Мутабельное состояние в замыкании

Замыкания захватывают переменную по ссылке, не по значению. Классическая ловушка с циклом:

# Проблема
def make_handlers():
    handlers = []
    for i in range(3):
        def handler():
            return i   # i — ссылка, не значение
        handlers.append(handler)
    return handlers

h = make_handlers()
print(h[0]())  # 2, а не 0!
print(h[1]())  # 2
print(h[2]())  # 2
# Все три handler видят i = 2 (последнее значение цикла)

# Решение: захватить значение через аргумент по умолчанию
def make_handlers_fixed():
    handlers = []
    for i in range(3):
        def handler(i=i):  # i=i захватывает текущее значение
            return i
        handlers.append(handler)
    return handlers

h = make_handlers_fixed()
print(h[0]())  # 0 ✓
print(h[1]())  # 1 ✓
print(h[2]())  # 2 ✓

Декорирование методов класса (self)

Декоратор, написанный для обычных функций, обычно работает с методами без изменений — self передаётся как первый позиционный аргумент через *args. Но есть нюанс с @staticmethod и @classmethod: они должны идти снаружи:

class MyClass:
    @staticmethod
    @timeit                # ← правильный порядок: staticmethod снаружи
    def static_method():
        time.sleep(0.01)

    # Не @timeit @staticmethod — это не сработает корректно

Также осторожно с декораторами, которые пытаются обращаться к атрибутам функции через func.__self__ или похожим — для несвязанных методов это недоступно.

11. Что спросят на собеседовании

Декораторы — обязательная тема для junior и middle Python-разработчиков. Разберём типичные вопросы.

Что такое декоратор? Функция (или класс), которая принимает функцию и возвращает функцию с расширенным поведением. Синтаксис @decorator разворачивается в func = decorator(func).

Что делает functools.wraps? Копирует метаданные (__name__, __doc__, __module__ и другие) из оригинальной функции на функцию-обёртку. Без него декоратор «маскирует» оригинальную функцию, что ломает интроспекцию, логирование и тесты.

Как написать декоратор с аргументами? Три уровня: фабрика принимает аргументы и возвращает декоратор, декоратор принимает функцию и возвращает обёртку, обёртка выполняет логику. Разворачивается так: @repeat(n=3)repeat(n=3) возвращает decoratordecorator(func) возвращает wrapper.

В каком порядке применяются стекированные декораторы? Снизу вверх: ближайший к def применяется первым. @A @B def f() разворачивается в f = A(B(f)). При вызове: A.wrapper → B.wrapper → f.

Когда использовать класс-декоратор вместо функции? Когда нужно состояние между вызовами (счётчики, накопленный контекст), дополнительный интерфейс через методы или наследование. Для большинства случаев функции достаточно.

Что такое __wrapped__? Атрибут, который functools.wraps устанавливает на обёртке — ссылка на оригинальную функцию. Позволяет получить доступ к исходной функции, минуя декоратор. Полезно в тестах: func.__wrapped__(args) вызывает оригинал без дополнительной логики.

Можно ли декорировать класс? Да. Декоратор класса принимает класс и возвращает класс (или объект-заменитель). Именно так работает @dataclass — принимает класс, добавляет методы, возвращает изменённый класс.

def add_repr(cls):
    """Добавляет __repr__ к классу, если его нет."""
    if "__repr__" not in cls.__dict__:
        def __repr__(self):
            attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
            return f"{cls.__name__}({attrs})"
        cls.__repr__ = __repr__
    return cls

@add_repr
class Config:
    def __init__(self, host, port):
        self.host = host
        self.port = port

c = Config("localhost", 8080)
print(c)   # Config(host='localhost', port=8080)

Хотите проверить знания на реальных вопросах? Посмотрите разбор 50 вопросов по Python. Для понимания @asyncio.coroutine и async def (которые работают через ту же механику декораторов) — asyncio: как работает event loop. Модель памяти, на которой живут замыкания декораторов — в управлении памятью в Python.

Потренируйся на реальных вопросах

50 вопросов по Python с разбором — декораторы, GIL, asyncio и паттерны.

Начать тренировку

Автор

Lexicon Team

Читайте также