Comment créer un décorateur Python utilisable avec ou sans paramètres?

88

Je voudrais créer un décorateur Python qui peut être utilisé soit avec des paramètres:

@redirect_output("somewhere.log")
def foo():
    ....

ou sans eux (par exemple pour rediriger la sortie vers stderr par défaut):

@redirect_output
def foo():
    ....

Est-ce possible du tout?

Notez que je ne cherche pas une solution différente au problème de la redirection de la sortie, c'est juste un exemple de la syntaxe que j'aimerais obtenir.

elifiner
la source
L'apparence par défaut @redirect_outputest remarquablement peu informative. Je dirais que c'est une mauvaise idée. Utilisez le premier formulaire et simplifiez-vous beaucoup la vie.
S.Lott
question intéressante cependant - jusqu'à ce que je l'ai vu et parcouru la documentation, j'aurais supposé que @f était la même chose que @f (), et je pense toujours que cela devrait être, pour être honnête (tous les arguments fournis seraient simplement collés sur l'argument de la fonction)
rog

Réponses:

63

Je sais que cette question est ancienne, mais certains des commentaires sont nouveaux et, bien que toutes les solutions viables soient essentiellement les mêmes, la plupart d'entre elles ne sont pas très claires ou faciles à lire.

Comme le dit la réponse de Thobe, la seule façon de gérer les deux cas est de vérifier les deux scénarios. Le moyen le plus simple est simplement de vérifier s'il y a un seul argument et qu'il est callabe (NOTE: des vérifications supplémentaires seront nécessaires si votre décorateur ne prend qu'un seul argument et qu'il se trouve être un objet appelable):

def decorator(*args, **kwargs):
    if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
        # called as @decorator
    else:
        # called as @decorator(*args, **kwargs)

Dans le premier cas, vous faites ce que fait n'importe quel décorateur normal, renvoyez une version modifiée ou encapsulée de la fonction passée.

Dans le second cas, vous retournez un 'nouveau' décorateur qui utilise d'une manière ou d'une autre les informations transmises avec * args, ** kwargs.

C'est bien et tout, mais devoir l'écrire pour chaque décorateur que vous créez peut être assez ennuyeux et pas aussi propre. Au lieu de cela, ce serait bien de pouvoir modifier automatiquement nos décorateurs sans avoir à les réécrire ... mais c'est à cela que servent les décorateurs!

En utilisant le décorateur décorateur suivant, nous pouvons déocrate nos décorateurs afin qu'ils puissent être utilisés avec ou sans arguments:

def doublewrap(f):
    '''
    a decorator decorator, allowing the decorator to be used as:
    @decorator(with, arguments, and=kwargs)
    or
    @decorator
    '''
    @wraps(f)
    def new_dec(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # actual decorated function
            return f(args[0])
        else:
            # decorator arguments
            return lambda realf: f(realf, *args, **kwargs)

    return new_dec

Maintenant, nous pouvons décorer nos décorateurs avec @doublewrap, et ils fonctionneront avec et sans arguments, avec une mise en garde:

J'ai noté ci-dessus mais je dois le répéter ici, la vérification dans ce décorateur fait une hypothèse sur les arguments qu'un décorateur peut recevoir (à savoir qu'il ne peut pas recevoir un seul argument appelable). Puisque nous le rendons applicable à n'importe quel générateur maintenant, il doit être gardé à l'esprit, ou modifié s'il est contredit.

Ce qui suit montre son utilisation:

def test_doublewrap():
    from util import doublewrap
    from functools import wraps    

    @doublewrap
    def mult(f, factor=2):
        '''multiply a function's return value'''
        @wraps(f)
        def wrap(*args, **kwargs):
            return factor*f(*args,**kwargs)
        return wrap

    # try normal
    @mult
    def f(x, y):
        return x + y

    # try args
    @mult(3)
    def f2(x, y):
        return x*y

    # try kwargs
    @mult(factor=5)
    def f3(x, y):
        return x - y

    assert f(2,3) == 10
    assert f2(2,5) == 30
    assert f3(8,1) == 5*7
bj0
la source
31

L'utilisation d'arguments de mots clés avec des valeurs par défaut (comme suggéré par kquinn) est une bonne idée, mais vous obligera à inclure les parenthèses:

@redirect_output()
def foo():
    ...

Si vous souhaitez une version qui fonctionne sans les parenthèses sur le décorateur, vous devrez tenir compte des deux scénarios dans votre code décorateur.

Si vous utilisiez Python 3.0, vous pouvez utiliser uniquement des arguments de mots clés pour cela:

def redirect_output(fn=None,*,destination=None):
  destination = sys.stderr if destination is None else destination
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn is None:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator
  else:
    return functools.update_wrapper(wrapper, fn)

En Python 2.x, cela peut être émulé avec des astuces varargs:

def redirected_output(*fn,**options):
  destination = options.pop('destination', sys.stderr)
  if options:
    raise TypeError("unsupported keyword arguments: %s" % 
                    ",".join(options.keys()))
  def wrapper(*args, **kwargs):
    ... # your code here
  if fn:
    return functools.update_wrapper(wrapper, fn[0])
  else:
    def decorator(fn):
      return functools.update_wrapper(wrapper, fn)
    return decorator

Chacune de ces versions vous permettrait d'écrire du code comme celui-ci:

@redirected_output
def foo():
    ...

@redirected_output(destination="somewhere.log")
def bar():
    ...
thobe
la source
1
Que mettez-vous dedans your code here? Comment appelle-t-on la fonction décorée? fn(*args, **kwargs)ne fonctionne pas.
lum
Je pense qu'il y a une réponse beaucoup plus simple, créer une classe qui sera le décorateur avec des arguments optionnels. créer une autre fonction avec les mêmes arguments avec les valeurs par défaut et renvoyer une nouvelle instance des classes de décorateur. devrait ressembler à quelque chose comme: def f(a = 5): return MyDecorator( a = a) et class MyDecorator( object ): def __init__( self, a = 5 ): .... désolé, il est difficile de l'écrire dans un commentaire, mais j'espère que c'est assez simple à comprendre
Omer Ben Haim
17

Je sais que c'est une vieille question, mais je n'aime vraiment aucune des techniques proposées donc j'ai voulu ajouter une autre méthode. J'ai vu que django utilise une méthode vraiment propre dans leur login_requireddécorateurdjango.contrib.auth.decorators . Comme vous pouvez le voir dans la documentation du décorateur , il peut être utilisé seul comme @login_requiredou avec des arguments,@login_required(redirect_field_name='my_redirect_field') .

La façon dont ils le font est assez simple. Ils ajoutent un kwarg( function=None) avant leurs arguments de décorateur. Si le décorateur est utilisé seul, ce functionsera la fonction réelle qu'il décore, alors que s'il est appelé avec des arguments, le functionsera None.

Exemple:

from functools import wraps

def custom_decorator(function=None, some_arg=None, some_other_arg=None):
    def actual_decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            # Do stuff with args here...
            if some_arg:
                print(some_arg)
            if some_other_arg:
                print(some_other_arg)
            return f(*args, **kwargs)
        return wrapper
    if function:
        return actual_decorator(function)
    return actual_decorator

@custom_decorator
def test1():
    print('test1')

>>> test1()
test1

@custom_decorator(some_arg='hello')
def test2():
    print('test2')

>>> test2()
hello
test2

@custom_decorator(some_arg='hello', some_other_arg='world')
def test3():
    print('test3')

>>> test3()
hello
world
test3

Je trouve cette approche que django utilise pour être plus élégante et plus facile à comprendre que toutes les autres techniques proposées ici.

dgel
la source
Ouais, j'aime cette méthode. Notez que vous devez utiliser kwargs lorsque vous appelez le décorateur, sinon le premier arg positionnel est assigné à function, puis les choses se cassent parce que le décorateur essaie d'appeler ce premier argument positionnel comme s'il s'agissait de votre fonction décorée.
Dustin Wyatt
12

Vous devez détecter les deux cas, par exemple en utilisant le type du premier argument, et en conséquence renvoyer soit le wrapper (lorsqu'il est utilisé sans paramètre) soit un décorateur (lorsqu'il est utilisé avec des arguments).

from functools import wraps
import inspect

def redirect_output(fn_or_output):
    def decorator(fn):
        @wraps(fn)
        def wrapper(*args, **args):
            # Redirect output
            try:
                return fn(*args, **args)
            finally:
                # Restore output
        return wrapper

    if inspect.isfunction(fn_or_output):
        # Called with no parameter
        return decorator(fn_or_output)
    else:
        # Called with a parameter
        return decorator

Lors de l'utilisation de la @redirect_output("output.log")syntaxe, redirect_outputest appelée avec un seul argument "output.log", et elle doit renvoyer un décorateur acceptant la fonction à décorer comme argument. Lorsqu'il est utilisé comme@redirect_output , il est appelé directement avec la fonction à décorer comme argument.

Ou en d'autres termes: la @syntaxe doit être suivie d'une expression dont le résultat est une fonction acceptant une fonction à décorer comme seul argument, et renvoyant la fonction décorée. L'expression elle-même peut être un appel de fonction, ce qui est le cas avec @redirect_output("output.log"). Alambiqué, mais vrai :-)

Remy Blank
la source
9

Plusieurs réponses ici résolvent déjà bien votre problème. En ce qui concerne le style, cependant, je préfère résoudre ce problème de décorateur en utilisant functools.partial, comme suggéré dans Python Cookbook 3 de David Beazley :

from functools import partial, wraps

def decorator(func=None, foo='spam'):
    if func is None:
         return partial(decorator, foo=foo)

    @wraps(func)
    def wrapper(*args, **kwargs):
        # do something with `func` and `foo`, if you're so inclined
        pass

    return wrapper

Bien que oui, vous pouvez simplement faire

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

sans solutions de contournement géniales, je trouve cela étrange et j'aime avoir la possibilité de simplement décorer avec @decorator.

En ce qui concerne l'objectif de la mission secondaire, la redirection de la sortie d'une fonction est abordée dans ce post de débordement de pile .


Si vous souhaitez approfondir, consultez le chapitre 9 (Métaprogrammation) dans Python Cookbook 3 , qui est disponible gratuitement pour être lu en ligne .

Une partie de ce matériel est démo en direct (et plus encore!) Dans la superbe vidéo YouTube de Beazley Python 3 Metaprogramming .

Bon codage :)

Henrywallace
la source
8

Un décorateur python est appelé d'une manière fondamentalement différente selon que vous lui donnez des arguments ou non. La décoration n'est en fait qu'une expression (syntaxiquement restreinte).

Dans votre premier exemple:

@redirect_output("somewhere.log")
def foo():
    ....

la fonction redirect_outputest appelée avec l'argument donné, qui est censé renvoyer une fonction décoratrice, elle-même appelée avecfoo comme argument, qui (enfin!) est censée renvoyer la fonction décorée finale.

Le code équivalent ressemble à ceci:

def foo():
    ....
d = redirect_output("somewhere.log")
foo = d(foo)

Le code équivalent pour votre deuxième exemple ressemble à ceci:

def foo():
    ....
d = redirect_output
foo = d(foo)

Vous pouvez donc faire ce que vous voulez mais pas de manière totalement transparente:

import types
def redirect_output(arg):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    if type(arg) is types.FunctionType:
        return decorator(sys.stderr, arg)
    return lambda f: decorator(arg, f)

Cela devrait être correct à moins que vous ne souhaitiez utiliser une fonction comme argument de votre décorateur, auquel cas le décorateur supposera à tort qu'il n'a pas d'arguments. Elle échouera également si cette décoration est appliquée à une autre décoration qui ne renvoie pas de type de fonction.

Une autre méthode consiste simplement à exiger que la fonction décoratrice soit toujours appelée, même si elle ne contient aucun argument. Dans ce cas, votre deuxième exemple ressemblerait à ceci:

@redirect_output()
def foo():
    ....

Le code de fonction du décorateur ressemblerait à ceci:

def redirect_output(file = sys.stderr):
    def decorator(file, f):
        def df(*args, **kwargs):
            print 'redirecting to ', file
            return f(*args, **kwargs)
        return df
    return lambda f: decorator(file, f)
rog
la source
2

En fait, le cas de mise en garde dans la solution de @ bj0 peut être vérifié facilement:

def meta_wrap(decor):
    @functools.wraps(decor)
    def new_decor(*args, **kwargs):
        if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
            # this is the double-decorated f. 
            # Its first argument should not be a callable
            doubled_f = decor(args[0])
            @functools.wraps(doubled_f)
            def checked_doubled_f(*f_args, **f_kwargs):
                if callable(f_args[0]):
                    raise ValueError('meta_wrap failure: '
                                'first positional argument cannot be callable.')
                return doubled_f(*f_args, **f_kwargs)
            return checked_doubled_f 
        else:
            # decorator arguments
            return lambda real_f: decor(real_f, *args, **kwargs)

    return new_decor

Voici quelques cas de test pour cette version à sécurité intégrée de meta_wrap.

    @meta_wrap
    def baddecor(f, caller=lambda x: -1*x):
        @functools.wraps(f)
        def _f(*args, **kwargs):
            return caller(f(args[0]))
        return _f

    @baddecor  # used without arg: no problem
    def f_call1(x):
        return x + 1
    assert f_call1(5) == -6

    @baddecor(lambda x : 2*x) # bad case
    def f_call2(x):
        return x + 1
    f_call2(5)  # raises ValueError

    # explicit keyword: no problem
    @baddecor(caller=lambda x : 100*x)
    def f_call3(x):
        return x + 1
    assert f_call3(5) == 600
Ainz Titor
la source
1
Merci. Ceci est utile!
Pragy Agarwal
0

Pour donner une réponse plus complète que celle ci-dessus:

"Existe-t-il un moyen de créer un décorateur qui puisse être utilisé avec et sans arguments?"

Non, il n'y a pas de moyen générique car il manque actuellement quelque chose dans le langage python pour détecter les deux cas d'utilisation différents.

Cependant Oui, comme déjà souligné par d'autres réponses telles que bj0s , il existe une solution de contournement maladroite qui consiste à vérifier le type et la valeur du premier argument positionnel reçu (et à vérifier si aucun autre argument n'a une valeur par défaut). Si vous êtes assuré que les utilisateurs ne passeront un appelable comme premier argument de votre décorateur, vous pouvez utiliser cette solution de contournement. Notez que c'est la même chose pour les décorateurs de classe (remplacez callable par class dans la phrase précédente).

