¿Cómo inyectar variable dentro del scope con un decorador?

[Descargo de responsabilidad: puede haber más formas pythonicas de hacer lo que quiero hacer, pero quiero saber cómo funciona el scope de Python aquí]

Estoy tratando de encontrar una manera de hacer un decorador que haga algo como inyectar un nombre en el scope de otra función (de modo que el nombre no se filtre fuera del scope del decorador). Por ejemplo, si tengo una función que dice 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 rastreo incluso apunta a donde se define var :

  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 

Así que no entiendo por qué no puede encontrar var .

    ¿Hay alguna manera de hacer algo como esto?

    Usted no puede Los nombres de ámbito (cierres) se determinan en el momento de la comstackción, no puede agregar más en tiempo de ejecución.

    Lo mejor que puede esperar lograr es agregar nombres globales , utilizando el propio espacio de nombres global 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 nombres global para la función envuelta, por lo que esto funciona incluso si el decorador vive en un módulo diferente. Si var se definió como global, se reemplazará con el nuevo valor y, después de llamar a la función, se restaurarán los globales.

    Esto funciona porque cualquier nombre en una función que no está asignado a, y no se encuentra en un ámbito circundante, está marcado como global.

    Manifestación:

     >>> c = 'Message' >>> @decorator_factory(c) ... def msg_printer(): ... print var ... >>> msg_printer() Message >>> 'var' in globals() False 

    Pero en lugar de decorar, también podría haber definido var en el ámbito global directamente .

    Tenga en cuenta que la modificación de los elementos globales no es segura para subprocesos, y cualquier llamada transitoria a otras funciones en el mismo módulo también verá este mismo nivel global.

    Usted no puede Python tiene scope 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 quieres sin algunos efectos secundarios potencialmente desagradables. Recomiendo simplemente pasar var en 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' 

    Aquí hay una forma de inyectar múltiples variables en el scope de una función de una manera similar a lo que hace @Martijn Pieters en su respuesta. Lo publico principalmente porque es una solución más general y no tendría que aplicarse varias veces para hacerlo, ya que se requeriría hacer lo mismo que sus respuestas (y muchas otras).

     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 forma limpia de hacer lo que quiere sin usar una variable global. Si desea ser apátrida e 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() 

    Aquí hay una demostración simple de usar un decorador para agregar una variable al scope 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! >>> 

    Asumiendo que en las funciones de python 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

     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.__name__ = 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) 

    En lugar de revisar el scope global, cambiar la función anotada en sí es más razonable.