Faire des décorateurs avec des arguments optionnels
from functools import wraps
def foo_register(method_name=None):
"""Does stuff."""
def decorator(method):
if method_name is None:
method.gw_method = method.__name__
else:
method.gw_method = method_name
@wraps(method)
def wrapper(*args, **kwargs):
method(*args, **kwargs)
return wrapper
return decorator
Exemple: Ce qui suit décore my_function
avec foo_register
au lieu de passer à decorator
.
@foo_register
def my_function():
print('hi...')
Exemple: Ce qui suit fonctionne comme prévu.
@foo_register('say_hi')
def my_function():
print('hi...')
Si je veux que cela fonctionne correctement dans les deux applications (une avec method.__name__
et une en passant le nom), je dois vérifier dans foo_register
pour voir si le premier argument est un décorateur, et si oui, je dois: return decorator(method_name)
(à la place de return decorator
). Ce genre de "vérification pour voir si c'est un callable" semble très hack. Existe-t-il un moyen plus agréable de créer un décorateur multi-usages comme celui-ci?
P.S. Je sais déjà que je peux exiger que le décorateur soit appelé, mais ce n'est pas une "solution". Je veux que l'API se sente naturelle. Ma femme adore décorer et je ne veux pas gâcher ça.
Glenn - Je devais le faire alors. Je suppose que je suis heureux qu'il n'y ait pas de moyen "magique" de le faire. Je les déteste.
Donc, voici ma propre réponse (noms de méthodes différents des précédents, mais concept identique):
from functools import wraps
def register_gw_method(method_or_name):
"""Cool!"""
def decorator(method):
if callable(method_or_name):
method.gw_method = method.__name__
else:
method.gw_method = method_or_name
@wraps(method)
def wrapper(*args, **kwargs):
method(*args, **kwargs)
return wrapper
if callable(method_or_name):
return decorator(method_or_name)
return decorator
Exemple d'utilisation (les deux versions fonctionnent de la même manière):
@register_gw_method
def my_function():
print('hi...')
@register_gw_method('say_hi')
def my_function():
print('hi...')
Le moyen le plus propre que je connaisse pour ce faire est le suivant:
import functools
def decorator(original_function=None, optional_argument1=None, optional_argument2=None, ...):
def _decorate(function):
@functools.wraps(function)
def wrapped_function(*args, **kwargs):
...
return wrapped_function
if original_function:
return _decorate(original_function)
return _decorate
Explication
Lorsque le décorateur est appelé sans arguments optionnels comme celui-ci:
@decorator
def function ...
La fonction est passée en tant que premier argument et decorate renvoie la fonction décorée, comme prévu.
Si le décorateur est appelé avec un ou plusieurs arguments optionnels comme celui-ci:
@decorator(optional_argument1='some value')
def function ....
Ensuite, decorator est appelé avec l'argument de fonction avec la valeur None. Une fonction de décoration est renvoyée, comme prévu.
Python 3
Notez que la signature du décorateur ci-dessus peut être améliorée avec la syntaxe *,
spécifique à Python 3 pour imposer l'utilisation sécurisée des arguments de mots clés. Remplacez simplement la signature de la fonction la plus externe par:
def decorator(original_function=None, *, optional_argument1=None, optional_argument2=None, ...):
Grâce aux réponses fournies ici et ailleurs et à une série d’essais et d’erreurs, j’ai trouvé qu’il existe un moyen beaucoup plus simple et générique de faire prendre aux arguments des arguments optionnels. Il vérifie les arguments avec lesquels il a été appelé, mais il n'y a pas d'autre moyen de le faire.
La clé est de décorer votre décorateur.
Code générique de décorateur
Voici le décorateur décorateur (ce code est générique et peut être utilisé par toute personne ayant besoin d'un décorateur optionnel arg)}:
def optional_arg_decorator(fn):
def wrapped_decorator(*args):
if len(args) == 1 and callable(args[0]):
return fn(args[0])
else:
def real_decorator(decoratee):
return fn(decoratee, *args)
return real_decorator
return wrapped_decorator
Usage
Son utilisation est aussi simple que:
- Créez un décorateur comme d'habitude.
- Après le premier argument de la fonction cible, ajoutez vos arguments facultatifs.
- Décorer le décorateur avec
optional_arg_decorator
Exemple:
@optional_arg_decorator
def example_decorator_with_args(fn, optional_arg = 'Default Value'):
...
return fn
Cas de test
Pour votre cas d'utilisation:
Donc, dans votre cas, pour enregistrer un attribut sur la fonction avec le nom de méthode transmis ou le __name__
si None:
@optional_arg_decorator
def register_method(fn, method_name = None):
fn.gw_method = method_name or fn.__name__
return fn
Ajouter des méthodes décorées
Vous avez maintenant un décorateur utilisable avec ou sans arguments:
@register_method('Custom Name')
def custom_name():
pass
@register_method
def default_name():
pass
assert custom_name.gw_method == 'Custom Name'
assert default_name.gw_method == 'default_name'
print 'Test passes :)'
Que diriez-vous
from functools import wraps, partial
def foo_register(method=None, string=None):
if not callable(method):
return partial(foo_register, string=method)
method.gw_method = string or method.__name__
@wraps(method)
def wrapper(*args, **kwargs):
method(*args, **kwargs)
return wrapper
Code générique amélioré de décorateur
Voici mon adaptation de @ NickC's answer avec les améliorations suivantes:
- les kwargs facultatifs peuvent être passés au décorateur décoré
- le décorateur décoré peut être une méthode liée
import functools
def optional_arg_decorator(fn):
@functools.wraps(fn)
def wrapped_decorator(*args, **kwargs):
is_bound_method = hasattr(args[0], fn.__name__) if args else False
if is_bound_method:
klass = args[0]
args = args[1:]
# If no arguments were passed...
if len(args) == 1 and len(kwargs) == 0 and callable(args[0]):
if is_bound_method:
return fn(klass, args[0])
else:
return fn(args[0])
else:
def real_decorator(decoratee):
if is_bound_method:
return fn(klass, decoratee, *args, **kwargs)
else:
return fn(decoratee, *args, **kwargs)
return real_decorator
return wrapped_decorator
Maintenant que ce vieux fil est de retour au sommet de toute façon, lemme suffit de rajouter une certaine décoratrice:
def magical_decorator(decorator):
@wraps(decorator)
def inner(*args, **kw):
if len(args) == 1 and not kw and callable(args[0]):
return decorator()(args[0])
else:
return decorator(*args, **kw)
return inner
Maintenant, votre décorateur magique n'est plus qu'à une seule ligne!
@magical_decorator
def foo_register(...):
# bla bla
En passant, cela fonctionne pour tout décorateur. Cela provoque simplement que @foo
se comporte (aussi près que possible) comme @foo()
.
Un décorateur générique pour les définitions de décorateur, exprimant que ce dernier accepte les arguments par défaut, qui sont définis si aucun n'est explicitement fourni.
from functools import wraps
def default_arguments(*default_args, **default_kwargs):
def _dwrapper(decorator):
@wraps(decorator)
def _fwrapper(*args, **kwargs):
if callable(args[0]) and len(args) == 1 and not kwargs:
return decorator(*default_args, **default_kwargs)(args[0])
return decorator(*args, **kwargs)
return _fwrapper
return _dwrapper
Il peut être utilisé de deux manières.
from functools import lru_cache # memoization decorator from Python 3
# apply decorator to decorator post definition
lru_cache = (default_arguments(maxsize=100)) (lru_cache)
# could also be:
# @default_arguments(maxsize=100)
# class lru_cache(object):
# def __init__(self, maxsize):
# ...
# def __call__(self, wrapped_function):
# ...
@lru_cache # this works
def fibonacci(n):
...
@lru_cache(200) # this also works
def fibonacci(n):
...
Si vous voulez cette fonctionnalité sur plusieurs décorateurs, vous pouvez échapper au code standard avec un décorateur pour un décorateur:
from functools import wraps
import inspect
def decorator_defaults(**defined_defaults):
def decorator(f):
args_names = inspect.getargspec(f)[0]
def wrapper(*new_args, **new_kwargs):
defaults = dict(defined_defaults, **new_kwargs)
if len(new_args) == 0:
return f(**defaults)
Elif len(new_args) == 1 and callable(new_args[0]):
return f(**defaults)(new_args[0])
else:
too_many_args = False
if len(new_args) > len(args_names):
too_many_args = True
else:
for i in range(len(new_args)):
arg = new_args[i]
arg_name = args_names[i]
defaults[arg_name] = arg
if len(defaults) > len(args_names):
too_many_args = True
if not too_many_args:
final_defaults = []
for name in args_names:
final_defaults.append(defaults[name])
return f(*final_defaults)
if too_many_args:
raise TypeError("{0}() takes {1} argument(s) "
"but {2} were given".
format(f.__name__,
len(args_names),
len(defaults)))
return wrapper
return decorator
@decorator_defaults(start_val="-=[", end_val="]=-")
def my_text_decorator(start_val, end_val):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
return "".join([f.__name__, ' ', start_val,
f(*args, **kwargs), end_val])
return wrapper
return decorator
@decorator_defaults(end_val="]=-")
def my_text_decorator2(start_val, end_val):
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
return "".join([f.__name__, ' ', start_val,
f(*args, **kwargs), end_val])
return wrapper
return decorator
@my_text_decorator
def func1a(value):
return value
@my_text_decorator()
def func2a(value):
return value
@my_text_decorator2("-=[")
def func2b(value):
return value
@my_text_decorator(end_val=" ...")
def func3a(value):
return value
@my_text_decorator2("-=[", end_val=" ...")
def func3b(value):
return value
@my_text_decorator("|> ", " <|")
def func4a(value):
return value
@my_text_decorator2("|> ", " <|")
def func4b(value):
return value
@my_text_decorator(end_val=" ...", start_val="|> ")
def func5a(value):
return value
@my_text_decorator2("|> ", end_val=" ...")
def func5b(value):
return value
print(func1a('My sample text')) # func1a -=[My sample text]=-
print(func2a('My sample text')) # func2a -=[My sample text]=-
print(func2b('My sample text')) # func2b -=[My sample text]=-
print(func3a('My sample text')) # func3a -=[My sample text ...
print(func3b('My sample text')) # func3b -=[My sample text ...
print(func4a('My sample text')) # func4a |> My sample text <|
print(func4b('My sample text')) # func4b |> My sample text <|
print(func5a('My sample text')) # func5a |> My sample text ...
print(func5b('My sample text')) # func5b |> My sample text ...
Note: cela a l’inconvénient que vous ne pouvez pas passer 1 argument en tant que fonction au décorateur.
Note2: si vous avez des conseils/astuces pour améliorer ce décorateur, vous pouvez commenter en passant en revue le code: https://codereview.stackexchange.com/questions/78829/python-decorator-for-optional-arguments-decorator
Voici une autre solution qui fonctionne également si l'argument optionnel est appelable:
def test_equal(func=None, optional_value=None):
if func is not None and optional_value is not None:
# prevent user to set func parameter manually
raise ValueError("Don't set 'func' parameter manually")
if optional_value is None:
optional_value = 10 # The default value (if needed)
def inner(function):
def func_wrapper(*args, **kwargs):
# do something
return function(*args, **kwargs) == optional_value
return func_wrapper
if not func:
return inner
return inner(func)
De cette façon, les deux syntaxes fonctionneront:
@test_equal
def does_return_10():
return 10
@test_equal(optional_value=20)
def does_return_20():
return 20
# does_return_10() return True
# does_return_20() return True
J'ai été incroyablement ennuyé par ce problème et j'ai finalement écrit une bibliothèque pour le résoudre: decopatch .
Il prend en charge deux styles de développement: nested (comme dans les usines de décorateurs Python) et flat (un niveau d'imbrication de moins). Voici comment votre exemple serait implémenté en mode plat:
from decopatch import function_decorator, DECORATED
from makefun import wraps
@function_decorator
def foo_register(method_name=None, method=DECORATED):
if method_name is None:
method.gw_method = method.__name__
else:
method.gw_method = method_name
# create a signature-preserving wrapper
@wraps(method)
def wrapper(*args, **kwargs):
method(*args, **kwargs)
return wrapper
Notez que j'utilise makefun.wraps au lieu de functools.wraps
ici pour que la signature soit entièrement préservée (le wrapper n'est pas appelé du tout si les arguments ne sont pas valides).
decopatch
prend en charge un style de développement supplémentaire, que j'appelle double-flat , dédié à la création d'encapsuleurs de fonctions préservant la signature comme celui-ci. Votre exemple serait implémenté comme ceci:
from decopatch import function_decorator, WRAPPED, F_ARGS, F_KWARGS
@function_decorator
def foo_register(method_name=None,
method=WRAPPED, f_args=F_ARGS, f_kwargs=F_KWARGS):
# this is directly the wrapper
if method_name is None:
method.gw_method = method.__name__
else:
method.gw_method = method_name
method(*f_args, **f_kwargs)
Notez que dans ce style, tout votre code est exécuté dans des appels à method
. Cela peut ne pas être souhaitable - vous voudrez peut-être effectuer des choses une fois au moment de la décoration seulement - pour cela, le style précédent serait meilleur.
Vous pouvez vérifier que les deux styles fonctionnent:
@foo_register
def my_function():
print('hi...')
@foo_register('say_hi')
def my_function():
print('hi...')
Veuillez vérifier la documentation pour plus de détails.
Voici une autre variante assez concise qui n’utilise pas functools:
def decorator(*args, **kwargs):
def inner_decorator(fn, foo=23, bar=42, abc=None):
'''Always passed <fn>, the function to decorate.
# Do whatever decorating is required.
...
if len(args)==1 and len(kwargs)==0 and callable(args[0]):
return inner_decorator(args[0])
else:
return lambda fn: inner_decorator(fn, *args, **kwargs)
Selon que inner_decorator
peut être appelé avec un seul paramètre, on peut alors faire @decorator
, @decorator()
, @decorator(24)
etc.
Ceci peut être généralisé à un 'décorateur décorateur':
def make_inner_decorator(inner_decorator):
def decorator(*args, **kwargs):
if len(args)==1 and len(kwargs)==0 and callable(args[0]):
return inner_decorator(args[0])
else:
return lambda fn: inner_decorator(fn, *args, **kwargs)
return decorator
@make_inner_decorator
def my_decorator(fn, a=34, b='foo'):
...
@my_decorator
def foo(): ...
@my_decorator()
def foo(): ...
@my_decorator(42)
def foo(): ...
Une solution similaire à celle vérifiant le type et la longueur des arguments à l'aide de classes appelables
class decor(object):
def __init__(self, *args, **kwargs):
self.decor_args = args
self.decor_kwargs = kwargs
def __call__(self, *call_args, **call_kwargs):
if callable(self.decor_args[0]) and len(self.decor_args) == 1:
func = self.decor_args[0]
return self.__non_param__call__(func, call_args, call_kwargs)
else:
func = call_args[0]
return self.__param__call__(func)
def __non_param__call__(self, func, call_args, call_kwargs):
print "No args"
return func(*call_args, **call_kwargs)
def __param__call__(self, func):
def wrapper(*args, **kwargs):
print "With Args"
return func(*args, **kwargs)
return wrapper
@decor(a)
def test1(a):
print 'test' + a
@decor
def test2(b):
print 'test' + b
J'ai fait un paquet simple pour résoudre le problème
Installation
Branche principale
pip install git+https://github.com/ferrine/biwrap
Dernière version
pip install biwrap
Vue d'ensemble
Certains wrappers peuvent avoir des arguments optionnels et nous voulons souvent éviter les appels @wrapper()
et utiliser plutôt @wrapper
.
Cela fonctionne pour un simple emballage
import biwrap
@biwrap.biwrap
def hiwrap(fn, hi=True):
def new(*args, **kwargs):
if hi:
print('hi')
else:
print('bye')
return fn(*args, **kwargs)
return new
Le wrapper défini peut être utilisé dans les deux sens
@hiwrap
def fn(n):
print(n)
fn(1)
#> hi
#> 1
@hiwrap(hi=False)
def fn(n):
print(n)
fn(1)
#> bye
#> 1
biwrap
fonctionne également pour les méthodes liées
class O:
@hiwrap(hi=False)
def fn(self, n):
print(n)
O().fn(1)
#> bye
#> 1
Les méthodes/propriétés de classe sont également supportées
class O:
def __init__(self, n):
self.n = n
@classmethod
@hiwrap
def fn(cls, n):
print(n)
@property
@hiwrap(hi=False)
def num(self):
return self.n
o = O(2)
o.fn(1)
#> hi
#> 1
print(o.num)
#> bye
#> 2
Fonctionne comme si l'appel est bien aussi
def fn(n):
print(n)
fn = hiwrap(fn, hi=False)
fn(1)
#> bye
#> 1
Voici ma solution, écrite pour python3. Son approche est différente des autres car elle définit une classe appelable plutôt qu'une fonction.
class flexible_decorator:
def __init__(self, arg="This is default"):
self.arg = arg
def __call__(self, func):
def wrapper(*args, **kwargs):
print("Calling decorated function. arg '%s'" % self.arg)
func(*args, **kwargs)
return wrapper
Vous devez toujours appeler explicitement le décorateur
@flexible_decorator()
def f(foo):
print(foo)
@flexible_decorator(arg="This is not default")
def g(bar):
print(bar)