web-dev-qa-db-fra.com

Comment injecter une variable dans la portée avec un décorateur?

[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?

48
beardc

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.

51
Martijn Pieters

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()
5
martineau

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.

5
newacct

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()
3
M07

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'
3
Alexander Otavka
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.

1
Martin Wang

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!
>>>
0
tlovely

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

0
dAn