Декоратори з параметрами?


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 не реалізував це, передаючи параметри як наступні аргументи декоратора після "функції". "Йо дауг, я чув, як ти любиш закриття ..." etcetera.
Мішель Мюллер

3
> Була б функція першим аргументом чи останнім? Очевидно спочатку, оскільки параметри - це список параметрів змінної довжини. > Також дивно, що ви б "викликали" функцію з підписом, відмінним від визначеного у визначенні. Як ви зазначаєте, він би насправді добре відповідав - це майже аналогічно тому, як називається метод класу. Щоб зробити це більш зрозумілим, ви можете мати щось на зразок декоратора (self_func, param1, ...). Але зауважте: я не виступаю за будь-які зміни тут, Python занадто далеко вниз для цього, і ми можемо побачити, як зламалися зміни, що відбулися ..
Мішель Мюллер,

21
ви забули ДУЖЕ КОРИСНІ functools.wraps для прикраси обгортки :)
socketpair

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

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

325

Редагувати : для глибшого розуміння ментальної моделі декораторів, подивіться на цю дивовижну розмову Pycon. ну варто 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метадані 's перекреслюються, зокрема docstring та ім'я функції.

>>> 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тут !?
displayname

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

85

Я хотів би показати ідею, яка є IMHO досить елегантною. Рішення, запропоноване 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
Не використав це саме так, але допоміг мені осягнути концепцію :) Дякую!
mouckatron

Я спробував це і виникли деякі проблеми .
Джефф

@Jeff, ти можеш поділитися з нами тими проблемами?
Даків

У мене це було пов'язане з моїм питанням, і я це зрозумів ... Мені потрібно було зателефонувати @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
Остерігайтеся занять з декораторів. Вони не працюють над методами, якщо ви вручну не винаходити логіку дескрипторів instancemethod.

9
delnan, хочете допрацювати? Мені довелося використовувати лише цей раз один раз, так що я ще не потрапив до жодної з підводних каменів.
Росс Роджерс

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

Чому вам потрібно зареєструватися locals()?
Shital Shah

@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!

Можливо, не настільки розширений, як інші рішення, але працював на мене.


Це працює. Хоча так, це ускладнює встановлення значення декоратору.
Arindam Roychowdhury

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 call (Зачекайте, * args, ** kwargs): спробуйте: arg_count = len (args) if (arg_count == 1): if callable (args [0]): SuperClass = Inspect. getmro (PolyArgDecoratorMeta) [1] r = SuperClass. виклик (Зачекайте, args [0]) else: r = DelayedDecorator (Wait, * args, ** kwargs) else: r = DelayedDecorator (Wait, * args, ** kwargs) нарешті: передайте return r

    час імпорту класу Wait (метаклас = PolyArgDecoratorMeta): def init (i, func, затримка = 2): i._func = func i._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

визначте цю "функцію декоратора" для створення спеціалізованої функції декоратора:

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

використовуйте його таким чином:

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

1

Чудові відповіді вище. Цей також ілюструє @wraps, що бере рядок doc та ім'я функції від вихідної функції та застосовує її до нової обгорнутої версії:

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.