Подтвердить что ты не робот

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

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

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

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

@redirect_output
def foo():
    ....

Возможно ли это?

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

4b9b3361

Ответ 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...