【问题标题】:Why does incorrect assignment to a global variable raise exception early?为什么不正确地分配给全局变量会提前引发异常?
【发布时间】:2016-11-26 19:09:52
【问题描述】:
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 声明的情况下读取,但不能写入(这种设计可以防止由于无意修改全局变量而导致的错误;这些错误特别难以调试,因为它们会导致发生在远离错误代码位置的错误)。我只是好奇为什么会提前引发异常。

【问题讨论】:

  • 我想你会发现python默认不能从函数内部访问全局变量。您必须明确声明您的意思是使用全局变量。 (顺便说一句,不要使用全局变量)。
  • @quamrana 不真实。如果您删除对本地 a 的分配,代码将打印 10
  • 对于它的价值,这不是特定于 python 3,只是在 python 2 中测试并且发生了同样的事情。
  • 我想你会发现 python 不能 (edit) access write to 来自内部函数的全局变量默认。您必须明确声明您的意思是使用全局变量。 (顺便说一句,不要使用全局变量)。
  • this这样的类似/重复有很多,但由于这个Q没有冗余代码,我认为它不应该被关闭。

标签: python function python-3.x scope


【解决方案1】:

根据Python's documentation,解释器将首先注意到f()范围内名为a的变量的赋值(无论赋值在函数中的位置),然后只识别变量a 作为此范围内的局部变量。这种行为有效地shadows 全局变量a

然后“早期”引发异常,因为“逐行”执行代码的解释器将遇到引用局部变量的 print 语句,该局部变量此时尚未绑定(请记住,Python 正在寻找local 变量在这里)。

正如您在问题中提到的,必须使用 global 关键字来明确告诉编译器此范围内的分配已完成对全局变量的正确代码:

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

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

【讨论】:

  • 脚本首先编译成字节码而不是逐行解释这一事实是否有任何其他(轻微)意外后果?
  • 并不是我突然意识到这一点,但我可能错了。我知道python没有被编译器优化太多,因为语言是非常动态的(没有静态类型,方法可以动态替换等),所以它非常困难。一般来说,代码被编译成字节码只是为了去除每次函数调用的解析时间(运行时只是在一个大的查找表中查找字节码,而不是再次解析所有内容)。 cc @max
  • @max:编译和解释与您的问题完全无关。您在询问 语义,即 Python 语言规范。 Python 语言规范说,函数中分配的所有变量都是局部变量,除非声明为 global。时期。编译和解释是 Pragmatics 的问题,即任何特定的 Python 实现。但是任何特定的 Python 实现,无论是编译还是解释,都必须遵守 Python 语言规范。否则它就不会成为“Python实现”,它……
  • ... 将是一种完全不同的语言的实现,有点类似于 Python。即使根本没有任何 Python 实现,如果 Python 只存在于纸上(或者甚至只存在于 Guido van Rossum 的头脑中),那么行为仍然与您所看到的相同,因为Python 语言规范说你正在看到的行为是你应该看到的。不要将编程语言“Python”与实现“CPython”混淆。例如:您看到的行为是“Python”的一部分,因此在 all 实现中是相同的(Pyston,...
  • ... PyPy, IronPython, Jython, CPython, Pynie, ...),而引用计数和确定性终结、GIL、C 扩展 API 是“CPython”的一部分,不一定存在于其他实现。
【解决方案2】:

在 Python 参考手册的 Resolution of Names 部分中说明了这一点:

[..] 如果当前作用域是一个函数作用域,并且 name 指的是一个局部变量,该局部变量在使用该名称时还没有绑定到值,则UnboundLocalError 引发异常 [..]

这是UnboundLocalError 出现时的官方说法。如果您查看 CPython 为您的函数 fdis 生成的字节码,您可以看到它在其值尚未设置时尝试从本地范围加载名称:

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

这是用于在函数中获取 local 变量的命令(命名为 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 为我们美化了它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-25
    • 2021-01-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多