Декораторы в Python
Декораторы в Python: замыкания, синтаксис @, functools.wraps, декораторы с аргументами и на классах, логирование, замер времени, кэширование.
Декоратор — это функция, которая принимает другую функцию (или класс) и возвращает новую, обычно расширяя её поведение, не меняя исходный код. Чтобы понять декораторы, нужно сначала разобраться с двумя вещами: функции как объекты первого класса и замыкания. Дальше идём от простого синтаксиса @ к декораторам с аргументами и практическим примерам.
Функции — объекты первого класса
В Python функция — это обычный объект. Её можно присвоить переменной, передать в другую функцию как аргумент и вернуть из функции. Именно это делает декораторы возможными.
def greet(name):
return f"Привет, {name}!"
# присваиваем функцию переменной (без скобок — не вызываем)
say = greet
print(say("Аня")) # Привет, Аня!
# функция как аргумент другой функции
def call_twice(func, value):
return func(value) + " " + func(value)
print(call_twice(greet, "Боб")) # Привет, Боб! Привет, Боб!
У функции есть атрибуты — например __name__ и __doc__. Они пригодятся позже, когда речь зайдёт о functools.wraps.
print(greet.__name__) # greet
Замыкания (closures)
Вложенная функция может «запоминать» переменные из внешней функции даже после того, как внешняя функция завершила работу. Такая связка вложенной функции с захваченным окружением и называется замыканием.
def make_multiplier(factor):
def multiply(x):
return x * factor # factor захвачен из внешней области
return multiply
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10)) # 20
print(triple(10)) # 30
Здесь double помнит factor = 2, а triple — factor = 3. Декоратор — это, по сути, частный случай замыкания: внешняя функция захватывает декорируемую функцию.
Что такое декоратор
Декоратор — это функция, которая принимает функцию и возвращает другую функцию. Покажем это без синтаксиса @, чтобы было видно, что происходит на самом деле.
def shout(func):
def wrapper(name):
result = func(name)
return result.upper()
return wrapper
def greet(name):
return f"привет, {name}"
greet = shout(greet) # оборачиваем вручную
print(greet("аня")) # ПРИВЕТ, АНЯ
Мы заменили greet на wrapper, который вызывает оригинал и дорабатывает результат.
Синтаксис @
Запись @decorator над определением функции — это просто короткая форма для func = decorator(func). Два примера ниже полностью эквивалентны.
# с синтаксисом @
@shout
def greet(name):
return f"привет, {name}"
# то же самое без @
def greet(name):
return f"привет, {name}"
greet = shout(greet)
Декоратор применяется в момент определения функции, один раз.
Простой декоратор-обёртка
Чтобы декоратор работал с любыми функциями (с любым числом аргументов), используют *args и **kwargs. Это универсальный шаблон обёртки.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Вызов {func.__name__} с args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"Результат: {result}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
add(2, 3)
# Вызов add с args=(2, 3), kwargs={}
# Результат: 5
Важно: wrapper обязательно возвращает результат оригинальной функции, иначе декорированная функция начнёт возвращать None.
functools.wraps — зачем он нужен
После декорирования имя и докстринг функции «теряются»: вместо них видны данные внутренней wrapper. Это ломает интроспекцию, документацию и отладку.
def log_call(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
"""Складывает два числа."""
return a + b
print(add.__name__) # wrapper (а ожидали add!)
print(add.__doc__) # None
Решение — обернуть wrapper декоратором functools.wraps(func). Он копирует __name__, __doc__ и другие метаданные с оригинала.
import functools
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b):
"""Складывает два числа."""
return a + b
print(add.__name__) # add
print(add.__doc__) # Складывает два числа.
Правило: всегда добавляйте @functools.wraps(func) к своим декораторам.
Декоратор с аргументами
Иногда декоратор нужно настраивать. Тогда добавляют ещё один уровень вложенности: внешняя функция принимает аргументы и возвращает сам декоратор. Получается три уровня функций.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def hello():
print("Привет!")
hello()
# Привет!
# Привет!
# Привет!
Запись @repeat(times=3) сначала вызывает repeat(3) — это возвращает decorator, который затем применяется к hello.
Несколько декораторов и порядок применения
Декораторы можно складывать стопкой. Применяются они снизу вверх (ближайший к функции — первым), а при вызове оборачивающий код выполняется сверху вниз.
import functools
def bold(func):
@functools.wraps(func)
def wrapper(*a, **k):
return "" + func(*a, **k) + ""
return wrapper
def italic(func):
@functools.wraps(func)
def wrapper(*a, **k):
return "" + func(*a, **k) + ""
return wrapper
@bold
@italic
def text():
return "привет"
print(text()) # привет
Эта запись эквивалентна text = bold(italic(text)). Сначала навешивается italic (он ближе к функции), потом bold оборачивает уже декорированную версию.
| Запись | Что эквивалентно | Результат |
|---|---|---|
| @bold + @italic | bold(italic(text)) | <b><i>...</i></b> |
| @italic + @bold | italic(bold(text)) | <i><b>...</b></i> |
Декораторы на классах
Здесь есть два разных случая, которые легко перепутать.
Декорирование методов класса
Обычный декоратор-функция отлично работает и с методами — просто помните про self в *args.
import functools
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"-> {func.__name__}")
return func(*args, **kwargs)
return wrapper
class Account:
def __init__(self):
self.balance = 0
@log_call
def deposit(self, amount):
self.balance += amount
return self.balance
Account().deposit(100) # -> deposit
Декоратор в виде класса
Декоратором может быть и сам класс — если у него есть метод __call__. Это удобно, когда декоратору нужно хранить состояние.
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"Вызов №{self.count}")
return self.func(*args, **kwargs)
@CountCalls
def say_hi():
print("Привет")
say_hi() # Вызов №1 / Привет
say_hi() # Вызов №2 / Привет
print(say_hi.count) # 2
Практика: логирование
Базовый и самый частый сценарий — логировать вызовы и результаты функций, не засоряя их тело.
import functools
def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"[LOG] -> {result}")
return result
return wrapper
@logged
def multiply(a, b):
return a * b
multiply(4, 5)
# [LOG] multiply((4, 5), {})
# [LOG] -> 20
Практика: замер времени
Декоратор-таймер измеряет, сколько выполняется функция. Полезно для профилирования.
import functools
import time
def timeit(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} заняла {elapsed:.4f} с")
return result
return wrapper
@timeit
def slow_sum(n):
return sum(range(n))
slow_sum(1_000_000)
# slow_sum заняла 0.0123 с
Практика: кэширование через @lru_cache
В стандартной библиотеке уже есть готовый декоратор кэширования — functools.lru_cache. Он запоминает результаты по аргументам и при повторном вызове не пересчитывает. Особенно заметно на рекурсии.
import functools
@functools.lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
print(fib(35)) # 9227465 — мгновенно
print(fib.cache_info()) # CacheInfo(hits=33, misses=36, ...)
В Python 3.9+ есть упрощённый @functools.cache — это то же, что lru_cache(maxsize=None). Кэшируемые аргументы должны быть хешируемыми (например, числа и строки, но не списки).
Практика: проверка прав и повтор при ошибке
Декораторы удобны для «сквозной» логики — авторизации и устойчивости к сбоям.
Проверка прав
import functools
def require_admin(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_admin"):
raise PermissionError("Нужны права администратора")
return func(user, *args, **kwargs)
return wrapper
@require_admin
def delete_post(user, post_id):
return f"Пост {post_id} удалён"
print(delete_post({"is_admin": True}, 42)) # Пост 42 удалён
# delete_post({"is_admin": False}, 42) -> PermissionError
Повтор при ошибке (retry)
import functools
import time
def retry(times=3, delay=0.5):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
print(f"Попытка {attempt} не удалась: {e}")
time.sleep(delay)
raise last_error
return wrapper
return decorator
@retry(times=3, delay=1)
def fetch_data():
raise ConnectionError("сеть недоступна")
# fetch_data()
# Попытка 1 не удалась: сеть недоступна
# Попытка 2 не удалась: сеть недоступна
# Попытка 3 не удалась: сеть недоступна
# -> ConnectionError
Шпаргалка по шаблонам
| Задача | Шаблон |
|---|---|
| Простой декоратор | def deco(func): ... return wrapper |
| Сохранить метаданные | @functools.wraps(func) над wrapper |
| Декоратор с аргументами | три уровня: deco(args) -> decorator -> wrapper |
| Декоратор с состоянием | класс с __call__ |
| Готовый кэш | @functools.lru_cache / @functools.cache |
Главное, что стоит запомнить: декоратор — это func = decorator(func), обёртка должна возвращать результат, а functools.wraps добавляется почти всегда.