Декораторы с параметрами?


401

У меня проблема с передачей переменной 'insurance_mode' декоратором. Я сделал бы это следующим оператором декоратора:

 @execute_complete_reservation(True)
 def test_booking_gta_object(self):
     self.test_select_gta_object()

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

def execute_complete_reservation(test_case,insurance_mode):
    def inner_function(self,*args,**kwargs):
        self.test_create_qsf_query()
        test_case(self,*args,**kwargs)
        self.test_select_room_option()
        if insurance_mode:
            self.test_accept_insurance_crosseling()
        else:
            self.test_decline_insurance_crosseling()
        self.test_configure_pax_details()
        self.test_configure_payer_details

    return inner_function

3
Ваш пример не синтаксически действителен. execute_complete_reservationпринимает два параметра, но вы передаете его один. Декораторы - это просто синтаксический сахар для обёртывания функций внутри других функций. См. Docs.python.org/reference/compound_stmts.html#function для полной документации.
Брайан Клэппер

Ответы:


687

Синтаксис декораторов с аргументами немного отличается - декоратор с аргументами должен возвращать функцию, которая будет принимать функцию и возвращать другую функцию. Так что действительно должен вернуть нормальный декоратор. Немного смущает, верно? Я имею в виду:

def decorator_factory(argument):
    def decorator(function):
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            result = function(*args, **kwargs)
            more_funny_stuff()
            return result
        return wrapper
    return decorator

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


56
Интересно, почему GVR не реализовал это, передав параметры в качестве последующих аргументов декоратора после 'function'. «Эй, черт возьми, я слышал, что вы как закрытие ...» и так далее.
Мишель Мюллер

3
> Будет ли функция первым или последним аргументом? Очевидно, во-первых, так как параметры это список параметров переменной длины. > Также странно, что вы «вызываете» функцию с сигнатурой, отличной от той, что в определении. Как вы указали, на самом деле это вполне соответствовало бы - это в значительной степени аналогично тому, как вызывается метод класса. Чтобы было понятнее, у вас может быть что-то вроде декоратора (self_func, param1, ...). Но обратите внимание: я не сторонник каких-либо изменений здесь, Python слишком далек от этого, и мы можем видеть, как сработали переломные изменения ...
Мишель Мюллер

21
Вы забыли ОЧЕНЬ ПОЛЕЗНЫЕ functools.wraps для украшения обертки :)
socketpair

10
Вы забыли о возврате при вызове функции, то есть return function(*args, **kwargs)
formiaczek

36
Может быть, очевидно, но на всякий случай: вам нужно использовать этот декоратор как, @decorator()а не только @decorator, даже если у вас есть только необязательные аргументы.
Патрик Мевзек

327

Изменить : для глубокого понимания ментальной модели декораторов, взгляните на этот удивительный Pycon Talk. стоит 30 минут.

Один из способов думать об декораторах с аргументами

@decorator
def foo(*args, **kwargs):
    pass

переводит на

foo = decorator(foo)

Так что, если у декоратора были аргументы,

@decorator_with_args(arg)
def foo(*args, **kwargs):
    pass

переводит на

foo = decorator_with_args(arg)(foo)

decorator_with_args является функцией, которая принимает пользовательский аргумент и возвращает фактический декоратор (который будет применен к декорированной функции).

Я использую простой трюк с частями, чтобы сделать мои декораторы легкими

from functools import partial

def _pseudo_decor(fun, argument):
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def foo(*args, **kwargs):
    pass

Обновить:

Выше fooстановитсяreal_decorator(foo)

Одним из эффектов декорирования функции является то, что имя fooпереопределяется при объявлении декоратора. foo«отменяется» тем, что возвращается real_decorator. В этом случае новая функция объекта.

Все fooметаданные переопределяются, в частности, строка документации и имя функции.

>>> print(foo)
<function _pseudo_decor.<locals>.ret_fun at 0x10666a2f0>

functools.wraps предоставляет нам удобный метод для «поднятия» строки документации и имени возвращаемой функции.

from functools import partial, wraps

