Dekoratorzy z parametrami?

401

Mam problem z przeniesieniem zmiennej „Insurance_mode” przez dekoratora. Zrobiłbym to przez następujące oświadczenie dekoratora:

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

ale niestety to stwierdzenie nie działa. Być może istnieje lepszy sposób na rozwiązanie tego problemu.

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
falek.marcin
źródło
3
Twój przykład nie jest poprawny pod względem składniowym. execute_complete_reservationbierze dwa parametry, ale przekazujesz jeden. Dekoratory to po prostu cukier składniowy do zawijania funkcji wewnątrz innych funkcji. Zobacz docs.python.org/reference/compound_stmts.html#function dla kompletnej dokumentacji.
Brian Clapper,

Odpowiedzi:

687

Składnia dekoratorów z argumentami jest nieco inna - dekorator z argumentami powinien zwrócić funkcję, która przyjmuje funkcję i zwraca inną funkcję. Więc naprawdę powinien zwrócić normalnego dekoratora. Trochę mylące, prawda? Chodzi mi o to że:

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

Tutaj możesz przeczytać więcej na ten temat - możliwe jest również zaimplementowanie tego przy użyciu obiektów na żądanie i to również tam wyjaśniono.

t.dubrownik
źródło
56
Zastanawiam się, dlaczego GVR nie zaimplementował go, przekazując parametry jako kolejne argumenty dekoratora po „funkcji”. „Yo dawg, słyszałem, że lubisz zamknięcia ...” itd.
Michel Müller,
3
> Czy funkcja byłaby pierwszym argumentem czy ostatnim? Oczywiście po pierwsze, ponieważ parametry są listą parametrów o zmiennej długości. > Dziwne jest również to, że „wywołujesz” funkcję z podpisem innym niż ten w definicji. Jak zauważyłeś, w rzeczywistości pasowałoby całkiem dobrze - jest to prawie analogiczne do tego, jak wywoływana jest metoda klasowa. Aby to wyjaśnić, możesz mieć coś w rodzaju konwencji dekoratora (self_func, param1, ...). Ale uwaga: nie opowiadam się za żadną zmianą tutaj, Python jest na to zbyt daleko i możemy zobaczyć, jak przełamały się zmiany.
Michel Müller
21
zapomniałeś BARDZO UŻYTECZNY funools.wraps do ozdabiania opakowania :)
socketpair
10
Zapomniałeś o powrocie podczas wywoływania funkcji, tj. return function(*args, **kwargs)
formiaczek
36
Być może oczywiste, ale na wszelki wypadek: musisz użyć tego dekoratora jako @decorator()nie tylko @decorator, nawet jeśli masz tylko opcjonalne argumenty.
Patrick Mevzek,
324

Edycja : aby uzyskać dogłębne zrozumienie mentalnego modelu dekoratorów, spójrz na niesamowitą rozmowę Pycon. warte 30 minut.

Jednym ze sposobów myślenia o dekoratorach z argumentami jest

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

przetłumaczyć na

foo = decorator(foo)

Więc jeśli dekorator miał argumenty,

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

przetłumaczyć na

foo = decorator_with_args(arg)(foo)

decorator_with_args to funkcja, która akceptuje niestandardowy argument i zwraca rzeczywisty dekorator (który zostanie zastosowany do dekorowanej funkcji).

Używam prostej sztuczki z częściami, aby ułatwić dekoratorom

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

Aktualizacja:

Powyżej foostaje sięreal_decorator(foo)

Jednym z efektów dekorowania funkcji foojest zastąpienie nazwy w deklaracji dekoratora. foojest „zastępowane” przez to, co jest zwracane real_decorator. W takim przypadku nowy obiekt funkcji.

Wszystkie foometadane są nadpisane, w szczególności nazwa dokumentu i nazwa funkcji.

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

funkools.wraps zapewnia nam wygodną metodę „podniesienia” dokumentu i nazwy do zwracanej funkcji.

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)>
srj
źródło
4
Twoja odpowiedź doskonale wyjaśniła wrodzoną ortogonalność dekoratora, dziękuję
zsf222
Można dodać @functools.wraps?
Mr_and_Mrs_D
1
@Mr_and_Mrs_D, zaktualizowałem post o przykład z functool.wraps. Dodanie go w tym przykładzie może jeszcze bardziej dezorientować czytelników.
srj
7
Co argtu jest !?
nazwa wyświetlana
1
Jak przekażesz argument przekazany barargumentowi real_decorator?
Chang Zhao,
85

Chciałbym pokazać pomysł, który jest IMHO dość elegancki. Rozwiązanie zaproponowane przez t.dubrownik pokazuje wzór, który jest zawsze taki sam: potrzebujesz trójwarstwowego opakowania niezależnie od tego, co robi dekorator.

