[Avertissement: il y a peut-être plus de façons Pythoniques de faire ce que je veux faire, mais je veux savoir comment fonctionne la portée de python ici]
J'essaie de trouver un moyen de créer un décorateur qui fasse quelque chose comme injecter un nom dans la portée d'une autre fonction (de telle sorte que le nom ne fuit pas en dehors de la portée du décorateur). Par exemple, si j'ai une fonction qui dit d'imprimer une variable nommée var
qui n'a pas été définie, je voudrais la définir dans un décorateur où elle est appelée. Voici un exemple qui casse:
c = 'Message'
def decorator_factory(value):
def msg_decorator(f):
def inner_dec(*args, **kwargs):
var = value
res = f(*args, **kwargs)
return res
return inner_dec
return msg_decorator
@decorator_factory(c)
def msg_printer():
print var
msg_printer()
Je voudrais qu'il affiche "Message
", mais il donne:
NameError: global name 'var' is not defined
Le traceback indique même où var
est défini:
<ipython-input-25-34b84bee70dc> in inner_dec(*args, **kwargs)
8 def inner_dec(*args, **kwargs):
9 var = value
---> 10 res = f(*args, **kwargs)
11 return res
12 return inner_dec
Je ne comprends donc pas pourquoi il ne trouve pas var
.
Y a-t-il un moyen de faire quelque chose comme ça?
Tu ne peux pas. Les noms étendus (fermetures) sont déterminés au moment de la compilation, vous ne pouvez pas en ajouter d'autres à l'exécution.
Le mieux que vous puissiez espérer obtenir est d'ajouter global noms, en utilisant propre l'espace de noms global de la fonction:
def decorator_factory(value):
def msg_decorator(f):
def inner_dec(*args, **kwargs):
g = f.__globals__ # use f.func_globals for py < 2.6
sentinel = object()
oldvalue = g.get('var', sentinel)
g['var'] = value
try:
res = f(*args, **kwargs)
finally:
if oldvalue is sentinel:
del g['var']
else:
g['var'] = oldvalue
return res
return inner_dec
return msg_decorator
f.__globals__
est l'espace de noms global pour la fonction encapsulée, donc cela fonctionne même si le décorateur vit dans un module différent. Si var
était déjà défini comme global, il est remplacé par la nouvelle valeur et après avoir appelé la fonction, les globaux sont restaurés.
Cela fonctionne car tout nom dans une fonction qui n'est pas affecté à, et qui ne se trouve pas dans une étendue environnante, est marqué comme global à la place.
Démo:
>>> c = 'Message'
>>> @decorator_factory(c)
... def msg_printer():
... print var
...
>>> msg_printer()
Message
>>> 'var' in globals()
False
Mais au lieu de décorer, j'aurais tout aussi bien pu définir var
dans la portée globale directement.
Notez que la modification des globaux n'est pas sûre pour les threads, et tout appel transitoire à d'autres fonctions dans le même module verra également ce même global.
Voici un moyen d'injecter plusieurs variables dans la portée d'une fonction d'une manière quelque peu similaire à ce que @ Martijn Pieters fait dans sa réponse . Je le poste principalement parce que c'est une solution plus générale et n'aurait pas besoin d'être appliqué plusieurs fois pour le faire - comme cela serait nécessaire pour faire le même chose ses réponses (et beaucoup d'autres).
from functools import wraps
def inject_variables(context):
""" Decorator factory. """
def variable_injector(func):
@wraps(func)
def decorator(*args, **kwargs):
try:
func_globals = func.__globals__ # Python 2.6+
except AttributeError:
func_globals = func.func_globals # Earlier versions.
saved_values = func_globals.copy() # Shallow copy of dict.
func_globals.update(context)
try:
result = func(*args, **kwargs)
finally:
func_globals = saved_values # Undo changes.
return result
return decorator
return variable_injector
if __== '__main__':
namespace = {'a': 5, 'b': 3}
@inject_variables(namespace)
def test():
print('a:', a)
print('b:', b)
test()
Tu ne peux pas. Python a portée lexicale. Cela signifie que la signification d'un identifiant est déterminée uniquement en fonction des étendues qui l'entourent physiquement lorsque vous regardez le code source.
Il existe un moyen propre de faire ce que vous voulez sans utiliser de variable globale. Si vous voulez être apatride et sans threads, vous n'avez pas vraiment le choix.
Utilisez la variable "kwargs":
c = 'Message'
def decorator_factory(value):
def msg_decorator(f):
def inner_dec(*args, **kwargs):
kwargs["var"] = value
res = f(*args, **kwargs)
return res
return inner_dec
return msg_decorator
@decorator_factory(c)
def msg_printer(*args, **kwargs):
print kwargs["var"]
msg_printer()
Python a une portée lexicale, donc je crains qu'il n'y ait pas de moyen propre de faire ce que vous voulez sans effets secondaires potentiellement désagréables. Je recommande de simplement passer var dans la fonction via le décorateur.
c = 'Message'
def decorator_factory(value):
def msg_decorator(f):
def inner_dec(*args, **kwargs):
res = f(value, *args, **kwargs)
return res
inner_dec.__= f.__name__
inner_dec.__doc__ = f.__doc__
return inner_dec
return msg_decorator
@decorator_factory(c)
def msg_printer(var):
print var
msg_printer() # prints 'Message'
def merge(d1, d2):
d = d1.copy()
d.update(d2)
return d
# A decorator to inject variables
def valueDecorator(*_args, **_kargs):
def wrapper(f):
def wrapper2(*args, **kargs):
return f(*args, **kargs)
wrapper2.__= f.__name__
wrapper2.__doc__ = f.__doc__
oldVars = getattr(f, 'Vars', [])
oldNamedVars = getattr(f, 'NamedVars', {})
wrapper2.Vars = oldVars + list(_args)
wrapper2.NamedVars = merge(oldNamedVars, _kargs)
return wrapper2
return wrapper
@valueDecorator(12, 13, a=2)
@valueDecorator(10, 11, a=1)
def func():
print(func.Vars)
print(func.NamedVars)
Au lieu de réviser la portée globale, changer la fonction annotée elle-même est plus raisonnable.
Voici une démonstration simple de l'utilisation d'un décorateur pour ajouter une variable dans la portée d'une fonction.
>>> def add_name(name):
... def inner(func):
... # Same as defining name within wrapped
... # function.
... func.func_globals['name'] = name
...
... # Simply returns wrapped function reference.
... return func
...
... return inner
...
>>> @add_name("Bobby")
... def say_hello():
... print "Hello %s!" % name
...
>>> print say_hello()
Hello Bobby!
>>>
En supposant que dans python sont des objets, vous pouvez faire ...
#!/usr/bin/python3
class DecorClass(object):
def __init__(self, arg1, arg2):
self.a1 = arg1
self.a2 = arg2
def __call__(self, function):
def wrapped(*args):
print('inside class decorator >>')
print('class members: {0}, {1}'.format(self.a1, self.a2))
print('wrapped function: {}'.format(args))
function(*args, self.a1, self.a2)
return wrapped
@DecorClass(1, 2)
def my_function(f1, f2, *args):
print('inside decorated function >>')
print('decorated function arguments: {0}, {1}'.format(f1, f2))
print('decorator class args: {}'.format(args))
if __== '__main__':
my_function(3, 4)
et le résultat est:
inside class decorator >>
class members: 1, 2
wrapped function: (3, 4)
inside decorated function >>
decorated function arguments: 3, 4
decorator class args: (1, 2)
plus d'explications ici http://python-3-patterns-idioms-test.readthedocs.io/en/latest/PythonDecorators.html