Декораторы в Python: как работают и где применяются
Полный разбор декораторов в Python: синтаксис @, functools.wraps, декораторы с аргументами, классы-декораторы, стекирование, lru_cache, retry-логика и типичные ошибки на собеседовании.
- 1. Функции — объекты первого класса
- 2. Декоратор без синтаксиса @
- 3. Синтаксис @decorator
- 4. functools.wraps — почему нельзя без него
- 5. Декораторы с аргументами
- 6. Классы как декораторы
- 7. Стекирование декораторов
- 8. Практические паттерны
- 8.1 Кэширование: @lru_cache и @cache
- 8.2 Измерение времени выполнения
- 8.3 Retry-логика
- 8.4 Авторизация / проверка прав
- 8.5 Логирование и трейсинг
- 9. Декораторы в популярных библиотеках
- @dataclass
- @property, @staticmethod, @classmethod
- @app.route / @router.get
- @login_required и @permission_required в Django
- @validator / @field_validator в Pydantic
- 10. Типичные ошибки
- Забыть functools.wraps
- Декоратор с () vs без ()
- Мутабельное состояние в замыкании
- Декорирование методов класса (self)
- 11. Что спросят на собеседовании
Декораторы — одна из тех тем, которые на первый взгляд выглядят как «просто синтаксический сахар», но за которыми скрывается значительная часть 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) возвращает decorator → decorator(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
Читайте также
backend
Управление памятью в Python: reference counting, циклический GC и утечки
Глубокий разбор управления памятью в CPython: PyObject, reference counting, циклический GC, weakref, __slots__, pymalloc и диагностика утечек с tracemalloc.
backend
Python: 50 вопросов на собеседовании junior–middle
Разбор 50 реальных вопросов по Python для junior и middle: типы данных, ООП, GIL и память, asyncio, декораторы и структура проекта. Примеры кода и объяснения.
general
Как проходит IT-собеседование в 2026: этапы, вопросы и лайфхаки
Полный разбор процесса найма в IT в 2026 году: HR-скрининг, тестовое задание, техническое интервью, системный дизайн, финальный разговор. Как подготовиться и что отвечать.