Python 如何在遇到局部变量的声明之前知道它的存在?

Ash*_*sia 2 python scoping

def f(): 
    print("Before", locals())   # line 2
    print(x);                   # line 3
    x = 2                       # line 4
    print("After", locals())    # line 5

x = 1
f()
Run Code Online (Sandbox Code Playgroud)

我知道 Python 中范围界定的 LEGB 规则。

对于上面的代码,当我注释掉第 4 行时,一切都按预期正常执行:对于第 3 行,python 在本地范围内找不到变量x,因此在找到它的全局范围中搜索它并打印 1。

但是当我按原样执行整个代码而不加注释时,它会引发UnboundLocalError: local variable 'x' referenced before assignment.

我确实知道我可以使用非本地和全局,但我的问题是:

  1. python 在遇到局部变量声明之前如何知道存在局部变量声明?
  2. 即使它确实知道本地作用域中有一个名为 x 的变量(尽管尚未初始化),为什么它不在 locals() 中显示它?

我尝试在类似问题建议中找到答案,但失败了。如果我的理解有错误,请指正。

che*_*ner 8

在某种程度上,答案是特定于实现的,因为Python只指定了预期的行为,而不是如何实现它。

也就是说,让我们看一下f通常的实现 CPython 生成的字节码:

>>> import dis
>>> dis.dis(f)
  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Before')
              4 LOAD_GLOBAL              1 (locals)
              6 CALL_FUNCTION            0
              8 CALL_FUNCTION            2
             10 POP_TOP

  3          12 LOAD_GLOBAL              0 (print)
             14 LOAD_FAST                0 (x)
             16 CALL_FUNCTION            1
             18 POP_TOP

  4          20 LOAD_CONST               2 (2)
             22 STORE_FAST               0 (x)

  5          24 LOAD_GLOBAL              0 (print)
             26 LOAD_CONST               3 ('After')
             28 LOAD_GLOBAL              1 (locals)
             30 CALL_FUNCTION            0
             32 CALL_FUNCTION            2
             34 POP_TOP
             36 LOAD_CONST               0 (None)
             38 RETURN_VALUE
Run Code Online (Sandbox Code Playgroud)

有几种不同的LOAD_*操作码用于检索各种值。LOAD_GLOBAL用于全局范围内的名称;LOAD_CONST用于未分配给任何名称的本地值。LOAD_FAST用于局部变量。局部变量甚至不是通过名称存在的,而是通过数组中的索引存在的。这就是为什么它们“快”;它们以数组而不是哈希表的形式提供。(LOAD_GLOBAL也使用整数参数,但这只是名称数组的索引;名称本身仍然需要在提供全局范围的任何映射中查找。)

您甚至可以查看与 关联的常量和局部值f

>>> f.__code__.co_consts
(None, 'Before', 2, 'After')
>>> f.__code__.co_varnames
('x',)
Run Code Online (Sandbox Code Playgroud)

LOAD_CONST 1Before放入堆栈 因为f.__code__.co_consts[1] == 'Before',并将LOAD_FAST 0的值x放入堆栈 因为f.__code__.co_varnames[0] == 'x'

这里的关键是字节码是在执行之前生成的。 fPython 并不是简单地在第一次看到每一行时就执行它。def除其他事项外,执行该声明还涉及:

  1. 阅读源代码
  2. 解析为抽象语法树(AST)
  3. 使用整个AST 生成存储在__code__函数对象属性中的字节码。

代码生成的一部分是注意到 namex是一个本地名称,由于函数体中某处的赋值(即使该函数在逻辑上无法访问),因此必须使用 来访问LOAD_FAST

在调用时locals(实际上LOAD_FAST 0是第一次使用 before ),尚未对(ie, ) 进行赋值,因此槽 0 中没有本地值可供查找。xSTORE_FAST 0