python - ¿Cómo inyectar la variable en el alcance con un decorador?
scope closures (7)
[Descargo de responsabilidad: puede haber formas más pitónicas de hacer lo que quiero hacer, pero quiero saber cómo funciona el alcance de Python aquí]
Estoy tratando de encontrar una forma de hacer que un decorador haga algo como inyectar un nombre en el alcance de otra función (de manera que el nombre no se filtre fuera del alcance del decorador). Por ejemplo, si tengo una función que dice que debo imprimir una variable llamada var
que no se ha definido, me gustaría definirla dentro de un decorador donde se llama. Aquí hay un ejemplo que rompe:
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()
Me gustaría imprimir " Message
", pero da:
NameError: global name ''var'' is not defined
El traceback incluso apunta a wher var
se define:
<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
Entonces no entiendo por qué no puede encontrar var
.
¿Hay alguna manera de hacer algo como esto?
Aquí hay una demostración simple de usar un decorador para agregar una variable al alcance de una función.
>>> 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!
>>>
Aquí hay una forma de inyectar múltiples variables en el alcance de una función de una manera algo similar a lo que hace @Martijn Pieters en su respuesta. Lo publico principalmente porque es una solución más general que aplicar el decorador varias veces, ya que sería necesario hacer lo mismo con el código en las respuestas de Martijn (y muchas de las demás).
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 __name__ == ''__main__'':
namespace = {''a'': 5, ''b'': 3}
@inject_variables(namespace)
def test():
print(''a:'', a)
print(''b:'', b)
test()
Hay una manera limpia de hacer lo que quiera sin usar la variable global. Si desea ser apátrida y los hilos seguros, realmente no tiene la opción.
Usa 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()
No puedes. Los nombres de ámbito (cierres) se determinan en tiempo de compilación, no puede agregar más en tiempo de ejecución.
Lo mejor que puede lograr es agregar nombres globales , utilizando el espacio de nombres global propio de la función:
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__
es el espacio de nombre global para la función envuelta, por lo que funciona incluso si el decorador vive en un módulo diferente. Si var
se ha definido como global, se reemplaza con el nuevo valor y, después de llamar a la función, se restauran los globales.
Esto funciona porque cualquier nombre en una función que no está asignada a, y no se encuentra en un ámbito circundante, se marca como un global en su lugar.
Manifestación:
>>> c = ''Message''
>>> @decorator_factory(c)
... def msg_printer():
... print var
...
>>> msg_printer()
Message
>>> ''var'' in globals()
False
Pero en lugar de decorar, igual podría haber definido var
en el ámbito global directamente .
Tenga en cuenta que alterar los globales no es seguro para subprocesos, y cualquier llamada transitoria a otras funciones en el mismo módulo también seguirá viendo este mismo global.
No puedes. Python tiene alcance léxico . Eso significa que el significado de un identificador se determina únicamente en función de los ámbitos que lo rodean físicamente cuando se mira el código fuente.
Python tiene un ámbito léxico, por lo que me temo que no hay una forma clara de hacer lo que quieras sin algunos efectos secundarios potencialmente desagradables. Recomiendo simplemente pasar var a la función a través del decorador.
c = ''Message''
def decorator_factory(value):
def msg_decorator(f):
def inner_dec(*args, **kwargs):
res = f(value, *args, **kwargs)
return res
inner_dec.__name__ = 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''
Suponiendo que en Python las funciones son objetos, puedes hacer ...
#!/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 __name__ == ''__main__'':
my_function(3, 4)
y el resultado es:
inside class decorator >>
class members: 1, 2
wrapped function: (3, 4)
inside decorated function >>
decorated function arguments: 3, 4
decorator class args: (1, 2)
más explicación aquí http://python-3-patterns-idioms-test.readthedocs.io/en/latest/PythonDecorators.html