Pour être sûr de ce qui précède, j'ai fait pas mal de recherches et même implémenté une bibliothèque nommée decopatchqui utilise une combinaison de toutes les stratégies citées ci-dessus (et bien d'autres, y compris l'introspection) pour effectuer "quelle que soit la solution de contournement la plus intelligente" en fonction sur votre besoin.

Mais franchement, le mieux serait de ne pas avoir besoin de bibliothèque ici et d'obtenir cette fonctionnalité directement à partir du langage python. Si, comme moi, vous pensez qu'il est dommage que le langage python ne soit pas à ce jour capable d'apporter une réponse soignée à cette question, n'hésitez pas à soutenir cette idée dans le bugtracker python : https: //bugs.python .org / issue36553 !

Merci beaucoup pour votre aide à faire de Python un meilleur langage :)

smarie
la source
0

Cela fait le travail sans aucun problème:

from functools import wraps

def memoize(fn=None, hours=48.0):
  def deco(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
      return fn(*args, **kwargs)
    return wrapper

  if callable(fn): return deco(fn)
  return deco
j4hangir
la source
0

Comme personne ne l'a mentionné, il existe également une solution utilisant une classe appelable que je trouve plus élégante, en particulier dans les cas où le décorateur est complexe et que l'on peut souhaiter le diviser en plusieurs méthodes (fonctions). Cette solution utilise __new__une méthode magique pour faire essentiellement ce que d'autres ont souligné. Détectez d'abord comment le décorateur a été utilisé, puis ajustez le retour de manière appropriée.

class decorator_with_arguments(object):

    def __new__(cls, decorated_function=None, **kwargs):

        self = super().__new__(cls)
        self._init(**kwargs)

        if not decorated_function:
            return self
        else:
            return self.__call__(decorated_function)

    def _init(self, arg1="default", arg2="default", arg3="default"):
        self.arg1 = arg1
        self.arg2 = arg2
        self.arg3 = arg3

    def __call__(self, decorated_function):

        def wrapped_f(*args):
            print("Decorator arguments:", self.arg1, self.arg2, self.arg3)
            print("decorated_function arguments:", *args)
            decorated_function(*args)

        return wrapped_f

@decorator_with_arguments(arg1=5)
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments()
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

@decorator_with_arguments
def sayHello(a1, a2, a3, a4):
    print('sayHello arguments:', a1, a2, a3, a4)

Si le décorateur est utilisé avec des arguments, cela équivaut à:

result = decorator_with_arguments(arg1=5)(sayHello)(a1, a2, a3, a4)

On voit que les arguments arg1sont correctement passés au constructeur et la fonction décorée est passée à__call__

Mais si le décorateur est utilisé sans arguments, cela équivaut à:

result = decorator_with_arguments(sayHello)(a1, a2, a3, a4)

Vous voyez que dans ce cas, la fonction décorée est passée directement au constructeur et l'appel à __call__est entièrement omis. C'est pourquoi nous devons utiliser la logique pour traiter ce cas en __new__méthode magique.

Pourquoi ne pouvons-nous pas utiliser à la __init__place de __new__? La raison est simple: python interdit de renvoyer des valeurs autres que None de__init__

ATTENTION

Cette approche a un effet secondaire. Cela ne conservera pas la signature de la fonction!

Majo
la source
-1

Avez-vous essayé des arguments de mots clés avec des valeurs par défaut? Quelque chose comme

def decorate_something(foo=bar, baz=quux):
    pass
kquinn
la source
-2

Généralement, vous pouvez donner des arguments par défaut en Python ...

def redirect_output(fn, output = stderr):
    # whatever

Je ne sais pas si cela fonctionne également avec les décorateurs. Je ne connais aucune raison pour laquelle ce ne serait pas le cas.

David Z
la source
2
Si vous dites @dec (abc), la fonction n'est pas transmise directement à dec. dec (abc) renvoie quelque chose, et cette valeur de retour est utilisée comme décorateur. Donc dec (abc) doit retourner une fonction, qui obtient alors la fonction décorée passée en paramètre. (Voir aussi le code des thobes)
qc
-2

S'appuyant sur la réponse de vartec:

imports sys

def redirect_output(func, output=None):
    if output is None:
        output = sys.stderr
    if isinstance(output, basestring):
        output = open(output, 'w') # etc...
    # everything else...
muhuk
la source
cela ne peut pas être utilisé comme décorateur comme dans l' @redirect_output("somewhere.log") def foo()exemple de la question.
ehabkost