Des décorateurs avec des paramètres?


401

J'ai un problème avec le transfert de la variable 'insurance_mode' par le décorateur. Je le ferais par la déclaration de décorateur suivante:

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

mais malheureusement, cette déclaration ne fonctionne pas. Peut-être existe-t-il une meilleure façon de résoudre ce problème.

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
Votre exemple n'est pas syntaxiquement valide. execute_complete_reservationprend deux paramètres, mais vous en passez un. Les décorateurs ne sont que du sucre syntaxique pour encapsuler des fonctions à l'intérieur d'autres fonctions. Voir docs.python.org/reference/compound_stmts.html#function pour une documentation complète.
Brian Clapper

Réponses:


687

La syntaxe des décorateurs avec des arguments est un peu différente - le décorateur avec des arguments doit retourner une fonction qui prendra une fonction et retournera une autre fonction. Il devrait donc vraiment rendre un décorateur normal. Un peu déroutant, non? Ce que je veux dire est:

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

Ici, vous pouvez en savoir plus sur le sujet - il est également possible de l'implémenter à l'aide d'objets appelables et cela est également expliqué ici.


56
Je me demande pourquoi GVR ne l'a pas implémenté en passant les paramètres comme arguments décorateurs ultérieurs après «fonction». «Yo dawg, je vous ai entendu comme des fermetures…» etc.
Michel Müller

3
> La fonction serait-elle le premier ou le dernier argument? Évidemment d'abord, puisque les paramètres sont une liste de paramètres de longueur variable. > C'est aussi bizarre que vous "appeliez" la fonction avec une signature différente de celle de la définition. Comme vous le faites remarquer, cela conviendrait assez bien en fait - c'est à peu près analogue à la façon dont une méthode de classe est appelée. Pour être plus clair, vous pourriez avoir quelque chose comme la convention décorateur (self_func, param1, ...). Mais notez: je ne préconise aucun changement ici, Python est trop loin pour cela et nous pouvons voir comment les changements de rupture ont fonctionné ..
Michel Müller

21
vous avez oublié des functools.wraps TRÈS UTILES pour décorer l'emballage :)
socketpair

10
Vous avez oublié le retour lors de l'appel de la fonction, c'estreturn function(*args, **kwargs)
formiaczek

36
Peut-être évident, mais juste au cas où: vous devez utiliser ce décorateur comme @decorator()et pas seulement @decorator, même si vous n'avez que des arguments optionnels.
Patrick Mevzek

327

Edit : pour une compréhension approfondie du modèle mental des décorateurs, jetez un œil à ce génial Pycon Talk. vaut bien les 30 minutes.

Une façon de penser les décorateurs avec des arguments est

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

Se traduit par

foo = decorator(foo)

Donc, si le décorateur avait des arguments,

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

Se traduit par

foo = decorator_with_args(arg)(foo)

decorator_with_args est une fonction qui accepte un argument personnalisé et qui renvoie le décorateur réel (qui sera appliqué à la fonction décorée).

J'utilise une astuce simple avec des partiels pour rendre mes décorateurs faciles

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

Mise à jour:

Ci-dessus, foodevientreal_decorator(foo)

Un effet de la décoration d'une fonction est que le nom fooest remplacé lors de la déclaration du décorateur. fooest "remplacé" par tout ce qui est retourné par real_decorator. Dans ce cas, un nouvel objet fonction.

Toutes fooles métadonnées de sont remplacées, notamment docstring et nom de fonction.

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

functools.wraps nous donne une méthode pratique pour "soulever" la docstring et le nom à la fonction retournée.

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
Votre réponse a parfaitement expliqué l'orthogonalité inhérente du décorateur, merci
zsf222

Pourriez-vous ajouter @functools.wraps?
Mr_and_Mrs_D

1
@Mr_and_Mrs_D, j'ai mis à jour le message avec un exemple avec functool.wraps. L'ajouter dans l'exemple peut embrouiller davantage les lecteurs.
srj

7
Qu'est-ce qui est argici!?
displayname

1
Comment allez-vous passer l'argument passé barà l'argument de real_decorator?
Chang Zhao

85

