Хочу ещё раз на примере декоратора 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)
.