119 votes

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

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 j'aimerais réaliser.

94voto

bj0 Points 825

Je sais que cette question est ancienne, mais certains commentaires sont nouveaux, et alors que toutes les autres solutions viables sont essentiellement les mêmes, la plupart d'entre eux ne sont pas très propre et facile à lire.

Comme thobe la réponse dit, la seule façon de gérer les deux cas est de vérifier pour les deux scénarios. La façon la plus simple est tout simplement de vérifier pour voir si il n'y a qu'un seul argument et il est callabe (REMARQUE: les contrôles supplémentaires seront nécessaires si votre décorateur prend seulement 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 tout le décorateur n', de retour d'une modification ou enveloppé version de la réussite dans la fonction.

Dans le second cas, vous revenez à un "nouveau" décorateur qui utilise en quelque sorte les informations transmises avec *args, **kwargs.

C'est très bien et tout, mais d'avoir à l'écrire pour chaque décorateur peut être assez ennuyeux et pas aussi propre. Au lieu de cela, il serait agréable d'être en mesure de modifier automatiquement nos décorateurs sans avoir à ré-écrire... mais c'est ce que les décorateurs sont pour!

Suivant le décorateur le décorateur, nous pouvons deocrate nos décorateurs de sorte qu'ils peuvent ê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 avec et sans arguments, avec une mise en garde:

Je l'ai noté ci-dessus, mais doit se répéter ici, les contrôles dans ce décorateur de fait une hypothèse sur les arguments qu'un décorateur peut recevoir (à savoir qu'il ne peut pas recevoir un seul, rachetable argument). Puisque nous sommes le rendre applicable à tout générateur de maintenant, il doit être gardé à l'esprit, ou modifiée si elle va être 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

36voto

thobe Points 985

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

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

Si vous souhaitez une version qui fonctionne sans la parenthèse sur le décorateur, vous aurez à rendre compte à la fois des scénarios dans votre décorateur code.

Si vous étiez à l'aide de Python 3.0 vous pouvez utiliser les mots clés arguments 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ée avec varargs astuces:

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

L'une de ces versions vous permettent d'écrire du code comme ceci:

@redirected_output
def foo():
    ...

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

30voto

dgel Points 5372

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

La façon de le faire est assez simple. Ils ajoutent kwarg (function=None) avant leur décorateur d'arguments. Si le décorateur est utilisé seul, function sera de la fonction réelle c'est de la décoration, tandis que si elle est appelée avec des arguments, function sera 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 la plus facile à comprendre que toutes les autres techniques proposées ici.

13voto

Remy Blank Points 2214

Vous avez besoin de détecter les deux cas, par exemple en utilisant le type du premier argument, et en conséquence retour de l'emballage (lorsqu'elle est utilisée sans paramètre) ou d'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 l' @redirect_output("output.log") de la syntaxe, redirect_output est appelée avec un seul argument "output.log", et il doit retourner un décorateur d'accepter la fonction d'être décoré comme un argument. Lorsqu'il est utilisé comme @redirect_output, il est appelé directement avec la fonction d'être décoré comme un argument.

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

8voto

rog Points 1304

Un python décorateur est appelé d'une façon fondamentalement différente selon que vous donnez des arguments ou pas. La décoration est en fait juste un (syntaxiquement restreint) de l'expression.

Dans votre premier exemple:

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

la fonction redirect_output est appelée avec le l'argument donné, qui est prévue pour retourner un décorateur la fonction, qui elle-même est appelée avec foo comme argument, qui (enfin!) retour est prévu le final décoré de la fonction.

L'équivalent du code 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)

De sorte que vous pouvez faire ce que vous voulez mais pas d'une 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)

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

Une autre méthode consiste simplement à exiger que la décorateur fonction est toujours appelé, même si c'est avec pas d'arguments. Dans ce cas, votre deuxième exemple ressembler à ceci:

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

La fonction décorateur code devrait ressembler à 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)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X