Wie übergebe ich zusätzliche Argumente an einen Python-Dekorateur?

93

Ich habe einen Dekorateur wie unten.

def myDecorator(test_func):
    return callSomeWrapper(test_func)
def callSomeWrapper(test_func):
    return test_func
@myDecorator
def someFunc():
    print 'hello'

Ich möchte diesen Dekorateur verbessern, um ein anderes Argument wie das folgende zu akzeptieren

def myDecorator(test_func,logIt):
    if logIt:
        print "Calling Function: " + test_func.__name__
    return callSomeWrapper(test_func)
@myDecorator(False)
def someFunc():
    print 'Hello'

Aber dieser Code gibt den Fehler,

TypeError: myDecorator () akzeptiert genau 2 Argumente (1 angegeben)

Warum wird die Funktion nicht automatisch übergeben? Wie übergebe ich die Funktion explizit an die Dekorationsfunktion?

balki
quelle
3
balki: Bitte vermeiden Sie die Verwendung von Boolean als Argument, es ist kein gd-Ansatz und verringern Sie die Lesbarkeit des Codes
Kit Ho
8
@KitHo - es ist ein boolesches Flag, daher ist die Verwendung eines booleschen Werts der richtige Ansatz.
AKX
2
@KitHo - was ist "gd"? Ist es gut"?
Rob Bednark

Antworten:

163

Da Sie den Dekorator wie eine Funktion aufrufen, muss eine andere Funktion zurückgegeben werden, die der eigentliche Dekorator ist:

def my_decorator(param):
    def actual_decorator(func):
        print("Decorating function {}, with parameter {}".format(func.__name__, param))
        return function_wrapper(func)  # assume we defined a wrapper somewhere
    return actual_decorator

Die äußere Funktion erhält alle Argumente, die Sie explizit übergeben, und sollte die innere Funktion zurückgeben. Der inneren Funktion wird die zu dekorierende Funktion übergeben und die geänderte Funktion zurückgegeben.

Normalerweise soll der Dekorateur das Funktionsverhalten ändern, indem er es in eine Wrapper-Funktion einwickelt. Hier ist ein Beispiel, das optional die Protokollierung hinzufügt, wenn die Funktion aufgerufen wird:

def log_decorator(log_enabled):
    def actual_decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if log_enabled:
                print("Calling Function: " + func.__name__)
            return func(*args, **kwargs)
        return wrapper
    return actual_decorator

Der functools.wrapsAufruf kopiert Dinge wie den Namen und die Dokumentzeichenfolge in die Wrapper-Funktion, um sie der ursprünglichen Funktion ähnlicher zu machen.

Anwendungsbeispiel:

>>> @log_decorator(True)
... def f(x):
...     return x+1
...
>>> f(4)
Calling Function: f
5
Interjay
quelle
11
Die Verwendung functools.wrapsist ratsam - der ursprüngliche Name, die Dokumentzeichenfolge usw. der umschlossenen Funktion bleiben erhalten.
AKX
@AKX: Danke, ich habe dies dem zweiten Beispiel hinzugefügt.
Interjay
1
Im Grunde genommen nimmt der Dekorateur immer nur ein Argument, nämlich die Funktion. Der Dekorator kann jedoch ein Rückgabewert einer Funktion sein, die Argumente annehmen kann. Ist das richtig?
Balki
2
@ Balki: Ja, das ist richtig. Was die Dinge verwirrt, ist, dass viele Leute die äußere Funktion ( myDecoratorhier) auch als Dekorateur bezeichnen. Dies ist praktisch für einen Benutzer des Dekorateurs, kann jedoch verwirrend sein, wenn Sie versuchen, einen zu schreiben.
Interjay
1
Kleines Detail, das mich verwirrt hat: Wenn Sie log_decoratorein Standardargument verwenden, das Sie nicht verwenden können @log_decorator, muss es sein@log_decorator()
Stardidi
44

Nur um einen anderen Standpunkt zu bieten: die Syntax

@expr
def func(...): #stuff

ist äquivalent zu

def func(...): #stuff
func = expr(func)

Insbesondere exprkann alles sein, was Sie möchten, solange es zu einem Callable ausgewertet wird. In bestimmten Insbesondere exprkann ein Dekorateur Werk sein: Sie geben es einige Parameter und es gibt Ihnen einen Dekorateur. Vielleicht ist ein besserer Weg, Ihre Situation zu verstehen, als

dec = decorator_factory(*args)
@dec
def func(...):

was dann auf gekürzt werden kann

@decorator_factory(*args)
def func(...):

Da es so aussieht, als wäre decorator_factoryes ein Dekorateur, neigen die Leute dazu, es zu benennen, um dies widerzuspiegeln. Dies kann verwirrend sein, wenn Sie versuchen, den Indirektionsebenen zu folgen.

Katriel
quelle
Danke, das hat mir wirklich geholfen, die Gründe für das, was vor sich geht, zu verstehen.
Aditya Sriram
24

Ich möchte nur einen nützlichen Trick hinzufügen, mit dem Dekorationsargumente optional gemacht werden können. Es ermöglicht auch die Wiederverwendung des Dekorators und die Verringerung der Verschachtelung

import functools

def myDecorator(test_func=None,logIt=None):
    if not test_func:
        return functools.partial(myDecorator, logIt=logIt)
    @functools.wraps(test_func)
    def f(*args, **kwargs):
        if logIt==1:
            print 'Logging level 1 for {}'.format(test_func.__name__)
        if logIt==2:
            print 'Logging level 2 for {}'.format(test_func.__name__)
        return test_func(*args, **kwargs)
    return f

#new decorator 
myDecorator_2 = myDecorator(logIt=2)

@myDecorator(logIt=2)
def pow2(i):
    return i**2

@myDecorator
def pow3(i):
    return i**3

@myDecorator_2
def pow4(i):
    return i**4

print pow2(2)
print pow3(2)
print pow4(2)
Alexey Smirnov
quelle
16

Nur eine andere Art, Dekorateure zu machen. Ich finde diesen Weg am einfachsten, meinen Kopf herumzuwickeln.

class NiceDecorator:
    def __init__(self, param_foo='a', param_bar='b'):
        self.param_foo = param_foo
        self.param_bar = param_bar

    def __call__(self, func):
        def my_logic(*args, **kwargs):
            # whatever logic your decorator is supposed to implement goes in here
            print('pre action baz')
            print(self.param_bar)
            # including the call to the decorated function (if you want to do that)
            result = func(*args, **kwargs)
            print('post action beep')
            return result

        return my_logic

# usage example from here on
@NiceDecorator(param_bar='baaar')
def example():
    print('example yay')


example()
Robert Fey
quelle
Danke! Ich habe mir ungefähr 30 Minuten lang einige umwerfende "Lösungen" angesehen, und dies ist die erste, die tatsächlich Sinn macht.
Canhazbits
0

Wenn Sie nun eine Funktion function1mit einem Dekorator aufrufen möchten decorator_with_argund in diesem Fall sowohl die Funktion als auch der Dekorator Argumente annehmen,

def function1(a, b):
    print (a, b)

decorator_with_arg(10)(function1)(1, 2)
SuperNova
quelle