Как ввести переменную в область видимости с помощью декоратора?

[Отказ от ответственности: могут быть другие питонические способы делать то, что я хочу сделать, но я хочу знать, как здесь работает область видимости Python]

Я пытаюсь найти способ сделать декоратор, который делает что-то вроде внедрения имени в область действия другой функции (чтобы имя не просачивалось за пределы области действия декоратора). Например, если у меня есть функция, которая говорит напечатать переменную с именем var, которая не была определена, я хотел бы определить ее в декораторе, где она вызывается. Вот пример, который ломается:

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

Я бы хотел, чтобы он печатал "Message", но это дает:

NameError: global name 'var' is not defined

Трассировка даже указывает на то, где определено var:

<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

Поэтому я не понимаю, почему он не может найти var.

Есть ли способ сделать что-то подобное?


person beardc    schedule 25.07.2013    source источник


Ответы (11)


Вы не можете. Имена с ограниченной областью (замыкания) определяются во время компиляции, вы не можете добавить больше во время выполнения.

Лучшее, на что вы можете надеяться, это добавить глобальные имена, используя собственное глобальное пространство имен функции:

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__ — это глобальное пространство имен для обернутой функции, так что это работает, даже если декоратор находится в другом модуле. Если var уже был определен как глобальный, он заменяется новым значением, и после вызова функции глобалы восстанавливаются.

Это работает, потому что любое имя в функции, которое не назначено и не найдено в окружающей области, вместо этого помечается как глобальное.

Демо:

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

Но вместо украшения я мог бы с тем же успехом определить var в глобальной области видимости непосредственно.

Обратите внимание, что изменение глобальных переменных не является потокобезопасным, и любые временные вызовы других функций в том же модуле также будут видеть эту же глобальную переменную.

person Martijn Pieters    schedule 25.07.2013
comment
Итак, если я делаю def msg_printer(): print var и пытаюсь вызвать msg_printer, я получаю ту же ошибку имени, но если я затем определяю var='Hi' и вызываю его, он печатает его нормально. В этом примере var не определяется во время выполнения после компиляции msg_printer? - person beardc; 25.07.2013
comment
Поскольку var не определено ни в функции, ни в родительских областях, var вместо этого помечается во время компиляции как глобальное имя. Но если бы была родительская область, то во время компиляции var вместо этого было бы помечено как имя области, и в этот момент трюк с декоратором больше не работал бы. - person Martijn Pieters; 25.07.2013
comment
Контекст глобальных переменных может быть перезаписан при наличии нескольких одновременных запросов. Я думал, что это невозможно, но это так - я использовал это решение для пользовательской аутентификации пользователя (моя ошибка) и через некоторое время увидел изменение контекста, если запрос не был обработан быстро. Обратите внимание, сейчас я ищу новое решение. - person Artyom Lisovskij; 21.11.2019
comment
@ArtyomLisovskij, поэтому мой ответ включает предупреждение в конце: Обратите внимание, что изменение глобальных переменных не является потокобезопасным - person Martijn Pieters; 21.11.2019

Есть простой способ делать то, что вы хотите, без использования глобальной переменной. Если вы хотите быть апатридом и потокобезопасным, у вас действительно нет выбора.

Используйте переменную 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()
person M07    schedule 30.11.2016
comment
Чем это концептуально отличается от передачи позиционного аргумента? - person Mad Physicist; 01.02.2018
comment
Это не так уж и отличается, но, поскольку это написано для позиционного аргумента, вы должны знать позицию своего аргумента. Как это декоратор, вы этого не знаете. kwargs — это самый безопасный способ, потому что вы контролируете имя своего аргумента. - person M07; 02.02.2018
comment
Это, безусловно, достаточно хорошее объяснение для меня, чтобы удалить свой отрицательный голос. Не уверен, что могу проголосовать за это в контексте принятого ответа. - person Mad Physicist; 02.02.2018

Вы не можете. Python имеет лексическую область видимости. Это означает, что значение идентификатора определяется исключительно на основе областей, которые физически его окружают, когда вы смотрите на исходный код.

person newacct    schedule 25.07.2013

Вот способ внедрения нескольких переменных в область действия функции способом, несколько похожим на то, что делает @Martijn Pieters в его ответ. Я публикую его в первую очередь потому, что это более общее решение, и его не нужно применять несколько раз, чтобы сделать это — как того требуют его (и многие другие) ответы.

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()
person martineau    schedule 25.05.2018

