代码之家  ›  专栏  ›  技术社区  ›  Ashish Chourasia

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

  •  2
  • Ashish Chourasia  · 技术社区  · 2 年前
    def f(): 
        print("Before", locals())   # line 2
        print(x);                   # line 3
        x = 2                       # line 4
        print("After", locals())    # line 5
    
    x = 1
    f()
    

    我知道Python中作用域的LEGB规则。

    对于上述代码,当我 ,一切正常执行:对于第3行,python没有找到变量 x 在局部范围内,因此在全局范围内搜索它,在全局范围内找到它并打印1。

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

    我知道我可以使用非局部和全局,但我的问题是:

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

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

    2 回复  |  直到 2 年前
        1
  •  7
  •   chepner    2 年前

    在某种程度上,答案是特定于实现的,因为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
    

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

    您甚至可以看到与 f :

    >>> f.__code__.co_consts
    (None, 'Before', 2, 'After')
    >>> f.__code__.co_varnames
    ('x',)
    

    LOAD_CONST 1 Before 在堆栈上,因为 f.__code__.co_consts[1] == 'Before' LOAD_FAST 0 x 在堆栈上,因为 f.__code__.co_varnames[0] == 'x' .

    这里的关键是生成字节码 之前 f 被执行过。Python不仅仅是在第一次看到每一行时就执行它。执行 def

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

    代码生成的一部分是注意名称 x 由于函数体中某处的赋值(即使该函数在逻辑上不可访问),是一个本地名称,因此必须使用 快速加载 .

    当时 locals 被称为(实际上是之前 是第一次使用),否 分配 x STORE_FAST 0 )已经制作好了,所以

        2
  •  0
  •   Aniket Jain    2 年前

    因为你在 f() 函数调用,

    让我们试试这个:

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