Как создать декоратор Python, который можно использовать с параметрами или без них?

88

Я хотел бы создать декоратор Python, который можно было бы использовать с параметрами:

@redirect_output("somewhere.log")
def foo():
    ....

или без них (например, для перенаправления вывода на stderr по умолчанию):

@redirect_output
def foo():
    ....

Это вообще возможно?

Обратите внимание, что я не ищу другого решения проблемы перенаправления вывода, это просто пример синтаксиса, которого я хотел бы достичь.

elifiner
источник
По умолчанию выглядит @redirect_outputочень малоинформативным. Я бы предположил, что это плохая идея. Используйте первую форму и значительно упростите себе жизнь.
S.Lott
интересный вопрос - пока я не увидел его и не просмотрел документацию, я бы предположил, что @f был таким же, как @f (), и я все еще думаю, что это должно быть, если честно (любые предоставленные аргументы будут просто добавлены на аргумент функции)
rog

Ответы:

63

Я знаю, что это старый вопрос, но некоторые комментарии являются новыми, и, хотя все жизнеспособные решения по существу одинаковы, большинство из них не очень чисты и не легко читаются.

Как говорится в ответе Тобе, единственный способ справиться с обоими случаями - это проверить оба сценария. Самый простой способ - просто проверить, есть ли единственный аргумент, и это 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
bj0
источник
31

Использование аргументов ключевого слова со значениями по умолчанию (как предлагает 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():
    ...
хотя
источник
1
Что ты вставляешь your code here? Как вы вызываете декорированную функцию? fn(*args, **kwargs)не работает.
lum
Я думаю, что есть гораздо более простой ответ: создать класс, который будет декоратором с необязательными аргументами. создать другую функцию с теми же аргументами и значениями по умолчанию и вернуть новый экземпляр классов декоратора. должен выглядеть примерно так: def f(a = 5): return MyDecorator( a = a) и class MyDecorator( object ): def __init__( self, a = 5 ): .... извините, что сложно написать это в комментарии, но я надеюсь, что это достаточно просто для понимания
Омер Бен Хаим
17

Я знаю, что это старый вопрос, но мне действительно не нравится ни один из предложенных методов, поэтому я хотел добавить еще один метод. Я видел, что 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, более элегантным и легким для понимания, чем любой другой из предложенных здесь методов.

dgel
источник
Да, мне нравится этот метод. Обратите внимание, что вы должны использовать kwargs при вызове декоратора, иначе первый позиционный аргумент назначается, functionа затем все ломается, потому что декоратор пытается вызвать этот первый позиционный аргумент, как если бы это была ваша украшенная функция.
Дастин Уятт,
12

Вам необходимо обнаружить оба случая, например, используя тип первого аргумента, и, соответственно, вернуть либо оболочку (при использовании без параметра), либо декоратор (при использовании с аргументами).

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"). Запутанно, но факт :-)

Реми Бланк
источник
9

Несколько ответов здесь уже хорошо решают вашу проблему. Что касается стиля, однако, я предпочитаю решать эту затруднительную ситуацию с декоратором, используя 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 .

Удачного кодирования :)

Генриволлас
источник
8

Декоратор 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)
рог
источник
2

Фактически, предостережение в решении @ 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
Аинз Титор
источник
1
Спасибо. Это полезно!
Pragy Agarwal
0

Чтобы дать более полный ответ, чем приведенный выше:

«Есть ли способ создать декоратор, который можно было бы использовать как с аргументами, так и без них?»

Нет, нет универсального способа, потому что в настоящее время в языке Python чего-то не хватает для обнаружения двух разных вариантов использования.

Однако « Да», как уже указывалось в других ответах, таких как bj0s , существует неуклюжий обходной путь, который заключается в проверке типа и значения первого полученного позиционного аргумента (и для проверки того, не имеют ли другие аргументы значения, отличные от значения по умолчанию). Если вам гарантировано, что пользователи никогда не передадут вызываемый объект в качестве первого аргумента вашего декоратора, вы можете использовать этот обходной путь. Обратите внимание, что это то же самое для декораторов классов (замените callable на class в предыдущем предложении).

Чтобы быть уверенным в вышесказанном, я провел немало исследований и даже реализовал библиотеку с именем, decopatchкоторая использует комбинацию всех стратегий, упомянутых выше (и многих других, включая самоанализ) для выполнения «наиболее разумного обходного пути» в зависимости от по вашей потребности.

Но, честно говоря, лучше всего было бы не нуждаться в какой-либо библиотеке здесь и получить эту функцию прямо с языка Python. Если, как и я, вы думаете, что очень жаль, что язык Python на сегодняшний день не может дать точный ответ на этот вопрос, не стесняйтесь поддержать эту идею в программе отслеживания ошибок Python : https: //bugs.python .org / issue36553 !

Большое спасибо за вашу помощь в создании лучшего языка для Python :)

умница
источник
0

Это делает работу без суеты:

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
j4hangir
источник
0

Поскольку об этом никто не упоминал, существует также решение, использующее вызываемый класс, который я считаю более элегантным, особенно в тех случаях, когда декоратор сложен и можно разделить его на несколько методов (функций). Это решение использует __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__

Но если декоратор используется без аргументов, это равно:

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

Вы видите, что в этом случае декорированная функция передается непосредственно конструктору, а вызов __call__полностью опускается. Вот почему нам нужно использовать логику, чтобы позаботиться об этом случае в __new__магическом методе.

Почему нельзя использовать __init__вместо __new__? Причина проста: python запрещает возвращать любые другие значения, кроме None из__init__

ПРЕДУПРЕЖДЕНИЕ

У этого подхода есть один побочный эффект. Он не сохранит подпись функции!

Majo
источник
-1

Вы пробовали аргументы ключевых слов со значениями по умолчанию? Что-то типа

def decorate_something(foo=bar, baz=quux):
    pass
Kquinn
источник
-2

Обычно в Python можно указать аргументы по умолчанию ...

def redirect_output(fn, output = stderr):
    # whatever

Не уверен, что это работает и с декораторами. Я не знаю ни одной причины, почему бы этого не произошло.

Дэвид З
источник
2
Если вы скажете @dec (abc), функция не будет передана напрямую в dec. dec (abc) что-то возвращает, и это возвращаемое значение используется как декоратор. Итак, dec (abc) должен возвращать функцию, которая затем получает декорированную функцию, переданную в качестве параметра. (См. Также код thobes)
чт,
-2

Основываясь на ответе 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()примере в вопросе.
ehabkost