Jak wprowadzić zmienną do zakresu za pomocą dekoratora?

[ZASTRZEŻENIE: Może być więcej pythonicznych sposobów robienia tego, co chcę robić, ale chcę wiedzieć, jak działa scoping Pythona tutaj]

Próbuję znaleźć sposób na stworzenie dekoratora, który robi coś w rodzaju wtryskiwania nazwy w zakres innej funkcji (tak, aby nazwa nie wyciekała poza zakres dekoratora). Na przykład, jeśli mam funkcję, która mówi, aby wydrukować zmienną o nazwie var, która nie została zdefiniowana, chciałbym zdefiniować ją w dekoratorze, gdzie jest dzwoniłem. Oto przykład, który się łamie:

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

Chciałbym, aby wydrukował "Message", ale daje:

NameError: global name 'var' is not defined

Ślad wskazuje nawet, gdzie var jest zdefiniowany:

<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

Więc nie rozumiem, dlaczego nie może znaleźć var.

Czy jest jakiś sposób, aby zrobić coś takiego?
Author: martineau, 2013-07-25

7 answers

Nie możesz. nazwy zakresów (zamknięcia) są ustalane w czasie kompilacji, nie możesz dodawać więcej w czasie wykonywania.

Najlepsze, co możesz osiągnąć, to dodać globalne nazwy, używając własnej przestrzeni nazw funkcji :

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__ jest globalną przestrzenią nazw dla funkcji zawiniętej, więc działa to nawet jeśli dekorator mieszka w innym module. Jeśli var została zdefiniowana jako globalna już, jest ona zastępowana nową wartością, a po wywołaniu funkcji globale są odrestaurowane.

Działa to, ponieważ każda nazwa funkcji, do której nie jest przypisana i nie znajduje się w otaczającym zakresie, jest oznaczana jako globalna.

Demo:

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

Ale zamiast dekorować, równie dobrze mogłem zdefiniować var w zasięgu globalnym bezpośrednio .

Zauważ, że zmiana globali nie jest bezpieczna dla wątków, a wszelkie przejściowe wywołania innych funkcji w tym samym module będą nadal widzieć tę samą globalną.

 42
Author: Martijn Pieters,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-15 16:50:28

Nie możesz. Python ma Zakres leksykalny . Oznacza to, że znaczenie identyfikatora jest określane wyłącznie na podstawie zakresów, które fizycznie go otaczają, gdy patrzysz na kod źródłowy.

 5
Author: newacct,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-25 21:35:47

Python ma leksykalny zasięg, więc obawiam się, że nie ma czystego sposobu na zrobienie tego, co chcesz, bez potencjalnie paskudnych efektów ubocznych. Polecam po prostu przekazanie var do funkcji przez dekoratora.

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'
 3
Author: Alexander Otavka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-15 07:01:27

Istnieje czysty sposób, aby zrobić to, co chcesz, bez użycia zmiennej globalnej. Jeśli chcesz być bezpaństwowcem i bezpiecznym, nie masz wyboru.

Użyj zmiennej "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()
 2
Author: M07,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-02-02 15:42:30

Oto prosta demonstracja użycia dekoratora do dodania zmiennej do zakresu funkcji.

>>> 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
Author: tlovely,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-18 16:06:28

Oto sposób wstrzykiwaniawielu zmiennych do zakresu funkcji w sposób nieco podobny do tego, co@Martijn Pieters robi w swojej odpowiedzi. Zamieszczam go przede wszystkim dlatego, że jest to bardziej ogólne rozwiązanie niż wielokrotne nakładanie dekoratora, co byłoby wymagane zrobienie tego samego z kodem w odpowiedziach Martijna (i wielu innych).

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()
 0
Author: martineau,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-05-25 21:07:25

Zakładając, że w Pythonie funkcje są obiektami, można to zrobić...

#!/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)

A wynikiem jest:

inside class decorator >>
class members: 1, 2
wrapped function: (3, 4)
inside decorated function >>
decorated function arguments: 3, 4
decorator class args: (1, 2)

Więcej wyjaśnień tutaj http://python-3-patterns-idioms-test.readthedocs.io/en/latest/PythonDecorators.html

 0
Author: dAn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-07-31 11:33:35