def _pseudo_decor(fun, argument):
    # magic sauce to lift the name and doc of the function
    @wraps(fun)
    def ret_fun(*args, **kwargs):
        #do stuff here, for eg.
        print ("decorator arg is %s" % str(argument))
        return fun(*args, **kwargs)
    return ret_fun

real_decorator = partial(_pseudo_decor, argument=arg)

@real_decorator
def bar(*args, **kwargs):
    pass

>>> print(bar)
<function __main__.bar(*args, **kwargs)>

4
Ваш ответ прекрасно объяснил присущую декоратору ортогональность, спасибо
zsf222

Не могли бы вы добавить @functools.wraps?
Mr_and_Mrs_D

1
@Mr_and_Mrs_D, я обновил пост с примером с functool.wraps. Добавление этого в пример может еще больше запутать читателей.
SRJ

7
Что argздесь?
отображаемое имя

1
Как вы передадите аргумент, переданный barаргументу real_decorator?
Чанг Чжао

85

Я хотел бы показать идею, которая является ИМХО довольно элегантной. Решение, предложенное t.dubrownik, показывает шаблон, который всегда один и тот же: вам нужна трехслойная оболочка независимо от того, что делает декоратор.

Поэтому я подумал, что это работа для мета-декоратора, то есть декоратора для декораторов. Поскольку декоратор является функцией, он фактически работает как обычный декоратор с аргументами:

def parametrized(dec):
    def layer(*args, **kwargs):
        def repl(f):
            return dec(f, *args, **kwargs)
        return repl
    return layer

Это может быть применено к обычному декоратору для добавления параметров. Так, например, скажем, у нас есть декоратор, который удваивает результат функции:

def double(f):
    def aux(*xs, **kws):
        return 2 * f(*xs, **kws)
    return aux

@double
def function(a):
    return 10 + a

print function(3)    # Prints 26, namely 2 * (10 + 3)

С помощью @parametrizedмы можем построить общий @multiplyдекоратор, имеющий параметр

@parametrized
def multiply(f, n):
    def aux(*xs, **kws):
        return n * f(*xs, **kws)
    return aux

@multiply(2)
def function(a):
    return 10 + a

print function(3)    # Prints 26

@multiply(3)
def function_again(a):
    return 10 + a

print function(3)          # Keeps printing 26
print function_again(3)    # Prints 39, namely 3 * (10 + 3)

Традиционно первым параметром параметризованного декоратора является функция, в то время как остальные аргументы будут соответствовать параметру параметризованного декоратора.

Интересным примером использования может быть тип-безопасный ассертивный декоратор:

import itertools as it

@parametrized
def types(f, *types):
    def rep(*args):
        for a, t, n in zip(args, types, it.count()):
            if type(a) is not t:
                raise TypeError('Value %d has not type %s. %s instead' %
                    (n, t, type(a))
                )
        return f(*args)
    return rep

@types(str, int)  # arg1 is str, arg2 is int
def string_multiply(text, times):
    return text * times

print(string_multiply('hello', 3))    # Prints hellohellohello
print(string_multiply(3, 3))          # Fails miserably with TypeError

И последнее замечание: здесь я не использую functools.wrapsфункции-оболочки, но я бы рекомендовал использовать его все время.


3
Не использовал это точно, но помог мне разобраться в концепции :) Спасибо!
Мукатрон

Я попробовал это, и у меня были некоторые проблемы .
Джефф

@ Джефф, не могли бы вы рассказать нам о своих проблемах?
Дакав

У меня это было связано по моему вопросу, и я понял это ... Мне нужно было позвонить @wrapsпо моему конкретному делу.
Джефф

4
О, мальчик, я потерял целый день на этом. К счастью, я нашел этот ответ (который, кстати, мог быть лучшим ответом, когда-либо созданным во всем Интернете). Они тоже используют твой @parametrizedтрюк. Проблема, с которой я столкнулся, заключалась в том, что я забыл, что @синтаксис равен фактическим вызовам (почему-то я знал это и не знал этого одновременно, как вы можете понять из моего вопроса). Поэтому, если вы хотите перевести @синтаксис в обычные вызовы, чтобы проверить, как он работает, вам лучше сначала временно закомментировать его, иначе вам придется дважды вызывать его и получать результаты
mumbojumbo

