为什么对全局变量的错误赋值会提前引发异常?

max*_*max 14 python scope function python-3.x

a = 10
def f():
  print(1)
  print(a) # UnboundLocalError raised here
  a = 20
f()
Run Code Online (Sandbox Code Playgroud)

这个代码当然会提升UnboundLocalError: local variable 'a' referenced before assignment.但是为什么在这print(a)条线上提出了这个例外?

如果解释器逐行执行代码(就像我认为的那样),它就不会知道print(a)到达时出了什么问题; 它只会认为是a指全局变量.

因此,解释器似乎提前读取整个函数以确定是否a用于赋值.这记录在哪里?还有其他任何解释器向前看的场合(除了检查语法错误)吗?

为了澄清,异常本身非常清楚:全局变量可以在没有global声明的情况下读取,但不能写入(这种设计可以防止由于无意中修改全局变量而导致的错误;这些错误特别难以调试,因为它们会导致错误发生在远离错误代码的位置).我只是好奇为什么早点提出异常.

Ant*_*lvy 14

根据Python的文档,解释器将首先注意到a在范围内命名的变量的赋值f()(无论函数中赋值的位置),然后作为结果仅将变量识别a为此范围中的局部变量.此行为有效地影响全局变量a.

然后引发异常"提前",因为执行代码"逐行"的解释器将遇到引用局部变量的print语句,此时此时尚未绑定(请记住,Python正在local此处查找变量) ).

正如您在问题中提到的那样,必须使用global关键字明确告诉编译器此范围中的赋值是对全局变量进行的,正确的代码将是:

a = 10
def f():
  global a
  print(1)
  print(a) # Prints 10 as expected
  a = 20
f()
Run Code Online (Sandbox Code Playgroud)

正如@ 2rs2ts在一个现在删除的答案中所说,这很容易解释为"Python不仅仅被解释,它被编译成字节码而不仅仅是逐行解释".


Jim*_*ard 9

在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
Run Code Online (Sandbox Code Playgroud)

如您所见,'a'通过以下LOAD_FAST命令将名称加载到堆栈中:

             13 LOAD_FAST                0 (a)
Run Code Online (Sandbox Code Playgroud)

这是用于获取函数中的局部变量的命令(FAST由于它比从全局范围加载要快得多LOAD_GLOBAL).

这实际上与a之前定义的全局名称无关.这与CPython将假设你玩得很好并生成一个LOAD_FASTfor引用的事实有关,'a'因为'a'它被分配给函数体内部(即做一个本地名称).

对于具有单个名称访问且没有相应赋值的函数,CPython不生成a 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
Run Code Online (Sandbox Code Playgroud)

因此,解释器似乎提前读取整个函数以确定是否a用于赋值.这记录在哪里?还有其他任何解释器向前看的场合(除了检查语法错误)吗?

在参考手册的"复合语句"部分中,对于函数定义说明如下:

函数定义是可执行语句.它的执行将当前本地命名空间中的函数名绑定到一个函数对象(该函数的可执行代码的包装器).

具体来说,它将名称绑定f到一个保存已编译代码的函数对象f.__code__,这dis对我们来说是美化.