Я хотел бы создать декоратор Python, который можно было бы использовать с параметрами:
@redirect_output("somewhere.log")
def foo():
....
или без них (например, для перенаправления вывода на stderr по умолчанию):
@redirect_output
def foo():
....
Это вообще возможно?
Обратите внимание, что я не ищу другого решения проблемы перенаправления вывода, это просто пример синтаксиса, которого я хотел бы достичь.
@redirect_output
очень малоинформативным. Я бы предположил, что это плохая идея. Используйте первую форму и значительно упростите себе жизнь.Ответы:
Я знаю, что это старый вопрос, но некоторые комментарии являются новыми, и, хотя все жизнеспособные решения по существу одинаковы, большинство из них не очень чисты и не легко читаются.
Как говорится в ответе Тобе, единственный способ справиться с обоими случаями - это проверить оба сценария. Самый простой способ - просто проверить, есть ли единственный аргумент, и это 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(): ...
источник
your code here
? Как вы вызываете декорированную функцию?fn(*args, **kwargs)
не работает.def f(a = 5): return MyDecorator( a = a)
иclass MyDecorator( object ): def __init__( self, a = 5 ): ....
извините, что сложно написать это в комментарии, но я надеюсь, что это достаточно просто для пониманияЯ знаю, что это старый вопрос, но мне действительно не нравится ни один из предложенных методов, поэтому я хотел добавить еще один метод. Я видел, что 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, более элегантным и легким для понимания, чем любой другой из предложенных здесь методов.
источник
function
а затем все ломается, потому что декоратор пытается вызвать этот первый позиционный аргумент, как если бы это была ваша украшенная функция.Вам необходимо обнаружить оба случая, например, используя тип первого аргумента, и, соответственно, вернуть либо оболочку (при использовании без параметра), либо декоратор (при использовании с аргументами).
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"
и должен возвращать декоратор, принимающий функцию, которая должна быть оформлена в качестве аргумента. При использовании as@redirect_output
он вызывается непосредственно с функцией, которую нужно оформить в качестве аргумента.Или другими словами: за
@
синтаксисом должно следовать выражение, результатом которого является функция, принимающая функцию, которая должна быть оформлена в качестве единственного аргумента, и возвращающая оформленную функцию. Само выражение может быть вызовом функции, как в случае с@redirect_output("output.log")
. Запутанно, но факт :-)источник
Несколько ответов здесь уже хорошо решают вашу проблему. Что касается стиля, однако, я предпочитаю решать эту затруднительную ситуацию с декоратором, используя
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
.Что касается вторичной цели миссии, перенаправление вывода функции рассматривается в этом сообщении о переполнении стека .
Если вы хотите погрузиться глубже, ознакомьтесь с главой 9 (Метапрограммирование) в Python Cookbook 3 , которая находится в свободном доступе для чтения в Интернете .
Некоторые из этих материалов демонстрируются вживую (и многое другое!) В потрясающем видео Бизли на YouTube Python 3 Metaprogramming .
Удачного кодирования :)
источник
Декоратор 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)
источник
Фактически, предостережение в решении @ 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
источник
Чтобы дать более полный ответ, чем приведенный выше:
Нет, нет универсального способа, потому что в настоящее время в языке Python чего-то не хватает для обнаружения двух разных вариантов использования.
Однако « Да», как уже указывалось в других ответах, таких как
bj0
s , существует неуклюжий обходной путь, который заключается в проверке типа и значения первого полученного позиционного аргумента (и для проверки того, не имеют ли другие аргументы значения, отличные от значения по умолчанию). Если вам гарантировано, что пользователи никогда не передадут вызываемый объект в качестве первого аргумента вашего декоратора, вы можете использовать этот обходной путь. Обратите внимание, что это то же самое для декораторов классов (замените callable на class в предыдущем предложении).Чтобы быть уверенным в вышесказанном, я провел немало исследований и даже реализовал библиотеку с именем,
decopatch
которая использует комбинацию всех стратегий, упомянутых выше (и многих других, включая самоанализ) для выполнения «наиболее разумного обходного пути» в зависимости от по вашей потребности.Но, честно говоря, лучше всего было бы не нуждаться в какой-либо библиотеке здесь и получить эту функцию прямо с языка Python. Если, как и я, вы думаете, что очень жаль, что язык Python на сегодняшний день не может дать точный ответ на этот вопрос, не стесняйтесь поддержать эту идею в программе отслеживания ошибок Python : https: //bugs.python .org / issue36553 !
Большое спасибо за вашу помощь в создании лучшего языка для Python :)
источник
Это делает работу без суеты:
from functools import wraps def memoize(fn=None, hours=48.0): def deco(fn): @wraps(fn) def wrapper(*args, **kwargs): return fn(*args, **kwargs) return wrapper if callable(fn): return deco(fn) return deco
источник
Поскольку об этом никто не упоминал, существует также решение, использующее вызываемый класс, который я считаю более элегантным, особенно в тех случаях, когда декоратор сложен и можно разделить его на несколько методов (функций). Это решение использует
__new__
магический метод, чтобы делать то, что указывали другие. Сначала определите, как использовался декоратор, а затем соответствующим образом настройте return.class decorator_with_arguments(object): def __new__(cls, decorated_function=None, **kwargs): self = super().__new__(cls) self._init(**kwargs) if not decorated_function: return self else: return self.__call__(decorated_function) def _init(self, arg1="default", arg2="default", arg3="default"): self.arg1 = arg1 self.arg2 = arg2 self.arg3 = arg3 def __call__(self, decorated_function): def wrapped_f(*args): print("Decorator arguments:", self.arg1, self.arg2, self.arg3) print("decorated_function arguments:", *args) decorated_function(*args) return wrapped_f @decorator_with_arguments(arg1=5) def sayHello(a1, a2, a3, a4): print('sayHello arguments:', a1, a2, a3, a4) @decorator_with_arguments() def sayHello(a1, a2, a3, a4): print('sayHello arguments:', a1, a2, a3, a4) @decorator_with_arguments def sayHello(a1, a2, a3, a4): print('sayHello arguments:', a1, a2, a3, a4)
Если декоратор используется с аргументами, это равно:
result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)
Видно, что аргументы
arg1
передаются конструктору правильно, а декорированная функция передается в__call__
Но если декоратор используется без аргументов, это равно:
Вы видите, что в этом случае декорированная функция передается непосредственно конструктору, а вызов
__call__
полностью опускается. Вот почему нам нужно использовать логику, чтобы позаботиться об этом случае в__new__
магическом методе.Почему нельзя использовать
__init__
вместо__new__
? Причина проста: python запрещает возвращать любые другие значения, кроме None из__init__
ПРЕДУПРЕЖДЕНИЕ
У этого подхода есть один побочный эффект. Он не сохранит подпись функции!
источник
Вы пробовали аргументы ключевых слов со значениями по умолчанию? Что-то типа
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...
источник
@redirect_output("somewhere.log") def foo()
примере в вопросе.