Декораторы и модуль functools
Декораторы в Python — это мощный инструмент для модификации поведения функций и методов, позволяющий писать чистый, повторно используемый и легко читаемый код. В этой главе мы глубоко погрузимся в мир декораторов, изучим их синтаксис, создание, применение, а также познакомимся с полезными инструментами из модуля functools.
Синтаксис декораторов
Декоратор — это функция, которая принимает другую функцию и возвращает новую функцию (или любой другой объект). Синтаксис декоратора — это «синтаксический сахар», который делает код более понятным.
@decorator
def foo(x):
return 42
Эквивалентно:
def foo(x):
return 42
foo = decorator(foo)
Таким образом, после применения декоратора имя foo будет ссылаться на результат вызова decorator(foo).
«Теория» декораторов: создание простого декоратора
Рассмотрим пример декоратора trace, который логирует вызовы функции:
def trace(func):
def inner(*args, **kwargs):
print(func.__name__, args, kwargs)
return func(*args, **kwargs)
return inner
Применим его:
@trace
def identity(x):
"I do nothing useful."
return x
identity(42) # Вывод: identity (42,) {} → 42
Проблема атрибутов функции
После применения декоратора исходные атрибуты функции (такие как __name__, __doc__) теряются:
identity.__name__ # 'inner'
help(identity) # Справка о inner, а не identity
Решение: functools.wraps
Модуль functools предоставляет декоратор wraps, который копирует метаданные исходной функции в декорированную:
import functools
def trace(func):
@functools.wraps(func)
def inner(*args, **kwargs):
print(func.__name__, args, kwargs)
return func(*args, **kwargs)
return inner
Теперь identity.__name__ и help(identity) работают корректно.
Декораторы с аргументами
Иногда нужно параметризовать декоратор. Например, указать файл для вывода в trace:
def trace(handle):
def decorator(func):
@functools.wraps(func)
def inner(*args, **kwargs):
print(func.__name__, args, kwargs, file=handle)
return func(*args, **kwargs)
return inner
return decorator
@trace(sys.stderr)
def identity(x):
return x
Эквивалентно:
decorator = trace(sys.stderr)
identity = decorator(identity)
Декораторы с опциональными аргументами
Чтобы декоратор можно было использовать как с аргументами, так и без них, применяют следующий паттерн:
def trace(func=None, *, handle=sys.stdout):
if func is None:
return lambda func: trace(func, handle=handle)
@functools.wraps(func)
def inner(*args, **kwargs):
print(func.__name__, args, kwargs, file=handle)
return func(*args, **kwargs)
return inner
Использование:
@trace
def foo(): ...
@trace(handle=sys.stderr)
def bar(): ...
Зачем только ключевые аргументы? Это предотвращает неоднозначность при вызове и делает код чище.
Практика: полезные декораторы
@timethis — замер времени выполнения
import time
def timethis(func=None, *, n_iter=100):
if func is None:
return lambda func: timethis(func, n_iter=n_iter)
@functools.wraps(func)
def inner(*args, **kwargs):
print(func.__name__, end=" ... ")
acc = float("inf")
for i in range(n_iter):
tick = time.perf_counter()
result = func(*args, **kwargs)
acc = min(acc, time.perf_counter() - tick)
print(acc)
return result
return inner
@once — выполнить не более одного раза
def once(func):
@functools.wraps(func)
def inner(*args, **kwargs):
if not inner.called:
inner.result = func(*args, **kwargs)
inner.called = True
return inner.result
inner.called = False
return inner
@memoized — мемоизация
def memoized(func):
cache = {}
@functools.wraps(func)
def inner(*args, **kwargs):
key = args + tuple(sorted(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
return cache[key]
return inner
Проблема: Словари нехэшируемы. Для универсального решения можно сериализовать аргументы в строку (например, через pickle).
@deprecated — пометить функцию как устаревшую
import warnings
def deprecated(func):
code = func.__code__
warnings.warn_explicit(
f"{func.__name__} is deprecated.",
category=DeprecationWarning,
filename=code.co_filename,
lineno=code.co_firstlineno + 1
)
return func
Контрактное программирование с декораторами
Реализуем простые контракты @pre и @post:
def pre(cond, message):
def decorator(func):
@functools.wraps(func)
def inner(*args, **kwargs):
assert cond(*args, **kwargs), message
return func(*args, **kwargs)
return inner
return decorator
def post(cond, message):
def decorator(func):
@functools.wraps(func)
def inner(*args, **kwargs):
result = func(*args, **kwargs)
assert cond(result), message
return result
return inner
return decorator
Использование:
@pre(lambda x: x >= 0, "negative argument")
@post(lambda r: not math.isnan(r), "result is NaN")
def log_positive(x):
return math.log(x)
Цепочки декораторов
Порядок применения декораторов имеет значение:
@deco1
@deco2
def foo(): ...
Эквивалентно:
foo = deco1(deco2(foo))
Модуль functools
lru_cache — мемоизация с ограничением
@functools.lru_cache(maxsize=128)
def expensive_func(x):
return x * x
maxsize=None— неограниченный кэш (опасно при большом количестве вызовов!).cache_info()— статистика использования кэша.
partial — частичное применение
f = functools.partial(sorted, key=lambda x: x[1])
f([('a', 4), ('b', 2)]) # [('b', 2), ('a', 4)]
singledispatch — обобщённые функции
Позволяет определять разные реализации функции для разных типов:
@functools.singledispatch
def pack(obj):
raise TypeError(f"Unsupported type: {type(obj)}")
@pack.register(int)
def _(obj):
return b"I" + hex(obj).encode("ascii")
@pack.register(list)
def _(obj):
return b"L" + b",".join(map(pack, obj))
reduce — свёртка последовательности
functools.reduce(lambda acc, x: acc * x, [1, 2, 3, 4]) # 24
Хотя reduce популярен в функциональных языках, в Python его используют редко, предпочитая явные циклы для ясности.
Заключение
Декораторы — это не просто «синтаксический сахар», а фундаментальный инструмент метапрограммирования в Python. Они позволяют:
- Модифицировать поведение функций без изменения их кода.
- Реализовывать аспектно-ориентированное программирование.
- Создавать выразительные и легко читаемые API.
Модуль functools предоставляет готовые решения для многих задач, связанных с функциональным программированием и декорированием.
Дополнительные материалы: