Область видимости переменных во вложенных функциях

Может ли кто-нибудь объяснить, почему следующая программа не работает:

def g(f):
  for _ in range(10):
    f()

def main():
  x = 10
  def f():
    print x
    x = x + 1
  g(f)

if __name__ == '__main__':
  main()

с сообщением:

Traceback (most recent call last):
  File "a.py", line 13, in <module>
    main()
  File "a.py", line 10, in main
    g(f)
  File "a.py", line 3, in g
    f()
  File "a.py", line 8, in f
    print x
UnboundLocalError: local variable 'x' referenced before assignment

Но если я просто изменю переменную x на массив, это сработает:

def g(f):
  for _ in range(10):
    f()

def main():
  x = [10]
  def f():
    print x[0]
    x[0] = x[0] + 1
  g(f)

if __name__ == '__main__':
  main()

с выходом

10
11
12
13
14
15
16
17
18
19

Причина, по которой я запутался, в том, что если из f() он не может получить доступ к x, почему он становится доступным, если x является массивом?

Спасибо.


person Kan Li    schedule 09.05.2013    source источник


Ответы (4)


Но этот ответ говорит, что проблема связана с назначением x. Если это так, то печать должна работать нормально, не так ли?

Вы должны понимать порядок, в котором происходят события. Еще до того, как ваш код Python будет скомпилирован и выполнен, нечто, называемое парсером, считывает код Python и проверяет синтаксис. Еще одна вещь, которую делает синтаксический анализатор, — помечает переменные как локальные. Когда синтаксический анализатор видит присваивание в коде в локальной области видимости, переменная в левой части присваивания помечается как локальная. В этот момент еще ничего не скомпилировано, не говоря уже о выполнении, и, следовательно, никакого присваивания не происходит; переменная просто помечена как локальная переменная.

После завершения парсера код компилируется и выполняется. Когда выполнение достигает оператора печати:

def main():
  x = 10     #<---x in enclosing scope

  def f():
    print x    #<-----

    x = x + 1  #<-- x marked as local variable inside the function f()

оператор печати выглядит так, как будто он должен продолжить и напечатать x в области enclosing («E» в процессе поиска LEGB). Однако, поскольку синтаксический анализатор ранее помечал x как локальную переменную внутри f(), python не выходит за пределы локальной области («L» в процессе поиска LEGB) для поиска x. Поскольку x не был назначен в локальной области во время выполнения «print x», python выдает ошибку.

Обратите внимание, что даже если код, в котором происходит присваивание, НИКОГДА не будет выполняться, синтаксический анализатор по-прежнему помечает переменную слева от присваивания как локальную переменную. Синтаксический анализатор понятия не имеет о том, как что-то будет выполняться, поэтому он слепо ищет синтаксические ошибки и локальные переменные в вашем файле — даже в коде, который никогда не может быть выполнен. Вот несколько примеров этого:

def dostuff ():
    x = 10 

    def f():
        print x

        if False:  #The body of the if will never execute...
            a b c  #...yet the parser finds a syntax error here


    return f

f = dostuff()
f()



--output:--
File "1.py", line 8
     a b c
      ^
SyntaxError: invalid syntax

Парсер делает то же самое при маркировке локальных переменных:

def dostuff ():
    x = 10 

    def f():
        print x

        if False:  #The body of the if will never execute...
            x = 0  #..yet the parser marks x as a local variable

    return f

f = dostuff()
f()

Теперь посмотрите, что происходит, когда вы выполняете эту последнюю программу:

Traceback (most recent call last):
  File "1.py", line 11, in <module>
    f()
  File "1.py", line 4, in f
    print x
UnboundLocalError: local variable 'x' referenced before assignment

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

Эта «функция» не уникальна для python — она встречается и в других языках.

Что касается примера массива, когда вы пишете:

x[0] = x[0] + 1

это говорит python искать массив с именем x и присваивать что-то его первому элементу. Поскольку в локальной области нет присваивания чему-либо с именем x, синтаксический анализатор не помечает x как локальную переменную.

person 7stud    schedule 09.05.2013
comment
+1 за упоминание компилятора, чтобы объяснить, как работают области. новая точка зрения для меня. - person Ivan Ferrer Villa; 18.12.2013

