Python局部变量编译原理

Python local variable compile principle

def fun():  
    if False:
        x=3
    print(locals())
    print(x)
fun()

输出和错误信息:

{}
---------------------------------------------------------------------------
UnboundLocalError                         Traceback (most recent call last)
<ipython-input-57-d9deb3063ae1> in <module>()
      4     print(locals())
      5     print(x)
----> 6 fun()

<ipython-input-57-d9deb3063ae1> in fun()
      3         x=3
      4     print(locals())
----> 5     print(x)
      6 fun()

UnboundLocalError: local variable 'x' referenced before assignment

我想知道 python 解释器是如何工作的。请注意,x=3 根本不是 运行,它不应被视为局部变量,这意味着错误将是“名称 'x' 未定义”。但是查看代码和错误消息,情况并非如此。谁能解释一下这种情况背后python解释器的编译机制原理?

x = 3 无法访问这一事实无关紧要。函数分配给它,所以它必须是本地名称。

请记住,整个文件是在执行开始之前编译的,但是函数是在执行阶段定义的,当编译的函数定义块被执行时,创建函数对象。

一个复杂的优化器可以消除无法访问的代码,但是 CPython 的优化器并不那么聪明——它只执行非常简单的锁孔优化。

要更深入地了解 Python 内部结构,请查看 ast 和 dis 模块。

函数中使用的名称对于整个函数体只能有一个作用域。范围是在编译时确定的(而不是在函数为 运行 时)。

如果函数中的任何地方都有一个名称赋值(无论调用函数时它是否为 运行),编译器默认将该名称视为函数的局部名称。您可以使用 globalnonlocal 语句明确告诉它使用不同的范围。

一种特殊情况是在一个函数的主体中分配了一个名称,并从第一个函数中定义的另一个函数访问。这样的变量将放在一个特殊的 closure 单元格中,该单元格将在函数之间共享。外部函数会将变量视为局部变量,而内部函数只有在名称有 nonlocal 语句时才能分配给它。这是闭包和 nonlocal 语句的示例:

def counter():
    val = 0
    def helper():
        nonlocal val
        val += 1
        return val
    return helper

除了您看到的问题之外,您可能还会看到另一种范围混淆:

x = 1
def foo():
   print(x)  # you might expect this to print the global x, but it raises an exception
   x = 2     # this assignment makes the compiler treat the name x as local to the function

foo 函数中,名称 x 在任何地方都被认为是本地的,即使 print 调用在它被分配到本地名称空间之前尝试使用它。

因此,Python 将始终将每个函数中的每个名称归类为 localnon-local全球。这些名称范围是排他的;在每个函数中(嵌套函数中的名称都有自己的命名范围),每个名称只能属于这些类别之一。

当Python编译这段代码时:

def fun():
    if False:
        x=3

它将产生一个抽象语法树,如下所示:

FunctionDef(
    name='fun', 
    args=arguments(...), b
    body=[
        If(test=NameConstant(value=False),
            body=[
                Assign(targets=[Name(id='x', ctx=Store())], value=Num(n=3))
            ], 
            orelse=[])
    ]
)

(为简洁起见省略了一些内容)。现在,当这个抽象语法树被编译成代码时,Python 将扫描所有名称节点。如果有任何 Name 节点,具有 ctx=Store(),则该名称被认为是 local 到封闭的 FunctionDef(如果有),除非被 global(即 global x)或 nonlocalnonlocal x)相同函数定义中的语句。

ctx=Store() 主要出现在相关名称用于赋值语句的左侧,或用作 for 循环中的迭代变量时。

现在,当Python将其编译成字节码时,生成的字节码是

>>> dis.dis(fun)
  4           0 LOAD_GLOBAL              0 (print)
              3 LOAD_FAST                0 (x)
              6 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
              9 POP_TOP
             10 LOAD_CONST               0 (None)
             13 RETURN_VALUE

优化器完全删除了 if 语句;然而,由于变量已经标记为函数的局部变量,LOAD_FAST 用于 x,这将导致 xlocal 变量访问, 和局部变量。由于尚未设置 x,因此抛出 UnboundLocalError。另一方面,名称 print 从未分配给,因此被视为此函数中的全局名称,因此其值加载为 LOAD_GLOBAL.