79

Вот слегка измененная версия ответа t.dubrownik . Почему?

  1. Как общий шаблон, вы должны вернуть возвращаемое значение из исходной функции.
  2. Это меняет имя функции, что может повлиять на другие декораторы / код.

Так что используйте @functools.wraps():

from functools import wraps

def decorator(argument):
    def real_decorator(function):
        @wraps(function)
        def wrapper(*args, **kwargs):
            funny_stuff()
            something_with_argument(argument)
            retval = function(*args, **kwargs)
            more_funny_stuff()
            return retval
        return wrapper
    return real_decorator

37

Я предполагаю, что ваша проблема заключается в передаче аргументов вашему декоратору. Это немного сложно и не просто.

Вот пример того, как это сделать:

class MyDec(object):
    def __init__(self,flag):
        self.flag = flag
    def __call__(self, original_func):
        decorator_self = self
        def wrappee( *args, **kwargs):
            print 'in decorator before wrapee with flag ',decorator_self.flag
            original_func(*args,**kwargs)
            print 'in decorator after wrapee with flag ',decorator_self.flag
        return wrappee

@MyDec('foo de fa fa')
def bar(a,b,c):
    print 'in bar',a,b,c

bar('x','y','z')

Печать:

in decorator before wrapee with flag  foo de fa fa
in bar x y z
in decorator after wrapee with flag  foo de fa fa

Смотрите статью Брюса Экеля для более подробной информации.


20
Остерегайтесь классов декораторов. Они не работают с методами, если вы не заново изобретаете логику дескрипторов методов экземпляра.

9
Делнан, хочешь уточнить? Мне пришлось использовать этот паттерн только один раз, так что я еще ни разу не попал в ловушку.
Росс Роджерс

2
@RossRogers Я полагаю, что @delnan имеет в виду вещи, __name__которые не будут иметь экземпляр класса декоратора?
Jamesc

9
@jamesc Это тоже, хотя это относительно легко решить. Конкретный случай, на который я ссылался, был, class Foo: @MyDec(...) def method(self, ...): blahкоторый не работает, потому Foo().methodчто не будет связанным методом и не пройдет selfавтоматически. Это тоже можно исправить, создав MyDecдескриптор и создав связанные методы __get__, но это более сложно и менее очевидно. В конце концов, классы декораторов не так удобны, как кажутся.

2
@delnan Я бы хотел, чтобы это предупреждение было более заметным. Я поражаюсь этим и заинтересован в том, чтобы увидеть решение, которое работает (более сложное, хотя и менее очевидное).
HaPsantran

12
def decorator(argument):
    def real_decorator(function):
        def wrapper(*args):
            for arg in args:
                assert type(arg)==int,f'{arg} is not an interger'
            result = function(*args)
            result = result*argument
            return result
        return wrapper
    return real_decorator

Использование декоратора

@decorator(2)
def adder(*args):
    sum=0
    for i in args:
        sum+=i
    return sum

Тогда

adder(2,3)

производит

10

но

adder('hi',3)

производит

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-143-242a8feb1cc4> in <module>
----> 1 adder('hi',3)

<ipython-input-140-d3420c248ebd> in wrapper(*args)
      3         def wrapper(*args):
      4             for arg in args:
----> 5                 assert type(arg)==int,f'{arg} is not an interger'
      6             result = function(*args)
      7             result = result*argument

AssertionError: hi is not an interger

8

Это шаблон для декоратора функции, который не требует, ()если не нужно указывать никаких параметров:

import functools


def decorator(x_or_func=None, *decorator_args, **decorator_kws):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kws):
            if 'x_or_func' not in locals() \
                    or callable(x_or_func) \
                    or x_or_func is None:
                x = ...  # <-- default `x` value
            else:
                x = x_or_func
            return func(*args, **kws)

        return wrapper

    return _decorator(x_or_func) if callable(x_or_func) else _decorator

пример этого приведен ниже:

def multiplying(factor_or_func=None):
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if 'factor_or_func' not in locals() \
                    or callable(factor_or_func) \
                    or factor_or_func is None:
                factor = 1
            else:
                factor = factor_or_func
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(factor_or_func) if callable(factor_or_func) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450