Pomyślałem więc, że jest to praca dla meta-dekoratora, czyli dekoratora dla dekoratorów. Ponieważ dekorator jest funkcją, faktycznie działa jako zwykły dekorator z argumentami:

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

Można to zastosować do zwykłego dekoratora w celu dodania parametrów. Powiedzmy na przykład, że mamy dekorator, który podwaja wynik funkcji:

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)

Z @parametrized możemy zbudować ogólny @multiplydekorator posiadający parametr

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

Konwencjonalnie pierwszy parametr sparametryzowany dekoratora jest funkcja, natomiast pozostałe argumenty będą odpowiadały parametrowi sparametryzowanego dekoratora.

Ciekawym przykładem użycia może być asertywna dekoratorka bezpieczna dla typu:

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

Ostatnia uwaga: tutaj nie używam functools.wrapsfunkcji otoki, ale zalecałbym używanie jej przez cały czas.

Dacav
źródło
3
Nie użyłem tego dokładnie, ale pomógł mi rozejrzeć się po koncepcji :) Dzięki!
mouckatron,
Próbowałem tego i miałem pewne problemy .
Jeff
@Jeff, czy możesz podzielić się z nami swoimi problemami?
Dacav
Połączyłem to z moim pytaniem i wymyśliłem to ... Musiałem zadzwonić @wrapsdo mnie w mojej konkretnej sprawie.
Jeff
4
Och chłopcze, straciłem na tym cały dzień. Na szczęście natrafiłem na tę odpowiedź (która, nawiasem mówiąc, może być najlepszą odpowiedzią, jaką kiedykolwiek stworzono w całym Internecie). Oni też używają twojej @parametrizedsztuczki. Problem, jaki miałem, polegał na tym, że zapomniałem, że @składnia jest równa rzeczywistym wywołaniom (jakoś to wiedziałem i nie wiedziałem o tym w tym samym czasie, co można zebrać na podstawie mojego pytania). Więc jeśli chcesz przetłumaczyć @składnię na przyziemne wywołania, by sprawdzić, jak to działa, lepiej najpierw skomentuj to tymczasowo, albo skończysz na dwukrotnym wywołaniu i uzyskaniu wyników
mumbojumbo
79

Oto nieco zmodyfikowana wersja odpowiedzi t.dubrownik . Dlaczego?

  1. Jako ogólny szablon powinieneś zwrócić wartość zwracaną z oryginalnej funkcji.
  2. Zmienia to nazwę funkcji, co może wpłynąć na inne dekoratory / kod.

Więc użyj @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
Ross R.
źródło
37

Zakładam, że twoim problemem jest przekazywanie argumentów dekoratorowi. Jest to trochę trudne i nie jest proste.

Oto przykład, jak to zrobić:

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')

Wydruki:

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

Aby uzyskać więcej informacji, zobacz artykuł Bruce'a Eckela.

Ross Rogers
źródło
20
Uważaj na klasy dekoratorów. Nie działają one na metodach, chyba że ręcznie zmienisz logikę deskryptorów metody instancemethod.
9
delnan, chcesz opracować? Musiałem użyć tego wzoru tylko raz, więc nie trafiłem jeszcze w żadną z pułapek.
Ross Rogers
2
@RossRogers Domyślam się, że @delnan odnosi się do takich rzeczy, jak to, __name__czego nie będzie miało wystąpienie klasy dekoratora?
jamesc
9
@jamesc To też, choć jest to stosunkowo łatwe do rozwiązania. Konkretny przypadek, o którym mówiłem, class Foo: @MyDec(...) def method(self, ...): blahnie działa, ponieważ Foo().methodnie będzie metodą powiązaną i nie przejdzie selfautomatycznie. To również można naprawić, tworząc MyDecdeskryptor i tworząc powiązane metody __get__, ale jest to bardziej zaangażowane i znacznie mniej oczywiste. W końcu klasy dekoratorów nie są tak wygodne, jak się wydaje.
2
@delnan Chciałbym, aby to zastrzeżenie było bardziej widoczne. Uderzam w to i jestem zainteresowany rozwiązaniem, które DZIAŁA (bardziej angażuje mniej oczywiste, choć może być).
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

Zastosowanie dekoratora

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

A później

adder(2,3)

produkuje

10

ale

adder('hi',3)

produkuje

---------------------------------------------------------------------------
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
Gajendra D Ambi
źródło
8

To jest szablon dekoratora funkcji, który nie wymaga, ()jeśli nie zostaną podane żadne parametry:

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

