Як створити декоратор Python, який можна використовувати як з параметрами, так і без них?


89

Я хотів би створити декоратор Python, який можна використовувати з параметрами:

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

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

@redirect_output
def foo():
    ....

Це взагалі можливо?

Зауважте, що я не шукаю іншого рішення проблеми перенаправлення виводу, це лише приклад синтаксису, якого я хотів би досягти.


Типовий вигляд @redirect_outputнадзвичайно малоінформативний. Я вважаю, що це погана ідея. Використовуйте першу форму і значно спростіть своє життя.
S.Lott,

цікаве питання, хоча - поки я не побачив його і не переглянув документацію, я вважав, що @f було таким самим, як @f (), і я все ще думаю, що це має бути, чесно кажучи (будь-які надані аргументи будуть просто враховані на аргумент функції)
rog

Відповіді:


64

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

Як сказано у відповіді thobe, єдиним способом вирішити обидва випадки є перевірка обох сценаріїв. Найпростіший спосіб - це просто перевірити, чи є один аргумент, і це 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

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, є більш елегантним та легшим для розуміння, ніж будь-яка інша техніка, запропонована тут.


Так, мені подобається цей метод. Зверніть увагу, що ви повинні використовувати 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", і він повинен повернути декоратор, що приймає функцію, яка буде оформлена як аргумент. При використанні як @redirect_output, він викликається безпосередньо з функцією, яка буде оформлена як аргумент.

Або іншими словами: @ синтаксис повинен супроводжуватися виразом, результатом якого є функція, яка приймає функцію, яка повинна бути оформлена як єдиний аргумент, і повертає декоровану функцію. Сам вираз може бути викликом функції, що і має місце у випадку @redirect_output("output.log"). Заплутаний, але правдивий :-)


9

Кілька відповідей тут вже добре вирішують вашу проблему. Що стосується стилю, я вважаю за краще вирішувати цю проблему декоратора, використовуючи 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 , який є у вільному доступі для читання в Інтернеті .

Частина цього матеріалу демонструється в прямому ефірі (плюс більше!) У дивовижному відео Бізлі на YouTube Python 3 Metaprogramming на YouTube .

Щасливого кодування :)


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 , існує незграбний обхідний шлях, який полягає у тому, щоб перевірити тип і значення першого позиційного аргументу (і перевірити, чи жоден інший аргумент не має значення за замовчуванням). Якщо вам гарантовано, що користувачі ніколи цього не зроблять передадуть виклик як перший аргумент вашого декоратора, тоді ви можете скористатися цим обхідним шляхом. Зверніть увагу, що це те саме для декораторів класів (замінити викликається на клас у попередньому реченні).

Щоб бути впевненим у вищесказаному, я провів там чимало досліджень і навіть запровадив бібліотеку з назвою, 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

0

Оскільки про це ніхто не згадував, існує також рішення, яке використовує викличний клас, який я вважаю більш елегантним, особливо у випадках, коли декоратор складний, і, можливо, ви хочете розділити його на кілька методів (функцій). Це рішення використовує __new__магічний метод, щоб зробити по суті те, на що вказували інші. Спочатку виявіть, як використовувався декоратор, ніж відрегулюйте належним чином повернення.

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__

УВАГА

Цей приклад має один побічний ефект. Це не збереже підпис функції!


-1

Ви пробували аргументи ключових слів із значеннями за замовчуванням? Щось на зразок

def decorate_something(foo=bar, baz=quux):
    pass

-2

Як правило, ви можете наводити аргументи за замовчуванням у Python ...

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

Не впевнений, що це працює і з декораторами. Я не знаю жодної причини, чому б цього не сталося.


2
Якщо ви кажете @dec (abc), функція не передається безпосередньо dec. dec (abc) щось повертає, і це повертане значення використовується як декоратор. Отже dec (abc) повинен повернути функцію, яка потім отримує оформлену функцію, передану як параметр. (Див. Також код Тобе)
sth

-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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.