Отметим также , что factor_or_func(или любой другой параметр) никогда не должен получает переназначен в wrapper().
norok2

1
Зачем вам нужно регистрироваться locals()?
Шиталь Шах

@ShitalShah, который охватывает случай, когда декоратор используется без ().
norok2

4

В моем случае я решил решить эту проблему с помощью однострочной лямбды, чтобы создать новую функцию декоратора:

def finished_message(function, message="Finished!"):

    def wrapper(*args, **kwargs):
        output = function(*args,**kwargs)
        print(message)
        return output

    return wrapper

@finished_message
def func():
    pass

my_finished_message = lambda f: finished_message(f, "All Done!")

@my_finished_message
def my_func():
    pass

if __name__ == '__main__':
    func()
    my_func()

Когда выполнено, это печатает:

Finished!
All Done!

Возможно, не так расширяемо, как другие решения, но сработало для меня.


Это работает. Хотя да, это затрудняет установку значения для декоратора.
Ариндам Ройховдхури

3

Написание декоратора, который работает с параметрами и без них, является сложной задачей, потому что Python ожидает совершенно различное поведение в этих двух случаях! Многие ответы пытались обойти это, и ниже приведено улучшение ответа от @ norok2. В частности, этот вариант исключает использование locals().

Следуя тому же примеру, что и @ norok2:

import functools

def multiplying(f_py=None, factor=1):
    assert callable(f_py) or f_py is None
    def _decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            return factor * func(*args, **kwargs)
        return wrapper
    return _decorator(f_py) if callable(f_py) else _decorator


@multiplying
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying()
def summing(x): return sum(x)

print(summing(range(10)))
# 45


@multiplying(factor=10)
def summing(x): return sum(x)

print(summing(range(10)))
# 450

Играй с этим кодом .

Уловка в том, что пользователь должен предоставить ключ, пары значений параметров вместо позиционных параметров, и первый параметр зарезервирован.


2

Хорошо известно, что следующие два фрагмента кода почти эквивалентны:

@dec
def foo():
    pass    foo = dec(foo)

############################################
foo = dec(foo)

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

@dec(1, 2, 3)
def foo():
    pass    
###########################################
foo = dec(foo, 1, 2, 3)

Было бы намного проще написать декораторы, если бы вышеприведенное @работало так. К сожалению, дело обстоит не так.


Рассмотрим декоратор, Waitкоторый мешает выполнению программы в течение нескольких секунд. Если вы не передаете время ожидания, то значение по умолчанию составляет 1 секунду. Варианты использования приведены ниже.

##################################################
@Wait
def print_something(something):
    print(something)

##################################################
@Wait(3)
def print_something_else(something_else):
    print(something_else)

##################################################
@Wait(delay=3)
def print_something_else(something_else):
    print(something_else)

Когда Waitесть аргумент, такой как @Wait(3), тогда вызов Wait(3) выполняется прежде, чем что-либо еще произойдет.

То есть следующие две части кода эквивалентны

@Wait(3)
def print_something_else(something_else):
    print(something_else)

###############################################
return_value = Wait(3)
@return_value
def print_something_else(something_else):
    print(something_else)

Это проблема.

if `Wait` has no arguments:
    `Wait` is the decorator.
else: # `Wait` receives arguments
    `Wait` is not the decorator itself.
    Instead, `Wait` ***returns*** the decorator

Одно решение показано ниже:

Давайте начнем с создания следующего класса DelayedDecorator:

class DelayedDecorator:
    def __init__(i, cls, *args, **kwargs):
        print("Delayed Decorator __init__", cls, args, kwargs)
        i._cls = cls
        i._args = args
        i._kwargs = kwargs
    def __call__(i, func):
        print("Delayed Decorator __call__", func)
        if not (callable(func)):
            import io
            with io.StringIO() as ss:
                print(
                    "If only one input, input must be callable",
                    "Instead, received:",
                    repr(func),
                    sep="\n",
                    file=ss
                )
                msg = ss.getvalue()
            raise TypeError(msg)
        return i._cls(func, *i._args, **i._kwargs)

