Comment passer des arguments supplémentaires à un décorateur Python?

93

J'ai un décorateur comme ci-dessous.

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

Je souhaite valoriser ce décorateur pour accepter un autre argument comme ci-dessous

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

Mais ce code donne l'erreur,

TypeError: myDecorator () prend exactement 2 arguments (1 donné)

Pourquoi la fonction n'est-elle pas automatiquement transmise? Comment passer explicitement la fonction à la fonction décorateur?

balki
la source
3
balki: évitez d'utiliser boolean comme argument, ce n'est pas une approche gd et réduisez la lisibilité du code
Kit Ho
8
@KitHo - c'est un drapeau booléen, donc utiliser une valeur booléenne est la bonne approche.
AKX
2
@KitHo - qu'est-ce que "gd"? Est-ce bien"?
Rob Bednark

Réponses:

164

Puisque vous appelez le décorateur comme une fonction, il doit renvoyer une autre fonction qui est le décorateur réel:

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

La fonction externe recevra tous les arguments que vous transmettez explicitement et doit renvoyer la fonction interne. La fonction interne recevra la fonction à décorer et retournera la fonction modifiée.

En général, vous voulez que le décorateur modifie le comportement de la fonction en l'enveloppant dans une fonction wrapper. Voici un exemple qui ajoute éventuellement la journalisation lorsque la fonction est appelée:

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

L' functools.wrapsappel copie des éléments comme le nom et la docstring dans la fonction wrapper, pour la rendre plus similaire à la fonction d'origine.

Exemple d'utilisation:

>>> @log_decorator(True)
... def f(x):
...     return x+1
...
>>> f(4)
Calling Function: f
5
interjay
la source
11
Et l'utilisation functools.wrapsest conseillée - elle conserve le nom d'origine, docstring, etc. de la fonction encapsulée.
AKX
@AKX: Merci, j'ai ajouté ceci au deuxième exemple.
entre le
1
Donc, fondamentalement, le décorateur prend toujours un seul argument qui est la fonction. Mais le décorateur peut être une valeur de retour d'une fonction qui peut prendre des arguments. Est-ce correct?
balki
2
@balki: Oui, c'est exact. Ce qui trouble les choses, c'est que beaucoup de gens appelleront également la fonction extérieure ( myDecoratorici) un décorateur. Ceci est pratique pour un utilisateur du décorateur, mais peut être déroutant lorsque vous essayez d'en écrire un.
entre le
1
Petit détail qui m'a dérouté: si votre log_decoratorprend un argument par défaut, vous ne pouvez pas l'utiliser @log_decorator, il doit l'être@log_decorator()
Stardidi
44

Juste pour fournir un point de vue différent: la syntaxe

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

est équivalent à

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

En particulier, exprpeut être tout ce que vous voulez, à condition qu'il soit évalué à un appelable. En particulier , exprpeut être une usine de décorateurs: vous lui donnez quelques paramètres et cela vous donne un décorateur. Alors peut-être qu'une meilleure façon de comprendre votre situation est de

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

qui peut ensuite être raccourci en

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

Bien sûr, comme il ressemble à decorator_factoryun décorateur, les gens ont tendance à le nommer pour refléter cela. Ce qui peut être déroutant lorsque vous essayez de suivre les niveaux d'indirection.

Katriel
la source
Merci, cela m'a vraiment aidé à comprendre la raison d'être de ce qui se passe.
Aditya Sriram
24

Je veux juste ajouter une astuce utile qui permettra de rendre les arguments du décorateur optionnels. Cela permettra également de réutiliser le décorateur et de diminuer l'emboîtement

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
la source
16

Juste une autre façon de faire des décorateurs. Je trouve que c'est la façon la plus simple de comprendre ma tête.

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
la source
Merci! étudie des «solutions» hallucinantes depuis environ 30 minutes et c'est la première qui a du sens.
canhazbits
0

Maintenant, si vous voulez appeler une fonction function1avec un décorateur decorator_with_arget dans ce cas, la fonction et le décorateur prennent des arguments,

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

decorator_with_arg(10)(function1)(1, 2)
SuperNova
la source