Question

J'aimerais créer un décorateur Python pouvant être utilisé 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?

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 syntaxe que je voudrais réaliser.

Était-ce utile?

La solution

Je sais que cette question est ancienne, mais certains commentaires sont nouveaux et, même si toutes les solutions viables sont essentiellement les mêmes, la plupart d’entre elles ne sont pas très propres ni faciles à lire.

Comme le dit la réponse de Thobe, le seul moyen 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 existe un seul argument qui soit appelable (NOTE: des vérifications supplémentaires seront nécessaires si votre décorateur ne prend qu'un argument et qu'il se trouve qu'il s'agit d'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 comme n'importe quel décorateur normal, vous renvoyez une version modifiée ou encapsulée de la fonction transmise.

Dans le deuxième cas, vous renvoyez un nouveau décorateur qui utilise en quelque sorte les informations transmises avec * args, ** kwargs.

C’est parfait, mais il faut écrire pour chaque décorateur que vous faites peut être assez ennuyant 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 à ça que servent les décorateurs!

En utilisant le décorateur suivant, nous pouvons le décompresser de manière à ce 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 travailleront avec et sans arguments, avec une mise en garde:

J'ai noté ci-dessus, mais je devrais répéter ici, le chèque de 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, il convient de le garder à l’esprit ou de le modifier 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

Autres conseils

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 devrez inclure la parenthèse:

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

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

Si vous utilisiez Python 3.0, vous pouvez utiliser des arguments avec un mot clé uniquement 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)

Dans Python 2.x, cela peut être émulé avec les astuces de 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

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

@redirected_output
def foo():
    ...

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

Vous devez détecter les deux cas, en utilisant par exemple le type du premier argument, et renvoyer en conséquence 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

Si vous utilisez la syntaxe @redirect_output (" output.log ") , redirect_output est appelé avec un seul argument " output.log " , et il doit renvoyer un décorateur acceptant la fonction à décorer en tant qu’argument. Lorsqu'il est utilisé en tant que @redirect_output , il est appelé directement avec la fonction à décorer en tant qu'argument.

Ou en d'autres termes: la syntaxe @ doit être suivie d'une expression dont le résultat est une fonction acceptant qu'une fonction soit décorée comme son unique 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") . Convolué, mais vrai: -)

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

Dans votre premier exemple:

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

la fonction redirect_output est appelée avec le argument donné, qui devrait renvoyer un décorateur fonction, qui est elle-même appelée avec foo en argument, qui (enfin!) devrait 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 de votre deuxième exemple ressemble à ceci:

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

Ainsi, vous pouvez 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 aller sauf si vous souhaitez utiliser une fonction en tant que argument à votre décorateur, auquel cas le décorateur supposera à tort qu'il n'a pas d'arguments. Il va aussi échouer 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 le La fonction décorateur est toujours appelée, même si elle n’a pas d’argument. Dans ce cas, votre deuxième exemple ressemblerait à ceci:

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

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

Je sais que c’est une vieille question, mais je n’aime vraiment aucune des techniques proposées et j’ai voulu ajouter une autre méthode. J'ai vu que django utilisait une méthode très propre dans leur décorateur_signal dans django.contrib.auth.decorators . Comme vous pouvez le voir dans le les documents du décorateur , 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 qu'il décorera, alors que s'il est appelé avec des arguments, function sera Aucun . .

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 que cette approche utilisée par Django est plus élégante et plus facile à comprendre que toutes les autres techniques proposées ici.

Plusieurs réponses ici abordent déjà votre problème bien. 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 le 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 géniales, je trouve cela étrange et j'aime bien pouvoir décorer simplement avec @decorator .

En ce qui concerne l'objectif de mission secondaire, la rediriger la sortie d'une fonction Dépassement de pile après .

Si vous souhaitez approfondir vos connaissances, consultez le chapitre 9 (Métaprogrammation) de Python Cookbook 3 , qui est librement disponible pour être lire en ligne .

Certains de ces documents sont en démonstration (et plus!) dans la superbe vidéo YouTube de Beazley, Métaprogrammation Python 3 .

Joyeux codage :)

En fait, le cas d’avertissement de 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 de meta_wrap à sécurité intrinsèque.

    @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

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

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

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

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

Construire 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...
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top