Comment créer un décorateur Python utilisable avec ou sans paramètres?
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.
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...