Dekorateure mit Parametern?


401

Ich habe ein Problem mit der Übertragung der Variablen 'insurance_mode' durch den Dekorateur. Ich würde es mit der folgenden Dekorateuraussage tun:

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

aber leider funktioniert diese Aussage nicht. Vielleicht gibt es einen besseren Weg, um dieses Problem zu lösen.

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
Ihr Beispiel ist syntaktisch nicht gültig. execute_complete_reservationnimmt zwei Parameter, aber Sie übergeben es einen. Dekorateure sind nur syntaktischer Zucker, um Funktionen in andere Funktionen zu verpacken. Eine vollständige Dokumentation finden Sie unter docs.python.org/reference/compound_stmts.html#function .
Brian Clapper

Antworten:


687

Die Syntax für Dekoratoren mit Argumenten ist etwas anders - der Dekorator mit Argumenten sollte eine Funktion zurückgeben, die eine Funktion übernimmt und eine andere Funktion zurückgibt. Es sollte also wirklich einen normalen Dekorateur zurückgeben. Ein bisschen verwirrend, oder? Was ich meine ist:

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

Hier können Sie mehr zu diesem Thema lesen - es ist auch möglich, dies mit aufrufbaren Objekten zu implementieren, und das wird auch dort erklärt.


56
Ich frage mich, warum GVR es nicht implementiert hat, indem es die Parameter als nachfolgende Dekorationsargumente nach 'function' übergeben hat. "Yo dawg, ich habe gehört, Sie mögen Verschlüsse ..." usw.
Michel Müller

3
> Wäre Funktion das erste oder letzte Argument? Offensichtlich zuerst, da die Parameter eine Parameterliste variabler Länge sind. > Es ist auch seltsam, dass Sie die Funktion mit einer anderen Signatur als der in der Definition "aufrufen". Wie Sie hervorheben, würde es eigentlich ziemlich gut passen - es ist ziemlich analog zu dem, wie eine Klassenmethode aufgerufen wird. Um es klarer zu machen, könnten Sie so etwas wie eine Dekorationskonvention (self_func, param1, ...) haben. Aber beachten Sie: Ich befürworte hier keine Änderung, Python ist zu weit davon entfernt und wir können sehen, wie sich bahnbrechende Änderungen ausgewirkt haben.
Michel Müller

21
Sie haben SEHR NÜTZLICHE functools.wraps für die Dekoration der Verpackung vergessen :)
Socketpair

10
Sie haben die Rückkehr vergessen, als Sie die Funktion return function(*args, **kwargs)
aufgerufen haben

36
Vielleicht offensichtlich, aber nur für den Fall: Sie müssen diesen Dekorator als @decorator()und nicht nur verwenden @decorator, auch wenn Sie nur optionale Argumente haben.
Patrick Mevzek

325

Bearbeiten : Um ein umfassendes Verständnis des mentalen Modells von Dekorateuren zu erhalten, werfen Sie einen Blick auf diesen fantastischen Pycon Talk. lohnt sich die 30 Minuten.

Eine Art, über Dekorateure mit Argumenten nachzudenken, ist

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

wird übersetzt in

foo = decorator(foo)

Also, wenn der Dekorateur Argumente hatte,

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

wird übersetzt in

foo = decorator_with_args(arg)(foo)

decorator_with_args ist eine Funktion, die ein benutzerdefiniertes Argument akzeptiert und den tatsächlichen Dekorator zurückgibt (der auf die dekorierte Funktion angewendet wird).

Ich benutze einen einfachen Trick mit Teiltönen, um meine Dekorateure einfach zu machen

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

Aktualisieren:

Über, foo wirdreal_decorator(foo)

Ein Effekt beim Dekorieren einer Funktion besteht darin, dass der Name foobei der Deklaratordeklaration überschrieben wird. foowird von allem "überschrieben", was von zurückgegeben wirdreal_decorator . In diesem Fall ein neues Funktionsobjekt.

Alle fooMetadaten werden überschrieben, insbesondere die Dokumentzeichenfolge und der Funktionsname.

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

functools.wraps bietet uns eine bequeme Methode, um die Dokumentzeichenfolge und den Namen zur zurückgegebenen Funktion zu "heben".

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
Ihre Antwort erklärte perfekt die inhärente Orthogonalität des Dekorateurs, danke
zsf222

Könnten Sie hinzufügen @functools.wraps?
Mr_and_Mrs_D

1
@Mr_and_Mrs_D, ich habe den Beitrag mit einem Beispiel mit aktualisiert functool.wraps. Das Hinzufügen im Beispiel kann die Leser weiter verwirren.
srj

7
Was ist arghier?
Anzeigename

1
Wie werden Sie das baran das Argument von übergebene Argument weitergeben real_decorator?
Chang Zhao

85

