2009-03-17 9 views
53

Je voudrais créer un décorateur Python qui peut être utilisé soit avec des paramètres:Comment créer un décorateur Python pouvant être utilisé avec ou sans 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 cela du tout possible?

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

+0

L'aspect par défaut '@ redirect_output' est remarquablement non-informatif. Je dirais que c'est une mauvaise idée. Utilisez le premier formulaire et simplifiez votre vie. –

+0

question intéressante - jusqu'à ce que je l'ai vu et regardé la documentation, j'aurais supposé que @f était le même que @f(), et je pense toujours qu'il devrait être, pour être honnête (tous les arguments fournis seraient juste collé sur l'argument de la fonction) – rog

Répondre

33

Je sais que cette question est vieux, mais quelques-uns 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 propres ou faciles à lire.

Comme la réponse de thobe l'indique, la seule façon de gérer les deux cas est de vérifier les deux scénarios. Le plus simple est simplement de vérifier s'il y a un seul argument et il est callabe (NB: Les contrôles supplémentaires seront nécessaires si votre décorateur ne prend 1 argument et il arrive à ê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 un décorateur normal, renvoyer une version modifiée ou encapsulée de la fonction transmise. Dans le second cas, vous renvoyez un 'nouveau' décorateur qui utilise en quelque sorte les informations transmises avec * args, ** kwargs. C'est très bien, mais le fait de l'écrire pour chaque décorateur que vous faites peut être assez ennuyeux et pas aussi propre. Au lieu de cela, il serait bien de pouvoir modifier automagiquement nos décorateurs sans avoir à les réécrire ... mais c'est ce à quoi servent les décorateurs!

Utilisation du décorateur de décorateur suivant, nous pouvons deocrate 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 vont travailler et sans arguments, avec une réserve:

Je note ci-dessus mais je dois 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 l'appliquons maintenant à n'importe quel générateur, il faut le garder à l'esprit ou le modifier s'il est contredit.

Ce qui suit dé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 
+1

c'est tellement déconcentré et cool! – gooli

-2

En général, vous pouvez donner des arguments par défaut en Python ...

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

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

+0

les décorateurs sont des fonctions. arguments par défaut fonctionnent – Geo

+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 renvoyer une fonction, qui obtient alors la fonction décorée passée en paramètre. (Voir aussi le code thobes) – sth

-1

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

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

Miser 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... 
+0

cela ne peut pas être utilisé comme décorateur comme dans l'exemple '@redirect_output (" somewhere.log ") def foo()' dans la question. – ehabkost

29

En utilisant des arguments de mots-clés avec des valeurs par défaut (comme suggéré par kquinn) est une bonne idée, mais vous demandera d'inclure la parenthèse:

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

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

Si vous utilisez Python 3.0, vous pouvez utiliser uniquement des arguments mot-clé 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 trucs 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 

Toutes ces versions vous permettrait d'écrire le code comme ceci:

@redirected_output 
def foo(): 
    ... 

@redirected_output(destination="somewhere.log") 
def bar(): 
    ... 
+1

Qu'est-ce que vous mettez dans «votre code ici»? Comment appelez-vous la fonction qui est décorée? 'fn (* args, ** kwargs)' ne fonctionne pas. – lum

11

Vous devez détecter les deux cas, f ou exemple en utilisant le type du premier argument, et en conséquence retourner soit le wrapper (lorsqu'il est utilisé sans paramètre) ou 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 

Lorsque vous utilisez la syntaxe @redirect_output("output.log"), redirect_output est appelée avec un seul argument "output.log", et il doit retourner un décorateur accepter la fonction à décorer comme argument. Lorsqu'il est utilisé comme @redirect_output, il est appelé directement avec la fonction à décorer en tant qu'argument. Autrement dit: 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").Convivial, mais vrai :-)

8

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

Dans votre premier exemple:

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

la fonction redirect_output est appelée avec l'argument donné , qui devrait retourner une fonction décorateur , qui est lui-même appelé à foo comme argument, qui (enfin!) devrait retourner 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 à:

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

vous peut donc faire ce que vous souhaitez, mais pas dans un façon 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) 

d être ok sauf si vous souhaitez utiliser une fonction comme argument à votre décorateur, auquel cas le décorateur supposera à tort qu'il n'a pas d'arguments. Il échouera également si cette décoration est appliquée à une autre décoration que ne retourne pas un type de fonction.

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

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

Le code de fonction 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) 
5

Je sais que c'est une vieille question, mais je n'aime vraiment aucune des techniques proposées afin que je voulais ajouter une autre méthode. J'ai vu que django utilise une méthode vraiment propre dans leur login_required decorator in django.contrib.auth.decorators. Comme vous pouvez le voir dans le decorator's docs, il peut être utilisé seul comme @login_required ou 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, function sera la fonction réelle qu'il décorera, alors que s'il est appelé avec des arguments, function sera .

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 qui nous django Pour être plus élégant et plus facile à comprendre que n'importe quelle autre technique proposée ici.

1

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 ce fail-safe version 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 
+1

Merci. C'est utile! –

2

Plusieurs réponses ici gèrent bien votre problème. En ce qui concerne le style, cependant, je préfère résoudre cette situation de décorateur en utilisant functools.partial, comme suggéré dans Python David Beazley livre de recettes 3:

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 

Alors oui, vous pouvez juste faire

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

sans froussard solutions de contournement, 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 traitée dans cette Stack Overflow post.


Si vous voulez plonger plus profondément, consultez le chapitre 9 (Métaprogrammation) dans Python Cookbook 3, qui est librement disponible pour être read online.

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.

Bonne codage :)

Questions connexes