Jak przekazać dodatkowe argumenty do dekoratora Pythona?

99

Mam dekoratora jak poniżej.

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

Chcę ulepszyć ten dekorator, aby zaakceptował inny argument, jak poniżej

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

Ale ten kod daje błąd,

TypeError: myDecorator () przyjmuje dokładnie 2 argumenty (podany 1)

Dlaczego funkcja nie jest przekazywana automatycznie? Jak jawnie przekazać tę funkcję do funkcji dekoratora?

balki
źródło
3
Balki: proszę unikać logiczną jako argumentu, że nie jest to podejście gd i zmniejszyć kodeksu readliability
Kit Ho
8
@KitHo - to flaga logiczna, więc użycie wartości boolowskiej jest właściwym podejściem.
AKX
2
@KitHo - co to jest „gd”? Czy to jest dobre"?
Rob Bednark,

Odpowiedzi:

172

Ponieważ wywołujesz dekorator jako funkcję, musi on zwrócić inną funkcję, która jest rzeczywistym dekoratorem:

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

Funkcja zewnętrzna otrzyma wszelkie argumenty, które przekażesz jawnie, i powinna zwrócić funkcję wewnętrzną. Funkcja wewnętrzna otrzyma funkcję dekoracyjną i zwróci zmodyfikowaną funkcję.

Zwykle chcesz, aby dekorator zmienił zachowanie funkcji, opakowując ją w funkcję opakowującą. Oto przykład, który opcjonalnie dodaje rejestrowanie, gdy funkcja jest wywoływana:

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

W functools.wrapskopiuje nazywać rzeczy takie jak imię i docstring do funkcji otoki, aby uczynić go bardziej podobna do pierwotnej funkcji.

Przykładowe użycie:

>>> @log_decorator(True)
... def f(x):
...     return x+1
...
>>> f(4)
Calling Function: f
5
interjay
źródło
11
Używanie functools.wrapsjest zalecane - zachowuje oryginalną nazwę, ciąg dokumentów itp. Opakowanej funkcji.
AKX
@AKX: Dzięki, dodałem to do drugiego przykładu.
interjay
1
Zasadniczo dekorator zawsze przyjmuje tylko jeden argument, którym jest funkcja. Ale dekorator może być wartością zwracaną przez funkcję, która może przyjmować argumenty. Czy to jest poprawne?
balki
2
@balki: Tak, zgadza się. To, co myli, to fakt, że wiele osób będzie również nazywać ( myDecoratortutaj) funkcję zewnętrzną dekoratorem. Jest to wygodne dla użytkownika dekoratora, ale może być mylące, gdy próbujesz go napisać.
interjay
1
Mały szczegół, który mnie zdezorientował: jeśli log_decoratorprzyjmujesz domyślny argument, nie możesz go użyć @log_decorator, to musi być@log_decorator()
Stardidi
46

Żeby przedstawić inny punkt widzenia: składnię

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

jest równa

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

W szczególności exprmoże to być cokolwiek chcesz, o ile ma wartość wywoływalną. W szczególności szczególności exprmoże być fabrykę dekorator: nadać mu pewne parametry i daje dekorator. Więc może lepszym sposobem na zrozumienie Twojej sytuacji jest

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

który można następnie skrócić do

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

Oczywiście, ponieważ wygląda jak decorator_factoryjest dekorator, ludzie mają tendencję, aby wymienić je odzwierciedlać. Co może być mylące, gdy próbujesz podążać za poziomami pośrednictwa.

Katriel
źródło
Dzięki, to naprawdę pomogło mi zrozumieć uzasadnienie tego, co się dzieje.
Aditya Sriram
25

Po prostu chcę dodać jakąś użyteczną sztuczkę, która pozwoli uczynić argumenty dekoratora opcjonalnymi. Pozwoli to również na ponowne użycie dekoratora i zmniejszenie zagnieżdżenia

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

Po prostu inny sposób robienia dekoratorów. Uważam, że ten sposób jest najłatwiejszy do ogarnięcia głowy.

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
źródło
Dziękuję Ci! Przez około 30 minut przyglądałem się pewnym zaskakującym „rozwiązaniom” i to jest pierwsze, które ma sens.
canhazbits
0

Teraz, jeśli chcesz wywołać funkcję function1z dekoratorem decorator_with_argiw tym przypadku zarówno funkcja, jak i dekorator przyjmują argumenty,

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

decorator_with_arg(10)(function1)(1, 2)
SuperNova
źródło