Как в реальном коде может произойти "NameError: свободная переменная 'var', на которую ссылаются перед присваиванием в пределах объема"?

Пока я болтался в чате Python, кто-то зашел и сообщил следующее исключение:

NameError: free variable 'var' referenced before assignment in enclosing scope

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

Немного поиграв, я смог воспроизвести исключение только с помощью игрушечного кода:

def multiplier(n):
    def multiply(x):
        return x * n
    del n
    return multiply

Что дает мне:

>>> triple = multiplier(3)
>>> triple(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in multiply
NameError: free variable 'n' referenced before assignment in enclosing scope

Все хорошо, но мне трудно понять, как это исключение может произойти в дикой природе, учитывая, что мой пример выше

  1. Довольно глупо
  2. Вряд ли случится случайно

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

Итак, как это конкретное исключение может возникнуть в реальном коде?

3 ответа

Решение

Подумайте о более сложной функции, где n связано в зависимости от какого-либо условия, или нет. Вам не нужно del имя, о котором идет речь, также происходит, если компилятор видит присваивание, поэтому имя является локальным, но путь к коду не берется, а имя никогда не назначается. Еще один глупый пример:

def f():
    def g(x):
        return x * n
    if False:
        n = 10
    return g

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

Итак, в сообщении об ошибке говорится:

NameError: ссылка на свободную переменную 'var' перед назначением в охватывающей области

Когда мы говорим о свободных переменных, мы имеем дело с вложенными функциями. Python проделал некоторую «магию», чтобы дать вложенным функциям возможность доступа к переменным, определенным внутри их родительской области. Если мы имеем:

      def outer():
    foo = 10
    def inner():
        print(foo)
    return inner

outer()()  # 10

Обычно у нас не должно быть доступа кfooвinnerфункция. Почему ? потому что после вызова и выполнения тела функции ее пространство имен уничтожается. По сути, любая локальная переменная, определенная внутри функции, становится недоступной после завершения функции.

Но у нас есть доступ...

Это волшебство происходит с помощью " Cell object ":

Объекты «Cell» используются для реализации переменных, на которые ссылаются несколько областей. Для каждой такой переменной создается объект ячейки для хранения значения; локальные переменные каждого кадра стека, который ссылается на значение, содержат ссылку на ячейки из внешних областей, которые также используют эту переменную. При доступе к значению значение, содержащееся в ячейке, используется вместо самого объекта ячейки.

Просто чтобы увидеть это скрытое сохраненное значение в ячейках (мы поговорим об этом чуть позже):

      def outer():
    foo = 10
    def inner():
        print(foo)
    return inner

print(outer().__closure__[0].cell_contents)  # 10

Как это работает?

Во время "компиляции "

когда Python видит функцию внутри другой функции, он принимает к сведению имена переменных, на которые ссылаются внутри вложенной функции, которые фактически определены во внешней функции. Эта информация хранится в объектах кода обеих функций. для внешней функции иco_freevarsдля внутренней функции:

      def outer():
    foo = 10
    def inner():
        print(foo)
    return inner

print(outer.__code__.co_cellvars)   # ('foo',)
print(outer().__code__.co_freevars) # ('foo',)

Теперь время выполнения..., (см. код)

Когда Python хочет выполнить функцию, он создает «объект ячейки» для каждой переменной (co_cellvars), который он принял к сведению.

Затем, когда он проходит по строкам, всякий раз, когда он видит присвоение таким переменным, он заполняет соответствующий объект ячейки этой переменной. (помните, что «они» косвенно содержат фактические значения.)

Когда выполнение доходит до строки создания внутренней функции, Python берет все созданные объекты ячеек и составляет из них кортеж. Затем этот кортеж присваивается внутренней функции__closure__.

Дело в том, что при создании этого кортежа некоторые ячейки могут еще не иметь значения. Они пусты (см. вывод)!...

В этот момент, когда вы вызываете внутреннюю функцию, эти ячейки без значения вызовут указанную ошибку!

      def outer():
    foo = 10

    def inner():
        print(foo)
        try:
            print(boo)
        except NameError as e:
            print(e)

    # Take a look at inner's __closure__ cells
    print(inner.__closure__)
    # So one boo is empty! This raises error
    inner()

    # Now lets look at inner's __closure__ cells one more time (they're filled now)
    boo = 20
    print(inner.__closure__)
    # This works fine now
    inner()

outer()

вывод из Python 3.10:

      (<cell at 0x7f14a5b62710: empty>, <cell at 0x7f14a5b62830: int object at 0x7f14a6f00210>)
10
free variable 'boo' referenced before assignment in enclosing scope
(<cell at 0x7f14a5b62710: int object at 0x7f14a6f00350>, <cell at 0x7f14a5b62830: int object at 0x7f14a6f00210>)
10
20

Ошибкаfree variable 'boo' referenced before assignment in enclosing scopeтеперь имеет смысл.

Примечание. Эта ошибка изменена в Python 3.11 на:

      cannot access free variable 'boo' where it is not associated with a value in enclosing scope

Но идея та же.


Если вы посмотрите на байт-кодouterфункции, вы увидите шаги, которые я упомянул в разделе «время выполнения» в действии:

      from dis import dis

def outer():
    foo = 10
    def inner():
        print(foo)
        print(boo)
    boo = 20
    return inner

dis(outer)

вывод из Python 3.11:

                    0 MAKE_CELL                1 (boo)
              2 MAKE_CELL                2 (foo)

  3           4 RESUME                   0

  4           6 LOAD_CONST               1 (10)
              8 STORE_DEREF              2 (foo)

  5          10 LOAD_CLOSURE             1 (boo)
             12 LOAD_CLOSURE             2 (foo)
             14 BUILD_TUPLE              2
             16 LOAD_CONST               2 (<code object inner at 0x7fb6d4731a30, file "", line 5>)
             18 MAKE_FUNCTION            8 (closure)
             20 STORE_FAST               0 (inner)

  8          22 LOAD_CONST               3 (20)
             24 STORE_DEREF              1 (boo)

  9          26 LOAD_FAST                0 (inner)
             28 RETURN_VALUE

MAKE_CELLявляется новым в Python3.11.
STORE_DEREFсохраняет значение внутри объекта ячейки.

Я только что получил это сообщение об ошибке во время работы над приложением Flask.

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

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

Я исправил ошибку, переместив созданиеобъект обратно вверх по направлению к вершинефункцию над ее появлением в обработчиках событий.

Другие вопросы по тегам