У меня работает обновление __globals__.

def f():
    print(a)


def with_context(**kw):
    def deco(fn):
        g = fn.__globals__
        g.update(kw)
        return fn

    return deco


with_context(a=3)(f)() # 3
person weaming    schedule 06.11.2019

Python имеет лексическую область видимости, поэтому я боюсь, что нет простого способа сделать то, что вы хотите, без некоторых потенциально неприятных побочных эффектов. Я рекомендую просто передать var в функцию через декоратор.

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'
person Alexander Otavka    schedule 12.08.2015
comment
Для чего нужны msg_decorator.__name__ = f.__name__ и msg_decorator.__doc__ = f.__doc__? Это необходимо? - person gogo_gorilla; 09.12.2016
comment
@stackoverflowwww Каждая функция в python имеет имя (если только она не была создана с помощью лямбда), и у многих есть строки документации. Оба важны для создания документации, поэтому мы копируем их в функцию-оболочку. Я сделал ошибку в своем ответе, на самом деле они должны быть скопированы на inner_dec. - person Alexander Otavka; 15.12.2016
comment
См. также functools.wraps для подобных вещей - person killthrush; 28.01.2017
comment
Из других ответов видно, что есть чистые способы сделать это... - person martineau; 25.05.2018

Я нашел интересный пост, предлагающий другое решение, создавая функции на лету. В основном:

def wrapper(func):
    cust_globals = func.__globals__.copy()

    # Update cust_globals to your liking

    # Return a new function
    return types.FunctionType(
        func.__code__, cust_globals, func.__name__, func.__defaults__, func.__closure__
    )

См. https://hardenedapple.github.io/stories/computers/python_function_override/.

person Johan de Vries    schedule 27.03.2020

Вот простая демонстрация использования декоратора для добавления переменной в область действия функции.

>>> 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!
>>>
person tlovely    schedule 18.06.2015
comment
Обратите внимание, что здесь вы работаете с общим словарем. Другие функции в том же модуле также увидят это изменение, а изменение словаря не является потокобезопасным. - person Martijn Pieters; 28.07.2015
comment
@MartijnPieters Является ли это проблемой, даже если значение, измененное декоратором, больше не изменяется и читается только после того, как декоратор вернулся? - person gogo_gorilla; 09.12.2016
comment
@stackoverflowwww: это изменяет глобальные переменные модуля всякий раз, когда вызывается функция. - person Martijn Pieters; 09.12.2016
comment
@MartijnPieters Итак, почему люди не всегда используют подход, предложенный выше и ниже Александром Отавкой и M07, то есть позволяя декоратору вызывать декорированные функции с аргументами и, таким образом, передавать значения декорированной функции? - person gogo_gorilla; 09.12.2016
comment
@stackoverflowwww: большинство людей действительно используют такие методы. - person Martijn Pieters; 09.12.2016

Предполагая, что в функциях python есть объекты, вы можете сделать...

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

и результат:

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

дополнительные пояснения здесь http://python-3-patterns-idioms-test.readthedocs.io/en/latest/PythonDecorators.html

person dAn    schedule 31.07.2018

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)

Вместо того, чтобы пересматривать глобальную область видимости, более разумно изменить саму аннотированную функцию.

person Martin Wang    schedule 09.03.2019

Я поймал проблему с решением, использующим глобальные переменные.

Контекст глобальных переменных может быть перезаписан при наличии нескольких одновременных запросов. Я думал, что это невозможно, но это так - через некоторое время я поймал изменение контекста (глобальные), если запрос не был быстрым. Лучшее решение - передать переменную с помощью kwargs:

def is_login(old_fuction):
    def new_function(request, *args, **kwargs):
        secret_token = request.COOKIES.get('secret_token')
        if secret_token:
            items = SomeModel.objects.get(cookie = secret_token)
            if len(items) > 0:
                item = items[0]
                kwargs['current_user'] = item
                return old_fuction(request, *args, **kwargs)
            else:
                return HttpResponse('error')
        return HttpResponse(status=404)
    return new_function

@is_login  
def some_func(request, current_user):
    return HttpResponse(current_user.name)

Вам нужно будет добавить дополнительный параметр к каждой украшенной функции.

person Artyom Lisovskij    schedule 20.11.2019