Почему некорректное присвоение глобальной переменной вызывает исключение раньше?

a = 10
def f():
  print(1)
  print(a) # UnboundLocalError raised here
  a = 20
f()

Этот код, конечно, поднимает UnboundLocalError: local variable 'a' referenced before assignment. Но почему это исключение возникает в строке print(a)?

Если интерпретатор выполнил код по строкам (как я думал, он это сделал), он не знал бы, что что-то было не так, когда print(a) было достигнуто; он просто подумал бы, что a ссылается на глобальную переменную.

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

Чтобы прояснить, само исключение совершенно ясно: глобальные переменные могут быть прочитаны без объявления global, но не написаны (этот проект предотвращает ошибки из-за непреднамеренно изменяющихся глобальных переменных; эти ошибки особенно сложно отлаживать, поскольку они приводят к ошибки, которые происходят далеко от местоположения ошибочного кода). Мне просто интересно, почему исключение поднимается раньше.

Ответы

Ответ 1

Согласно документации Python, интерпретатор сначала заметит назначение переменной a в области f() ( независимо от положения назначения в функции), а затем, как следствие, распознать переменную a как локальную переменную в этой области. Это поведение эффективно shadows глобальная переменная a.

Затем исключение возникает "раньше", потому что интерпретатор, выполняющий код "строка за строкой", столкнется с оператором печати, ссылающимся на локальную переменную, которая еще не связана в этот момент (помните, что Python ищет a local здесь).

Как вы упомянули в своем вопросе, нужно использовать ключевое слово global, чтобы явным образом сообщить компилятору, что присвоение в этой области выполняется для глобальной переменной, правильный код:

a = 10
def f():
  global a
  print(1)
  print(a) # Prints 10 as expected
  a = 20
f()

Как @2rs2ts говорится в теперь удаленном ответе, это легко объяснить тем, что "Python не просто интерпретируется, он скомпилирован в байт-код и не просто интерпретируется по строкам".

Ответ 2

В разделе Resolution of Names справочного руководства Python сказано:

[..] Если текущая область действия является областью действия, а имя относится к локальной переменной, которая еще не привязана к значению в точке, где используется имя, возникает исключение UnboundLocalError [..]

что официальное слово, когда происходит UnboundLocalError. Если вы посмотрите на байт-код, который создает CPython для вашей функции f с помощью dis, вы можете увидеть его, пытаясь загрузить имя из локальная область, когда ее значение еще не установлено:

>>> dis.dis(f)
  3           0 LOAD_GLOBAL              0 (print)
              3 LOAD_CONST               1 (1)
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
              9 POP_TOP

  4          10 LOAD_GLOBAL              0 (print)
             13 LOAD_FAST                0 (a)      # <-- this command
             16 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             19 POP_TOP

  5          20 LOAD_CONST               2 (20)
             23 STORE_FAST               0 (a)
             26 LOAD_CONST               0 (None)
             29 RETURN_VALUE

Как вы можете видеть, имя 'a' загружается в стек с помощью команды LOAD_FAST:

             13 LOAD_FAST                0 (a)

Это команда, которая используется для захвата локальных переменных в функции (с именем FAST из-за того, что она выполняется быстрее, чем загрузка из глобальной области с помощью LOAD_GLOBAL).

Это действительно не имеет ничего общего с глобальным именем a, которое было определено ранее. Это связано с тем, что CPython предположит, что вы играете красиво и генерируете LOAD_FAST для ссылок на 'a', поскольку 'a' назначается (т.е. Создается локальное имя) внутри тела функции.

Для функции с единственным именем доступа и без соответствующего назначения CPython не генерирует LOAD_FAST и вместо этого обращается к глобальной области с помощью LOAD_GLOBAL:

>>> def g():
...    print(b)
>>> dis.dis(g)
  2           0 LOAD_GLOBAL              0 (print)
              3 LOAD_GLOBAL              1 (b)
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
              9 POP_TOP
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE

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

В разделе "Составные выражения" справочного руководства для определения функций указано следующее:

Определение функции - исполняемый оператор. Его выполнение связывает имя функции в текущем локальном пространстве имен с объектом функции (оболочкой вокруг исполняемого кода для функции).

В частности, он связывает имя f с функциональным объектом, который содержит скомпилированный код f.__code__, который dis превалирует для нас.