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.
@redirect_output
est remarquablement peu informative. Je dirais que c'est une mauvaise idée. Utilisez le premier formulaire et simplifiez-vous beaucoup la vie.Réponses:
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
la source
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(): ...
la source
your code here
? Comment appelle-t-on la fonction décorée?fn(*args, **kwargs)
ne fonctionne pas.def f(a = 5): return MyDecorator( a = a)
etclass 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 à comprendreJe 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_required
décorateurdjango.contrib.auth.decorators
. Comme vous pouvez le voir dans la documentation 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, cefunction
sera la fonction réelle qu'il décore, alors que s'il est appelé avec des arguments, lefunction
seraNone
.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.
la source
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.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_output
est 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 :-)la source
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 :)
la source
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_output
est 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)
la source
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
la source
Pour donner une réponse plus complète que celle ci-dessus:
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
bj0
s , 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
decopatch
qui 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 :)
la source
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
la source
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
arg1
sont 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 à:
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!
la source
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
la source
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.
la source
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...
la source
@redirect_output("somewhere.log") def foo()
exemple de la question.