Причина в том, что в первом примере вы использовали операцию присваивания, x = x + 1, поэтому, когда функции были определены, python подумал, что x является локальной переменной. Но когда вы на самом деле вызвали функцию, python не смог найти какое-либо значение для x на RHS локально, поэтому возникла ошибка.

Во втором примере вместо назначения вы просто изменили изменяемый объект, поэтому python никогда не будет возражать и будет извлекать значение x[0] из объемлющей области (на самом деле он ищет его сначала в охватывающей области, затем в глобальной области и, наконец, во встроенных функциях , но останавливается, как только находит).

В python 3x вы можете справиться с этим, используя ключевое слово nonlocal, а в py2x вы можете либо передать значение внутренней функции, либо использовать атрибут функции.

Использование атрибута функции:

def main():
  main.x = 1
  def f():
      main.x = main.x + 1
      print main.x
  return f

main()()   #prints 2

Явная передача значения:

def main():
  x = 1
  def f(x):
      x = x + 1
      print x
      return x
  x = f(x)     #pass x and store the returned value back to x

main()   #prints 2

Использование nonlocal в py3x:

def main():
  x = 1
  def f():
      nonlocal x
      x = x + 1
      print (x)
  return f

main()()  #prints 2
person Ashwini Chaudhary    schedule 09.05.2013
comment
Атрибут функции не может быть рекомендован. Поскольку существует только одна копия атрибута, любая рекурсия приведет к неожиданным результатам. - person Dietrich Epp; 09.05.2013

Проблема в том, что переменная x подхватывается замыканием. Когда вы пытаетесь присвоить значение переменной, полученной из замыкания, python будет жаловаться, если вы не используете ключевые слова global или nonlocal1. В случае, когда вы используете list, вы не присваиваете имя — вы можете изменить объект, который был выбран в замыкании, но вы не можете присвоить ему.


По сути, ошибка возникает в строке print x, потому что, когда python анализирует функцию, он видит, что x назначено, поэтому предполагает, что x должна быть локальной переменной. Когда вы дойдете до строки print x, python попытается найти локальную строку x, но ее там нет. Это можно увидеть, используя dis.dis для проверки байт-кода. Здесь python использует инструкцию LOAD_FAST, которая используется для локальных переменных, а не инструкцию LOAD_GLOBAL, которая используется для нелокальных переменных.

Обычно это вызывает NameError, но python пытается быть немного более полезным, ища x в func_closure или func_globals 2. Если он находит x в одном из них, он вместо этого вызывает UnboundLocalError, чтобы дать вам лучшее представление о том, что происходит - у вас есть локальная переменная, которую не удалось найти (не "связана").

1только python3.x

2python2.x -- в python3.x эти атрибуты изменились на __closure__ и __globals__ соответственно

person mgilson    schedule 09.05.2013
comment
Но тогда почему он терпит неудачу в print x вместо x = x + 1? - person Kan Li; 09.05.2013
comment
@icando: print x на первом месте? - person Ry-♦; 09.05.2013
comment
@rynah: Но в этом ответе говорится, что проблема связана с назначением на x. Если это так, то printing должно работать нормально, не так ли? - person cHao; 09.05.2013
comment
Проблема заключается в чтении переменной, которую вы назначаете, но до того, как вы ей назначаете. Python заранее знает, что вы присваиваете x где-то в функции, поэтому он предполагает, что x является локальным, но тогда возникает ошибка, когда вы пытаетесь прочитать из него до того, как присвоили ему значение. - person BrenBarn; 09.05.2013
comment
@icando - извините за поздний ответ. Мой компьютер начал испытывать трудности, поэтому я не мог уточнить до сих пор. - person mgilson; 09.05.2013

Проблема в линии

x = x + 1

Это первый раз, когда x назначается в функции f(), сообщая компилятору, что x является локальным именем. Он конфликтует с предыдущей строкой print x, которая не может найти никакого предыдущего назначения локального x. Вот откуда ваша ошибка UnboundLocalError: local variable 'x' referenced before assignment.

Обратите внимание, что ошибка возникает, когда компилятор пытается выяснить, к какому объекту относится x в print x. Таким образом, print x не выполняется.

Измените его на

x[0] = x[0] + 1

Новое имя не добавляется. Таким образом, компилятор знает, что вы имеете в виду массив вне f().

person Ade YU    schedule 09.05.2013