przykład tego podano poniżej:

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
norok2
źródło
Należy również zauważyć, że factor_or_func(lub jakikolwiek inny parametr) nigdy nie powinien zostaje przeniesiony w wrapper().
norok2
Dlaczego musisz się zameldować locals()?
Shital Shah
@ShitalShah, który obejmuje przypadek, w którym dekorator jest używany bez ().
norok2
4

W moim przypadku postanowiłem rozwiązać ten problem za pomocą jednowierszowej lambdy, aby utworzyć nową funkcję dekoratora:

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()

Po uruchomieniu drukuje:

Finished!
All Done!

Być może nie tak rozszerzalne jak inne rozwiązania, ale działało dla mnie.

ZacBook
źródło
To działa. Chociaż tak, utrudnia to ustawienie wartości dekoratora.
Arindam Roychowdhury
3

Napisanie dekoratora, który działa z parametrem i bez niego, stanowi wyzwanie, ponieważ w tych dwóch przypadkach Python oczekuje zupełnie innego zachowania! Wiele odpowiedzi próbowało obejść ten problem, a poniżej jest poprawa odpowiedzi przez @ norok2. W szczególności ta odmiana eliminuje użycielocals() .

Postępując zgodnie z przykładem podanym przez @ 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

Graj z tym kodem .

Chodzi o to, że użytkownik musi podać klucz, pary wartości parametrów zamiast parametrów pozycyjnych, a pierwszy parametr jest zarezerwowany.

Shital Shah
źródło
2

Powszechnie wiadomo, że następujące dwa fragmenty kodu są prawie równoważne:

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

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

Częstym błędem jest myślenie, że @po prostu kryje się argument po lewej stronie.

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

Byłoby o wiele łatwiej pisać dekoratorów, jeśli powyższe jest, jak @działa. Niestety, nie tak to się robi.


Zastanów się nad dekoratorem, Waitktóry na kilka sekund wstrzymuje wykonywanie programu. Jeśli nie upłynie czas oczekiwania, wartość domyślna to 1 sekunda. Przypadki użycia pokazano poniżej.

##################################################
@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)

Gdy Waitma argument, taki jak @Wait(3), wówczas wywołanie Wait(3) jest wykonywane wcześniej cokolwiek innego się wydarzy.

Oznacza to, że następujące dwa fragmenty kodu są równoważne

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

To jest problem.

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

Jedno rozwiązanie pokazano poniżej:

Zacznijmy od utworzenia następującej klasy 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)

Teraz możemy pisać takie rzeczy jak:

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

Uwaga:

  • dec nie akceptuje wielu argumentów.
  • dec akceptuje tylko funkcję do zapakowania.

    import inspect class PolyArgDecoratorMeta (type): def call (Wait, * args, ** kwargs): try: arg_count = len (args) if (arg_count == 1): if callable (args [0]): SuperClass = inspect. getmro (PolyArgDecoratorMeta) [1] r = SuperClass. połączenie (czekaj, args [0]) else: r = DelayedDecorator (czekaj, * args, ** kwargs) else: r = DelayedDecorator (czekaj, * args, ** kwargs) na koniec: pass return r

    klasa czasu importu Wait (metaclass = PolyArgDecoratorMeta): def init (i, func, delay = 2): i._func = func i._delay = opóźnienie

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

Poniższe dwa fragmenty kodu są równoważne:

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

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

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

Możemy drukować "something"na konsoli bardzo powoli, w następujący sposób:

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")

Uwagi końcowe

To może wyglądać jak dużo kodu, ale nie trzeba pisać klas DelayedDecoratori PolyArgDecoratorMetaza każdym wymiarze. Jedyny kod, który musisz osobiście napisać w następujący sposób, który jest dość krótki:

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
Samuel Muldoon
źródło
1

zdefiniuj tę „funkcję dekoratora”, aby wygenerować niestandardową funkcję dekoratora:

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

użyj tego w ten sposób:

    @decoratorize(FUN, arg1 = , arg2 = , ...)
    def bar(...):
        ...
chen.wq
źródło
1

Świetne odpowiedzi powyżej. Ten ilustruje również @wraps, który pobiera ciąg dokumentu i nazwę funkcji z oryginalnej funkcji i stosuje ją do nowej zapakowanej wersji:

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__)

Wydruki:

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
run_the_race
źródło
0

W przypadku, gdy zarówno funkcja, jak i dekorator muszą wziąć argumenty, możesz zastosować poniższe podejście.

Na przykład jest dekorator o nazwie, decorator1który bierze argument

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

func1(1, 2)

Teraz, jeśli decorator1argument musi być dynamiczny lub przekazywany podczas wywoływania funkcji,

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


a = 1
b = 2
seconds = 10

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

W powyższym kodzie

  • seconds jest argumentem za decorator1
  • a, b są argumentami func1
SuperNova
źródło