Как создать декоратор Python, который можно использовать как с параметрами, так и без них?
Я хотел бы создать декоратор Python, который можно использовать либо с параметрами:
@redirect_output("somewhere.log")
def foo():
....
или без них (например, для перенаправления вывода на stderr по умолчанию):
@redirect_output
def foo():
....
Возможно ли это?
Обратите внимание, что я не ищу другого решения проблемы перенаправления вывода, это просто пример синтаксиса, который я хотел бы достичь.
Ответы
Ответ 1
Я знаю, что этот вопрос старый, но некоторые из комментариев новы, и хотя все жизнеспособные решения по существу одинаковы, большинство из них не очень чисты или легко читаются.
Как говорится в ответах на тему ответа, единственный способ справиться с обоими случаями - проверить оба сценария. Самый простой способ - просто проверить, есть ли один аргумент, и это callabe (ПРИМЕЧАНИЕ: дополнительные проверки потребуются, если ваш декоратор принимает только один аргумент, и это может быть вызываемый объект):
def decorator(*args, **kwargs):
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
# called as @decorator
else:
# called as @decorator(*args, **kwargs)
В первом случае вы делаете то, что делает любой нормальный декоратор, возвращают измененную или завернутую версию переданной функции.
Во втором случае вы возвращаете "новый" декоратор, который каким-то образом использует информацию, переданную с помощью * args, ** kwargs.
Это хорошо и все, но писать все для каждого декоратора, который вы делаете, может быть довольно раздражающим и не таким чистым. Вместо этого было бы неплохо иметь возможность автоматически изменять наши декораторы, не переписывая их... но для чего нужны декораторы!
Используя следующий декоратор-декоратор, мы можем осветить наши декораторы, чтобы они могли использоваться с аргументами или без них:
def doublewrap(f):
'''
a decorator decorator, allowing the decorator to be used as:
@decorator(with, arguments, and=kwargs)
or
@decorator
'''
@wraps(f)
def new_dec(*args, **kwargs):
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
# actual decorated function
return f(args[0])
else:
# decorator arguments
return lambda realf: f(realf, *args, **kwargs)
return new_dec
Теперь мы можем украсить наши декораторы с помощью @doublewrap, и они будут работать с аргументами и без них с одной оговоркой:
Я отметил выше, но должен повторить здесь, проверка этого декоратора делает предположение о аргументах, которые может получить декоратор (а именно, что он не может получить один аргумент, вызываемый). Поскольку мы применяем его применительно к любому генератору, его нужно помнить или изменять, если это будет противоречить.
Ниже показано его использование:
def test_doublewrap():
from util import doublewrap
from functools import wraps
@doublewrap
def mult(f, factor=2):
'''multiply a function return value'''
@wraps(f)
def wrap(*args, **kwargs):
return factor*f(*args,**kwargs)
return wrap
# try normal
@mult
def f(x, y):
return x + y
# try args
@mult(3)
def f2(x, y):
return x*y
# try kwargs
@mult(factor=5)
def f3(x, y):
return x - y
assert f(2,3) == 10
assert f2(2,5) == 30
assert f3(8,1) == 5*7
Ответ 2
Использование аргументов ключевых слов со значениями по умолчанию (как предложено kquinn) - хорошая идея, но вам потребуется включить скобки:
@redirect_output()
def foo():
...
Если вам нужна версия, которая работает без круглых скобок на декораторе, вам придется учитывать оба сценария в коде вашего декоратора.
Если вы использовали Python 3.0, для этого можно использовать только аргументы ключевого слова:
def redirect_output(fn=None,*,destination=None):
destination = sys.stderr if destination is None else destination
def wrapper(*args, **kwargs):
... # your code here
if fn is None:
def decorator(fn):
return functools.update_wrapper(wrapper, fn)
return decorator
else:
return functools.update_wrapper(wrapper, fn)
В Python 2.x это можно эмулировать с помощью трюков varargs:
def redirected_output(*fn,**options):
destination = options.pop('destination', sys.stderr)
if options:
raise TypeError("unsupported keyword arguments: %s" %
",".join(options.keys()))
def wrapper(*args, **kwargs):
... # your code here
if fn:
return functools.update_wrapper(wrapper, fn[0])
else:
def decorator(fn):
return functools.update_wrapper(wrapper, fn)
return decorator
Любая из этих версий позволит вам написать такой код:
@redirected_output
def foo():
...
@redirected_output(destination="somewhere.log")
def bar():
...
Ответ 3
Вам нужно определить оба случая, например, используя тип первого аргумента и соответственно вернуть либо оболочку (при использовании без параметра), либо декоратор (при использовании с аргументами).
from functools import wraps
import inspect
def redirect_output(fn_or_output):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **args):
# Redirect output
try:
return fn(*args, **args)
finally:
# Restore output
return wrapper
if inspect.isfunction(fn_or_output):
# Called with no parameter
return decorator(fn_or_output)
else:
# Called with a parameter
return decorator
При использовании синтаксиса @redirect_output("output.log")
, redirect_output
вызывается с единственным аргументом "output.log"
, и он должен возвращать декоратор, принимающий функцию, которая будет украшена в качестве аргумента. При использовании в качестве @redirect_output
он вызывается непосредственно с функцией, которая должна быть украшена в качестве аргумента.
Или, другими словами: синтаксис @
должен сопровождаться выражением, результатом которого является функция, принимающая функцию, которая должна быть украшена в качестве единственного аргумента, и возвращает украшенную функцию. Само выражение может быть вызовом функции, что имеет место с @redirect_output("output.log")
. Convoluted, но верно: -)
Ответ 4
Декоратор python вызывается принципиально другим способом в зависимости от того, передаете ли вы его аргументы или нет. Украшение на самом деле является просто выражением (синтаксически ограниченным).
В первом примере:
@redirect_output("somewhere.log")
def foo():
....
функция redirect_output
вызывается с
данный аргумент, который, как ожидается, вернет декоратора
функция, которая сама по себе называется foo
в качестве аргумента,
который (наконец!) должен вернуть окончательную оформленную функцию.
Эквивалентный код выглядит следующим образом:
def foo():
....
d = redirect_output("somewhere.log")
foo = d(foo)
Эквивалентный код для вашего второго примера выглядит так:
def foo():
....
d = redirect_output
foo = d(foo)
Итак, вы можете делать все, что хотите, но не полностью:
import types
def redirect_output(arg):
def decorator(file, f):
def df(*args, **kwargs):
print 'redirecting to ', file
return f(*args, **kwargs)
return df
if type(arg) is types.FunctionType:
return decorator(sys.stderr, arg)
return lambda f: decorator(arg, f)
Это должно быть хорошо, если вы не хотите использовать функцию как
аргумент вашему декоратору, и в этом случае декоратор
ошибочно предположим, что у него нет аргументов. Он также потерпит неудачу
если это украшение применяется к другому украшению, которое
не возвращает тип функции.
Альтернативный метод - просто потребовать, чтобы
функция декоратора всегда называется, даже если она не имеет аргументов.
В этом случае ваш второй пример будет выглядеть следующим образом:
@redirect_output()
def foo():
....
Код функции декоратора будет выглядеть следующим образом:
def redirect_output(file = sys.stderr):
def decorator(file, f):
def df(*args, **kwargs):
print 'redirecting to ', file
return f(*args, **kwargs)
return df
return lambda f: decorator(file, f)
Ответ 5
Я знаю, что это старый вопрос, но мне действительно не нравятся предложенные методы, поэтому я хотел добавить другой метод. Я видел, что django использует действительно чистый метод в login_required
decorator в django.contrib.auth.decorators
. Как вы можете видеть в decorator docs, его можно использовать как @login_required
или с аргументами @login_required(redirect_field_name='my_redirect_field')
.
То, как они это делают, довольно просто. Они добавляют kwarg
(function=None
) перед их аргументами декоратора. Если декоратор используется один, function
будет фактической функцией, которую он украшает, тогда как если он вызывается с аргументами, function
будет None
.
Пример:
from functools import wraps
def custom_decorator(function=None, some_arg=None, some_other_arg=None):
def actual_decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
# Do stuff with args here...
if some_arg:
print(some_arg)
if some_other_arg:
print(some_other_arg)
return f(*args, **kwargs)
return wrapper
if function:
return actual_decorator(function)
return actual_decorator
@custom_decorator
def test1():
print('test1')
>>> test1()
test1
@custom_decorator(some_arg='hello')
def test2():
print('test2')
>>> test2()
hello
test2
@custom_decorator(some_arg='hello', some_other_arg='world')
def test3():
print('test3')
>>> test3()
hello
world
test3
Я нахожу этот подход, который django использует, чтобы быть более изящным и понятным, чем любой другой предлагаемый здесь метод.
Ответ 6
Несколько ответов здесь уже хорошо затрагивают вашу проблему. Однако в отношении стиля я предпочитаю решать это затруднительное положение декоратора с помощью functools.partial
, как это предложено в книге Дэвида Бейзли Python поваренной книги 3:
from functools import partial, wraps
def decorator(func=None, foo='spam'):
if func is None:
return partial(decorator, foo=foo)
@wraps(func)
def wrapper(*args, **kwargs):
# do something with `func` and `foo`, if you're so inclined
pass
return wrapper
Пока да, вы можете просто сделать
@decorator()
def f(*args, **kwargs):
pass
без фанковых обходных решений, я нахожу его странным, и мне нравится иметь возможность просто украшать с помощью @decorator
.
Что касается цели вторичной миссии, перенаправление выхода функции адресуется в этом post overflow post.
Если вы хотите погрузиться глубже, ознакомьтесь с Главой 9 (Метапрограммирование) в Python Cookbook 3, которая свободно доступна для читать онлайн.
Некоторые из этих материалов воспроизводятся в реальном времени (плюс больше!) в Beazley. Великолепное видео YouTube Python 3 Metaprogramming.
Счастливое кодирование:)
Ответ 7
Фактически, случай оговорки в решении @bj0 можно легко проверить:
def meta_wrap(decor):
@functools.wraps(decor)
def new_decor(*args, **kwargs):
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
# this is the double-decorated f.
# Its first argument should not be a callable
doubled_f = decor(args[0])
@functools.wraps(doubled_f)
def checked_doubled_f(*f_args, **f_kwargs):
if callable(f_args[0]):
raise ValueError('meta_wrap failure: '
'first positional argument cannot be callable.')
return doubled_f(*f_args, **f_kwargs)
return checked_doubled_f
else:
# decorator arguments
return lambda real_f: decor(real_f, *args, **kwargs)
return new_decor
Вот несколько тестовых примеров для этой отказобезопасной версии meta_wrap
.
@meta_wrap
def baddecor(f, caller=lambda x: -1*x):
@functools.wraps(f)
def _f(*args, **kwargs):
return caller(f(args[0]))
return _f
@baddecor # used without arg: no problem
def f_call1(x):
return x + 1
assert f_call1(5) == -6
@baddecor(lambda x : 2*x) # bad case
def f_call2(x):
return x + 1
f_call2(5) # raises ValueError
# explicit keyword: no problem
@baddecor(caller=lambda x : 100*x)
def f_call3(x):
return x + 1
assert f_call3(5) == 600
Ответ 8
Чтобы дать более полный ответ, чем приведенный выше:
"Есть ли способ создать декоратор, который можно использовать как с аргументами, так и без них?"
Нет, нет никакого общего способа, потому что в настоящее время в языке Python чего-то не хватает для обнаружения двух разных вариантов использования.
Однако Да, как уже указывалось в других ответах, таких как bj0
s, существует неуклюжий обходной путь, который проверяет тип и значение первого получен позиционный аргумент (и проверить, нет ли других аргументов значения не по умолчанию). Если вам гарантировано, что пользователи никогда не передадут вызываемый элемент в качестве первого аргумента вашего декоратора, то вы можете использовать этот обходной путь. Обратите внимание, что это то же самое для декораторов классов (замените вызываемое классом в предыдущем предложении).
Чтобы быть уверенным в вышесказанном, я провел немало исследований и даже реализовал библиотеку с именем decopatch
, которая использует комбинацию всех стратегий, упомянутых выше (и многие другие, включая самоанализ) для выполнения "какой бы самый умный обходной путь" в зависимости от ваших потребностей.
Но, честно говоря, лучше всего было бы не нуждаться в какой-либо библиотеке здесь и получить эту функцию прямо из языка Python. Если, как и я, вы считаете, что жаль, что язык python на сегодняшний день не способен дать четкий ответ на этот вопрос, не стесняйтесь поддержать эту идею в багтрекере python: https://bugs.python.org/issue36553 !
Большое спасибо за вашу помощь в улучшении языка Python :)
Ответ 9
Вы пытались использовать аргументы ключевых слов со значениями по умолчанию? Что-то вроде
def decorate_something(foo=bar, baz=quux):
pass
Ответ 10
Как правило, вы можете указать аргументы по умолчанию в Python...
def redirect_output(fn, output = stderr):
# whatever
Не уверен, что это работает и с декораторами. Я не знаю ни одной причины, почему бы и нет.
Ответ 11
Основываясь на ответе vartec:
imports sys
def redirect_output(func, output=None):
if output is None:
output = sys.stderr
if isinstance(output, basestring):
output = open(output, 'w') # etc...
# everything else...