Ich möchte eine Idee zeigen, die meiner Meinung nach ziemlich elegant ist. Die von t.dubrownik vorgeschlagene Lösung zeigt ein Muster, das immer das gleiche ist: Sie benötigen die dreischichtige Hülle, unabhängig davon, was der Dekorateur tut.

Also dachte ich, das ist ein Job für einen Meta-Dekorateur, das heißt einen Dekorateur für Dekorateure. Da ein Dekorateur eine Funktion ist, arbeitet er tatsächlich als normaler Dekorateur mit Argumenten:

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

Dies kann auf einen normalen Dekorateur angewendet werden, um Parameter hinzuzufügen. Nehmen wir zum Beispiel an, wir haben den Dekorateur, der das Ergebnis einer Funktion verdoppelt:

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)

Mit können @parametrizedwir einen generischen @multiplyDekorator mit einem Parameter erstellen

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

Herkömmlicherweise der erste Parameter eines parametrisierten Dekorators die Funktion, während die verbleibenden Argumente dem Parameter des parametrisierten Dekorators entsprechen.

Ein interessantes Anwendungsbeispiel könnte ein typsicherer, durchsetzungsfähiger Dekorateur sein:

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

Ein letzter Hinweis: Hier verwende ich nicht functools.wrapsfür die Wrapper-Funktionen, aber ich würde empfehlen, sie immer zu verwenden.


3
Ich habe das nicht genau verwendet, aber es hat mir geholfen, mich mit dem Konzept vertraut zu machen :) Danke!
Mouckatron

Ich habe es versucht und hatte einige Probleme .
Jeff

@ Jeff, kannst du uns die Art von Problemen mitteilen, die du hattest?
Dacav

Ich hatte es mit meiner Frage verknüpft und ich habe es herausgefunden ... Ich musste @wrapsmeine für meinen speziellen Fall anrufen .
Jeff

4
Oh Mann, ich habe einen ganzen Tag verloren. Zum Glück kam ich vorbei diese Antwort gestoßen (die übrigens die beste Antwort sein könnte, die jemals im gesamten Internet erstellt wurde). Auch sie benutzen deinen @parametrizedTrick. Das Problem, das ich hatte, war, dass ich vergessen habe, dass die @Syntax den tatsächlichen Aufrufen entspricht (irgendwie wusste ich das und wusste das nicht zur gleichen Zeit, wie Sie meiner Frage entnehmen können). Wenn Sie also die @Syntax in weltliche Aufrufe übersetzen möchten, um zu überprüfen, wie sie funktioniert, sollten Sie sie zuerst vorübergehend auskommentieren, oder Sie würden sie am Ende zweimal aufrufen und mumbojumbo-Ergebnisse erhalten
z33k

79

Hier ist eine leicht modifizierte Version der Antwort von t.dubrownik . Warum?

  1. Als allgemeine Vorlage sollten Sie den Rückgabewert der ursprünglichen Funktion zurückgeben.
  2. Dies ändert den Namen der Funktion, was sich auf andere Dekoratoren / Codes auswirken kann.

Verwenden Sie also @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

Ich nehme an, Ihr Problem besteht darin, Ihrem Dekorateur Argumente zu übermitteln. Dies ist etwas schwierig und nicht einfach.

Hier ist ein Beispiel dafür:

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

Drucke:

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

Weitere Informationen finden Sie im Artikel von Bruce Eckel.


20
Vorsicht vor Dekorateurklassen. Sie funktionieren nur mit Methoden, wenn Sie die Logik der Instanzmethoden-Deskriptoren manuell neu erfinden.

9
Delnan, möchten Sie näher darauf eingehen? Ich musste dieses Muster nur einmal verwenden, daher habe ich noch keine der Fallstricke getroffen.
Ross Rogers

2
@ RossRogers Ich vermute, dass sich @delnan auf Dinge bezieht, __name__die eine Instanz der Decorator-Klasse nicht haben wird?
Jamesc

9
@jamesc Auch das, obwohl das relativ einfach zu lösen ist. Der spezielle Fall, auf den ich mich bezog, war class Foo: @MyDec(...) def method(self, ...): blahder, der nicht funktioniert, weil er Foo().methodkeine gebundene Methode ist und nicht selfautomatisch übergeben wird. Auch dies kann behoben werden, indem MyDecein Deskriptor erstellt und gebundene Methoden erstellt werden. Dies ist jedoch aufwändiger __get__und viel weniger offensichtlich. Am Ende sind Dekorateurklassen nicht so bequem, wie sie scheinen.

2
@delnan Ich würde gerne sehen, dass diese Einschränkung stärker hervorgehoben wird. Ich treffe es und bin daran interessiert, eine Lösung zu sehen, die funktioniert (mehr involviert und weniger offensichtlich, obwohl es sein mag).
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