Je voudrais montrer une idée qui est à mon humble avis assez élégante. La solution proposée par t.dubrownik présente un motif qui est toujours le même: vous avez besoin de l'emballage à trois couches indépendamment de ce que fait le décorateur.

J'ai donc pensé que c'était un travail pour un méta-décorateur, c'est-à-dire un décorateur pour les décorateurs. Comme un décorateur est une fonction, il fonctionne en fait comme un décorateur ordinaire avec des arguments:

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

Cela peut être appliqué à un décorateur ordinaire afin d'ajouter des paramètres. Par exemple, disons que nous avons le décorateur qui double le résultat d'une fonction:

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)

Avec @parametrizednous pouvons construire un @multiplydécorateur générique ayant un paramètre

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

Classiquement, le premier paramètre d'un décorateur paramétré est la fonction, tandis que les arguments restants correspondront au paramètre du décorateur paramétré.

Un exemple d'utilisation intéressant pourrait être un décorateur assertif de type sécurisé:

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

Une dernière note: ici, je n'utilise pas functools.wrapsles fonctions wrapper, mais je recommanderais de l'utiliser tout le temps.


3
Je n'ai pas utilisé cela exactement, mais cela m'a aidé à comprendre le concept :) Merci!
mouckatron

J'ai essayé cela et j'ai eu quelques problèmes .
Jeff

@Jeff pourriez-vous partager avec nous le genre de problèmes que vous avez eus?
Dacav

Je l'avais lié sur ma question, et je l'ai compris ... Je devais appeler le @wrapsmien pour mon cas particulier.
Jeff

4
Oh mon garçon, j'ai perdu une journée entière à ce sujet. Heureusement, je suis tombé sur cette réponse (qui pourrait d'ailleurs être la meilleure réponse jamais créée sur Internet). Eux aussi utilisent votre @parametrizedastuce. Le problème que j'ai eu, c'est que j'ai oublié que la @syntaxe est égale aux appels réels (en quelque sorte, je le savais et je ne le savais pas en même temps que vous pouvez le comprendre à partir de ma question). Donc, si vous voulez traduire la @syntaxe en appels banals pour vérifier comment cela fonctionne, vous feriez mieux de la commenter temporairement d'abord ou vous finiriez par l'appeler deux fois et obtenir des résultats
mumbojumbo

79

Voici une version légèrement modifiée de la réponse de t.dubrownik . Pourquoi?

  1. En tant que modèle général, vous devez renvoyer la valeur de retour de la fonction d'origine.
  2. Cela change le nom de la fonction, ce qui pourrait affecter d'autres décorateurs / code.

Alors utilisez @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

Je suppose que votre problème consiste à transmettre des arguments à votre décorateur. C'est un peu délicat et pas simple.

Voici un exemple de la façon de procéder:

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

Tirages:

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

Voir l'article de Bruce Eckel pour plus de détails.


20
Méfiez-vous des cours de décoration. Ils ne fonctionnent sur les méthodes que si vous réinventez manuellement la logique des descripteurs de méthode d'instanciation.

9
delnan, vous voulez élaborer? Je n'ai eu à utiliser ce modèle qu'une seule fois, donc je n'ai encore rencontré aucun piège.
Ross Rogers

2
@RossRogers Je suppose que @delnan fait référence à des choses comme __name__lesquelles une instance de la classe décorateur n'aura pas?
jamesc

9
@jamesc Cela aussi, bien que ce soit relativement facile à résoudre. Le cas spécifique class Foo: @MyDec(...) def method(self, ...): blahauquel je faisais référence ne fonctionnait pas car Foo().methodil ne s'agirait pas d'une méthode liée et ne passerait pas selfautomatiquement. Cela aussi peut être corrigé, en créant MyDecun descripteur et en créant des méthodes liées __get__, mais c'est plus compliqué et beaucoup moins évident. En fin de compte, les cours de décoration ne sont pas aussi pratiques qu'ils le semblent.

2
@delnan J'aimerais que cette mise en garde soit mise en évidence. Je le frappe et je suis intéressé à voir une solution qui FONCTIONNE (plus impliquée et moins évidente).
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

Utilisation du décorateur

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

Puis le

