Хочу ещё раз на примере декоратора trace пояснить, какие типы декораторов
используются на практике и как они работают.
Общая структура декоратора и пример использования:
def trace(func):
def inner(*args, **kwargs):
print(func.__name__, args, kwargs)
return func(*args, **kwargs)
return inner
@trace
def identity(x):
return xПрименение декоратора trace заменяет имя identity в текущей области видимости
на результат вызова trace c текущим значением identity в качестве аргумента:
identity = trace(identity)Общая структура и пример использования:
def trace(handle):
def decorator(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О применении декоратора с аргументами удобно думать как о процессе из двух
шагов: сначала вычисляем выражение после символа @, чтобы получить декоратор,
затем применяем декоратор к функции:
trace_stderr = trace(sys.stderr)
identity = trace_stderr(identity)Тройная вложенность версии декоратора trace с аргументами несколько
удручает. Но выход есть! Можно обобщить логику декораторов с аргументами
в виде декоратора with_arguments.
def with_arguments(deco):
@functools.wraps(deco)
def wrapper(*dargs, **dkwargs): # 1.
def decorator(func): # 2.
result = deco(func, *dargs, **dkwargs) # 3.
functools.update_wrapper(result, func)
return result # 4.
return decorator
return wrapperТеперь декоратор trace можно переписать так:
@with_arguments
def trace(func, handle):
# Обратите внимание, что вызывать `functools.wraps` не нужно:
# это уже делает за нас декоратор `with_arguments`.
def inner(*args, **kwargs):
print(func.__name__, args, kwargs, file=handle)
return func(*args, **kwargs)
return innerРазберём, как это работает, по шагам:
-
Имя
traceзаменяется на результат применения декоратораwith_argumentsк декораторуtrace:trace = with_arguments(trace)
Таким образом, по имени
traceтеперь доступна функцияwrapper, в которой аргументdecoуказывает на тело декоратораtrace:def wrapper(*dargs, **dkwargs): def decorator(func): result = deco(func, *dargs, **dkwargs) functools.update_wrapper(result, func) return result return decorator
-
Следующий шаг рассмотрим на примере:
@trace(sys.stderr) def identity(x): return x
Как и в предыдщуей версии, сначала вычисляется выражение после символа
@:trace_stderr = trace(sys.stderr)
В результате вызова
traceпо имениtrace_stderrбудет доступна функцияdecorator, в которой аргументdargsзамкнут на значение(sys.stderr, ). -
Полученный декоратор
trace_stderrприменяется к функцииidentity:identity = trace_stderr(identity)
В этот момент вычисляется тело функции
decorator. Напомню, чтоdecoуказывает на тело декоратораtrace, аdargsсодержатsys.stderr. -
В завершении по имени
identityзаписывается значениеresultиз тела функцииdecorator.
Декоратор with_arguments допускает указание ключевых аргументов. Попробуем это
на примере trace:
@with_arguments
def trace(func, handle=sys.stdout):
def inner(*args, **kwargs):
print(func.__name__, args, kwargs, file=handle)
return func(*args, **kwargs)
return innerПолучившийся декоратор trace можно вызывать без аргументов, но при этом
обязательно использовать скобки:
@trace()
def identity(x):
return xВ противном случае имя identity будет указывать на функцию decorator из
тела декоратора with_arguments:
>>> @trace
... def identity(x):
... return x
...
>>> identity
<function __main__.with_arguments.<locals>.wrapper.<locals>.decorator>Уйти от лишних скобок можно с помощью только ключевых аргументов:
def trace(func=None, *, handle=sys.stdout):
# со скобками
if func is None:
def decorator(func):
return trace(func, handle=handle)
return decorator
# без скобок
@functools.wraps(func)
def inner(*args, **kwargs):
print(func.__name__, args, kwargs, file=handle)
return func(*args, **kwargs)
return innerПочему это работает?
-
Когда мы вызываем декоратор без скобок,
funcуказывает на декорируемую функцию, а для аругментаhandleиспользуется значение по умолчанию.@trace def identity(x): return x
-
Для понимания вызова с
handleполезно вспомнить про два шага применения декораторов с аргументами:@trace(handle=sys.stderr) def identity(x): return x
На первом шаге
trace_stderr = trace(handle=sys.stderr). В этом случае срабатывает веткаfunc is Noneи по имениtrace_stderrзаписывается локальная функцияdecorator, которая фиксирует аргументhandleв значениеsys.stderr. На втором шагеidentity = trace_stderr(identity), что эквивалентноidentity = trace(identity, handle=sys.stderr).