Verwendung des Dekorateurs

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

Dann ist die

adder(2,3)

produziert

10

aber

adder('hi',3)

produziert

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

Dies ist eine Vorlage für einen Funktionsdekorateur, die nicht erforderlich ist, ()wenn keine Parameter angegeben werden sollen:

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

Ein Beispiel hierfür ist unten angegeben:

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

Beachten Sie auch , dass factor_or_func(oder einen anderen Parameter) sollte nie zugewiesen wird in wrapper().
Norok2

Warum müssen Sie einchecken locals()?
Shital Shah

@ShitalShah, das den Fall abdeckt, in dem der Dekorateur ohne verwendet wird ().
norok2

4

In meinem Fall habe ich beschlossen, dies über ein einzeiliges Lambda zu lösen, um eine neue Dekorationsfunktion zu erstellen:

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

Bei der Ausführung wird Folgendes gedruckt:

Finished!
All Done!

Vielleicht nicht so erweiterbar wie andere Lösungen, hat aber für mich funktioniert.


Das funktioniert. Obwohl ja, macht es dies schwierig, den Wert auf den Dekorateur einzustellen.
Arindam Roychowdhury

3

Das Schreiben eines Dekorators, der mit und ohne Parameter arbeitet, ist eine Herausforderung, da Python in diesen beiden Fällen ein völlig anderes Verhalten erwartet! Viele Antworten haben versucht, dies zu umgehen, und im Folgenden finden Sie eine Verbesserung der Antwort von @ norok2. Insbesondere eliminiert diese Variation die Verwendung vonlocals() .

Nach dem gleichen Beispiel wie bei @ 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

Spielen Sie mit diesem Code .

Der Haken ist, dass der Benutzer Schlüssel- und Wertepaare von Parametern anstelle von Positionsparametern angeben muss und der erste Parameter reserviert ist.


2

Es ist bekannt, dass die folgenden zwei Codeteile nahezu gleichwertig sind:

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

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

Ein häufiger Fehler ist zu denken, dass @das Argument ganz links einfach verborgen bleibt .

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

Es wäre viel einfacher, Dekorateure zu schreiben, wenn das oben Genannte so @funktioniert. Leider wird das nicht so gemacht.


WaitStellen Sie sich einen Dekorateur vor, der die Programmausführung für einige Sekunden übernimmt. Wenn Sie keine Wartezeit einhalten, beträgt der Standardwert 1 Sekunde. Anwendungsfälle sind unten aufgeführt.

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

Wenn Waitein Argument wie z. B. vorhanden ist @Wait(3), wird der Aufruf zuvorWait(3) ausgeführt etwas anderes passiert.

Das heißt, die folgenden zwei Codeteile sind äquivalent

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

Das ist ein 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

Eine Lösung ist unten gezeigt:

Beginnen wir mit der Erstellung der folgenden Klasse 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)

Jetzt können wir Dinge schreiben wie:

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

Beachten Sie, dass:

  • dec akzeptiert nicht mehrere Argumente.
  • dec akzeptiert nur die zu verpackende Funktion.

    import inspect class PolyArgDecoratorMeta (Typ): 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. Anruf (Wait, args [0]) else: r = DelayedDecorator (Wait, * args, ** kwargs) else: r = DelayedDecorator (Wait, * args, ** kwargs) endlich: pass return r

    Zeitklasse importieren Warten (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 

Die folgenden zwei Codeteile sind äquivalent:

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

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

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

Wir können "something"sehr langsam wie folgt auf die Konsole drucken :

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

Schlussbemerkungen

Es mag wie eine Menge Code aussehen, aber Sie müssen nicht die Klassen schreiben DelayedDecoratorund PolyArgDecoratorMetajede Zeit. Der einzige Code, den Sie persönlich schreiben müssen, ist ziemlich kurz:

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

Definieren Sie diese "Dekorationsfunktion", um eine benutzerdefinierte Dekorationsfunktion zu generieren:

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

benutze es so:

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

1

Tolle Antworten oben. Dies zeigt auch @wraps, wie die Dokumentzeichenfolge und der Funktionsname von der ursprünglichen Funktion übernommen und auf die neue umschlossene Version angewendet werden:

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

Drucke:

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

Falls sowohl die Funktion als auch der Dekorateur Argumente annehmen müssen, können Sie den folgenden Ansatz verfolgen.

Zum Beispiel gibt es einen Dekorateur namens, decorator1der ein Argument akzeptiert

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

func1(1, 2)

Wenn das decorator1Argument nun dynamisch sein oder beim Aufrufen der Funktion übergeben werden muss,

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


a = 1
b = 2
seconds = 10

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

Im obigen Code

  • seconds ist das Argument für decorator1
  • a, b sind die Argumente von func1
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.