adder(2,3)

produit

10

mais

adder('hi',3)

produit

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

Ceci est un modèle pour un décorateur de fonction qui ne nécessite pas ()si aucun paramètre ne doit être donné:

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

un exemple de ceci est donné ci-dessous:

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

Notez également que factor_or_func(ou tout autre paramètre) ne doit jamais se fait réaffectés dans wrapper().
norok2

1
Pourquoi devez-vous vous enregistrer locals()?
Shital Shah

@ShitalShah qui couvre le cas où le décorateur est utilisé sans ().
norok2

4

Dans mon cas, j'ai décidé de résoudre ce problème via un lambda à une ligne pour créer une nouvelle fonction de décorateur:

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

Une fois exécuté, il affiche:

Finished!
All Done!

Peut-être pas aussi extensible que d'autres solutions, mais cela a fonctionné pour moi.


Cela marche. Bien que oui, cela rend difficile la définition de la valeur pour le décorateur.
Arindam Roychowdhury

3

Écrire un décorateur qui fonctionne avec et sans paramètre est un défi car Python attend un comportement complètement différent dans ces deux cas! De nombreuses réponses ont essayé de contourner cela et ci-dessous est une amélioration de la réponse par @ norok2. Plus précisément, cette variation élimine l'utilisation de locals().

En suivant le même exemple que celui donné par @ 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

Jouez avec ce code .

Le hic est que l'utilisateur doit fournir des paires de paramètres clés et valeurs au lieu de paramètres positionnels et le premier paramètre est réservé.


2

Il est bien connu que les deux morceaux de code suivants sont presque équivalents:

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

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

Une erreur courante est de penser que @cache simplement l'argument le plus à gauche.

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

Il serait beaucoup plus facile d'écrire des décorateurs si ce qui précède est comme cela a @fonctionné. Malheureusement, ce n'est pas ainsi que les choses se font.


Considérons un décorateur Waitqui interrompt l'exécution du programme pendant quelques secondes. Si vous ne passez pas de temps d'attente, la valeur par défaut est 1 seconde. Les cas d'utilisation sont indiqués ci-dessous.

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

Quand Waita un argument, tel que @Wait(3), alors l'appel Wait(3) est exécuté avant qu'il ne se passe quoi que ce soit d'autre.

Autrement dit, les deux morceaux de code suivants sont équivalents

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

C'est un problème.

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

Une solution est présentée ci-dessous:

Commençons par créer la classe suivante 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)

Maintenant, nous pouvons écrire des choses comme:

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

Notez que:

  • dec n'accepte pas plusieurs arguments.
  • dec accepte uniquement la fonction à encapsuler.

    import inspect classe 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. call (Wait, args [0]) else: r = DelayedDecorator (Wait, * args, ** kwargs) else: r = DelayedDecorator (Wait, * args, ** kwargs) enfin: pass return r

    importation de la classe de temps Wait (métaclasse = 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 

Les deux morceaux de code suivants sont équivalents:

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

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

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

Nous pouvons imprimer "something"sur la console très lentement, comme suit:

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

Notes finales

Cela peut ressembler à beaucoup de code, mais vous n'avez pas à écrire les classes DelayedDecoratoret à PolyArgDecoratorMetachaque fois. Le seul code que vous devez écrire personnellement comme suit, qui est assez court:

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

définir cette "fonction décorateur" pour générer une fonction décoratrice personnalisée:

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

utilisez-le de cette façon:

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

1

Excellentes réponses ci-dessus. Celui-ci illustre également @wraps, qui prend la chaîne de doc et le nom de la fonction de la fonction d'origine et l'applique à la nouvelle version encapsulée:

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

Tirages:

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

Dans le cas où la fonction et le décorateur doivent prendre des arguments, vous pouvez suivre l'approche ci-dessous.

Par exemple, il y a un décorateur nommé decorator1qui prend un argument

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

func1(1, 2)

Maintenant, si l' decorator1argument doit être dynamique ou passé lors de l'appel de la fonction,

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


a = 1
b = 2
seconds = 10

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

Dans le code ci-dessus

  • seconds est l'argument pour decorator1
  • a, b sont les arguments de func1
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.