Теперь мы можем написать такие вещи, как:

 dec = DelayedDecorator(Wait, delay=4)
 @dec
 def delayed_print(something):
    print(something)

Обратите внимание, что:

  • dec не принимает множественные аргументы.
  • dec принимает только функцию для переноса.

    класс проверки импорта PolyArgDecoratorMeta (тип): вызов def (Wait, * args, ** kwargs): try: arg_count = len (args) if (arg_count == 1): if callable (args [0]): SuperClass = inspect. getmro (PolyArgDecoratorMeta) [1] r = Суперкласс. call (Wait, args [0]) else: r = DelayedDecorator (Wait, * args, ** kwargs) else: r = DelayedDecorator (Wait, * args, ** kwargs) наконец: pass return r

    класс времени импорта Wait (metaclass = PolyArgDecoratorMeta): def init (i, func, delay = 2): i._func = func i._delay = delay

    def __call__(i, *args, **kwargs):
        time.sleep(i._delay)
        r = i._func(*args, **kwargs)
        return r 

Следующие две части кода эквивалентны:

@Wait
def print_something(something):
     print (something)

##################################################

def print_something(something):
    print(something)
print_something = Wait(print_something)

Мы можем печатать "something"на консоли очень медленно, следующим образом:

print_something("something")

#################################################
@Wait(delay=1)
def print_something_else(something_else):
    print(something_else)

##################################################
def print_something_else(something_else):
    print(something_else)

dd = DelayedDecorator(Wait, delay=1)
print_something_else = dd(print_something_else)

##################################################

print_something_else("something")

Заключительные заметки

Это может выглядеть как много кода, но вам не нужно писать классы DelayedDecoratorи PolyArgDecoratorMetaкаждый раз. Единственный код, который вы должны лично написать что-то вроде следующего, что довольно коротко:

from PolyArgDecoratorMeta import PolyArgDecoratorMeta
import time
class Wait(metaclass=PolyArgDecoratorMeta):
 def __init__(i, func, delay = 2):
     i._func = func
     i._delay = delay

 def __call__(i, *args, **kwargs):
     time.sleep(i._delay)
     r = i._func(*args, **kwargs)
     return r

1

Определите эту «функцию decoratorize» для генерации настраиваемой функции decorator:

def decoratorize(FUN, **kw):
    def foo(*args, **kws):
        return FUN(*args, **kws, **kw)
    return foo

используйте это так:

    @decoratorize(FUN, arg1 = , arg2 = , ...)
    def bar(...):
        ...

1

Отличные ответы выше. Этот пример также иллюстрирует @wraps, который берет строку документа и имя функции из исходной функции и применяет ее к новой упакованной версии:

from functools import wraps

def decorator_func_with_args(arg1, arg2):
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            print("Before orginal function with decorator args:", arg1, arg2)
            result = f(*args, **kwargs)
            print("Ran after the orginal function")
            return result
        return wrapper
    return decorator

@decorator_func_with_args("foo", "bar")
def hello(name):
    """A function which prints a greeting to the name provided.
    """
    print('hello ', name)
    return 42

print("Starting script..")
x = hello('Bob')
print("The value of x is:", x)
print("The wrapped functions docstring is:", hello.__doc__)
print("The wrapped functions name is:", hello.__name__)

Печать:

Starting script..
Before orginal function with decorator args: foo bar
hello  Bob
Ran after the orginal function
The value of x is: 42
The wrapped functions docstring is: A function which prints a greeting to the name provided.
The wrapped functions name is: hello

0

Если и функция, и декоратор должны принимать аргументы, вы можете следовать нижеприведенному подходу.

Например, есть декоратор с именем, decorator1который принимает аргумент

@decorator1(5)
def func1(arg1, arg2):
    print (arg1, arg2)

func1(1, 2)

Теперь, если decorator1аргумент должен быть динамическим или передаваться при вызове функции,

def func1(arg1, arg2):
    print (arg1, arg2)


a = 1
b = 2
seconds = 10

decorator1(seconds)(func1)(a, b)

В приведенном выше коде

  • seconds это аргумент в пользу decorator1
  • a, b аргументы func1
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.