Как создать декоратор Python, который можно использовать как с параметрами, так и без них?
Вопрос
Я хотел бы создать декоратор Python, который можно использовать либо с параметрами:
@redirect_output("somewhere.log")
def foo():
....
или без них (например, чтобы перенаправить вывод на stderr по умолчанию):
@redirect_output
def foo():
....
Это вообще возможно?
Обратите внимание, что я не ищу другое решение проблемы перенаправления вывода, это просто пример синтаксиса, которого я хотел бы достичь.
Решение
Я знаю, что этот вопрос старый, но некоторые комментарии являются новыми, и хотя все жизнеспособные решения по сути одинаковы, большинство из них не очень чисты или их легко прочитать.
Как и в ответе Тобе, единственный способ справиться с обоими случаями - проверить оба сценария. Самый простой способ - просто проверить, существует ли один аргумент и является ли он callabe (ПРИМЕЧАНИЕ: дополнительные проверки будут необходимы, если ваш декоратор принимает только 1 аргумент и это может быть вызываемый объект):
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's 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
Другие советы
Использование аргументов ключевого слова со значениями по умолчанию (как предложено 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():
...
Необходимо обнаружить оба случая, например, используя тип первого аргумента, и соответственно вернуть либо оболочку (при использовании без параметров), либо декоратор (при использовании с аргументами).
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
он вызывается напрямую с функцией, которая должна быть оформлена в качестве аргумента.
Или другими словами: за синтаксисом @
должно следовать выражение, результатом которого является функция, принимающая функцию, которая должна быть оформлена в качестве единственного аргумента, и возвращающая декорированную функцию. Само выражение может быть вызовом функции, как в случае с <=>. Запутанно, но верно: -)
Декоратор 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)
Я знаю, что это старый вопрос, но мне действительно не нравится ни один из предложенных методов, поэтому я хотел добавить еще один метод.Я видел, что django использует действительно чистый метод в своих login_required
декоратор в django.contrib.auth.decorators
.Как вы можете видеть в документы декоратора, его можно использовать отдельно как @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, более элегантен и прост для понимания, чем любой другой метод, предложенный здесь.
Несколько ответов здесь уже хорошо решают вашу проблему. Однако, что касается стиля, я предпочитаю решить эту проблему с использованием functools.partial
, как это было предложено в Python Cookbook 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
.
Что касается вторичной цели миссии, перенаправление вывода функции рассматривается в этом Сообщение переполнения стека . Р> <Ч>
Если вы хотите погрузиться глубже, ознакомьтесь с главой 9 (Метапрограммирование) в Python Cookbook 3 , которая свободно доступна по адресу для чтения в Интернете .
Некоторые из этих материалов демонстрируются в прямом эфире (и даже больше!) в потрясающем видео Beazley на YouTube Метапрограммирование Python 3 . р>
Удачного кодирования:)
Фактически, случай предостережения в решении @ 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
Вы пробовали аргументы ключевых слов со значениями по умолчанию? Что-то вроде
def decorate_something(foo=bar, baz=quux):
pass
Как правило, вы можете задать аргументы по умолчанию в Python ...
def redirect_output(fn, output = stderr):
# whatever
Не уверен, что это работает и с декораторами. Я не знаю ни одной причины, почему это не так.
Опираясь на ответ 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...