【问题标题】:How references to variables are resolved in Python如何在 Python 中解析对变量的引用
【发布时间】:2013-12-13 08:01:15
【问题描述】:

这个消息有点长,有很多例子,但我希望它 将帮助我和其他人更好地掌握变量的全部内容 和 Python 2.7 中的属性查找。

我使用的是 PEP 227 的条款 (http://www.python.org/dev/peps/pep-0227/) 用于代码块(例如 模块、类定义、函数定义等)和 变量绑定(例如赋值、参数声明、类 和函数声明,for循环等)

我将术语变量用于可以在没有名称的情况下调用的名称 点,以及需要用对象限定的名称的属性 名称(如对象 obj 的属性 x 为 obj.x)。

所有代码块在 Python 中都有三个作用域,但函数:

  • 本地
  • 全球
  • 内置

Python 中有四个块仅用于函数(根据 PEP 227):

  • 本地
  • 封闭函数
  • 全球
  • 内置

变量绑定并在块中找到它的规则是 很简单:

  • 任何将变量绑定到块中的对象都会使该变量 此块的本地变量,除非变量被声明为全局变量(在那个 如果变量属于全局范围)
  • 使用规则 LGB(本地, 全局,内置)适用于所有块,但函数
  • 使用规则 LEGB(本地, 封闭、全局、内置)仅用于函数。

让我知道以验证此规则的示例为例,并展示了许多 特别案例。对于每个例子,我都会给出我的理解。请 如果我错了,请纠正我。对于最后一个例子,我不明白 结果。

示例 1:

x = "x in module"
class A():
    print "A: "  + x                    #x in module
    x = "x in class A"
    print locals()
    class B():
        print "B: " + x                 #x in module
        x = "x in class B"
        print locals()
        def f(self):
            print "f: " + x             #x in module
            self.x = "self.x in f"
            print x, self.x
            print locals()

>>>A.B().f()
A: x in module
{'x': 'x in class A', '__module__': '__main__'}
B: x in module
{'x': 'x in class B', '__module__': '__main__'}
f: x in module
x in module self.x in f
{'self': <__main__.B instance at 0x00000000026FC9C8>}

类(规则 LGB)和函数没有嵌套范围 一个类不能访问该类的属性而不使用 限定名称(本例中为 self.x)。这在 PEP227。

示例 2:

z = "z in module"
def f():
    z = "z in f()"
    class C():
        z = "z in C"
        def g(self):
            print z
            print C.z
    C().g()
f()
>>> 
z in f()
z in C

这里使用 LEGB 规则查找函数中的变量,但如果 一个类在路径中,类参数被跳过。又是在这里, 这就是 PEP 227 所解释的。

示例 3:

var = 0
def func():
    print var
    var = 1
>>> func()

Traceback (most recent call last):
  File "<pyshell#102>", line 1, in <module>
func()
  File "C:/Users/aa/Desktop/test2.py", line 25, in func
print var
UnboundLocalError: local variable 'var' referenced before assignment

我们期望像 python 这样的动态语言,一切都是 动态解决。但对于函数来说,情况并非如此。当地的 变量是在编译时确定的。 PEP 227 和 http://docs.python.org/2.7/reference/executionmodel.html 描述这个 这种行为方式

"如果名称绑定操作发生在代码块中的任何位置,则所有 块内名称的使用被视为对 当前区块。”

示例 4:

x = "x in module"
class A():
    print "A: " + x
    x = "x in A"
    print "A: " + x
    print locals()
    del x
    print locals()
    print "A: " + x
>>> 
A: x in module
A: x in A
{'x': 'x in A', '__module__': '__main__'}
{'__module__': '__main__'}
A: x in module

但是我们在这里看到 PEP227 中的这条语句“如果名称绑定 操作发生在代码块中的任何位置,名称的所有使用 块内被视为对当前块的引用。”是 当代码块是一个类时出错。此外,对于课程,似乎 本地名称绑定不是在编译时进行的,而是在 使用类命名空间执行。在这方面, PEP227 和 Python 文档中的执行模型具有误导性,并且对于 有些地方不对。

示例 5:

x = 'x in module'
def  f2():
    x = 'x in f2'
    def myfunc():
        x = 'x in myfunc'
        class MyClass(object):
            x = x
            print x
        return MyClass
    myfunc()
f2()
>>> 
x in module

我对这段代码的理解如下。指令 x = x 首先查找表达式右手 x 所指的对象 到。在这种情况下,在类中本地查找对象,然后 遵循 LGB 规则,在全局范围内查找它,即 字符串“模块中的 x”。那么 MyClass 的局部属性 x 是 在类字典中创建并指向字符串对象。

示例 6:

现在这是一个我无法解释的例子。 它非常接近示例 5,我只是在更改本地 MyClass 从 x 到 y 的属性。

x = 'x in module'
def  f2():
    x = 'x in f2'
    def myfunc():
        x = 'x in myfunc'
        class MyClass(object):
            y = x
            print y
        return MyClass
    myfunc()
f2()
>>>
x in myfunc

为什么在这种情况下 MyClass 中的 x 引用在 最里面的功能?

【问题讨论】:

  • 很难用缩进的方式说出最后几个示例中应该发生的事情 - 你能解决它吗? (请记住,4 个空格的缩进会创建一个代码块 - 之后的每个空格在代码示例中都显示为空格)。
  • 这看起来是一个非常有趣的问题,但请修正缩进。
  • @SeanVieira 感谢您的关注。我有很多制表符而不是空格。现在已经修复了。
  • 一个 优秀 问题 - 我希望它可以投票 10 次,但是 +1 直到我可以!
  • 作为一个编写了很多 python 却没有解构 PEP(从不擅长阅读用户手册)的人说话,示例 6 对我来说似乎很直观,而示例 5 似乎违反了最小意外原则——不是反过来。类作用域应该离开函数作用域行为并检查全局作用域 before 封闭作用域,这似乎是不直观的。毫无疑问,有(历史?)原因。但是触发它的特定示例,x=x 无论如何都是一个非常讨厌的、不可维护的习语。

标签: python variables python-2.7 scope python-internals


【解决方案1】:

一句话,示例5和示例6的区别在于,示例5中变量x也被赋值在同一个作用域内,而示例6中没有。这触发了一个历史可以理解的差异原因。

这会引发 UnboundLocalError:

x = "foo"
def f():
    print x
    x = 5
f()

而不是打印“foo”。这有点道理,即使一开始看起来很奇怪:函数 f() 在本地定义变量 x,即使它是在打印之后,所以在同一个函数中对 x 的任何引用都必须是到那个局部变量。至少这是有道理的,因为如果您错误地在本地重用了全局变量的名称,并且尝试同时使用全局变量和局部变量,它可以避免奇怪的惊喜。这是一个好主意,因为这意味着我们可以静态地知道,只需查看一个变量,是哪个变量。例如,我们知道print x 在这里指的是局部变量(因此可能会引发 UnboundLocalError):

x = "foo"
def f():
    if some_condition:
        x = 42
    print x
f()

现在,这条规则不适用于类级别的范围:在那里,我们希望像 x = x 这样的表达式能够工作,将全局变量 x 捕获到类级别的范围中。这意味着类级作用域不遵循上述基本规则:我们无法知道此作用域中的x 是指某个外部变量还是本地定义的x --- 例如:

class X:
    x = x     # we want to read the global x and assign it locally
    bar = x   # but here we want to read the local x of the previous line

class Y:
    if some_condition:
        x = 42
    print x     # may refer to either the local x, or some global x

class Z:
    for i in range(2):
        print x    # prints the global x the 1st time, and 42 the 2nd time
        x = 42

所以在类作用域中,使用了不同的规则:它通常会引发 UnboundLocalError --- 并且仅在这种情况下 --- 它改为在模块全局变量中查找。就是这样:它不遵循嵌套范围链。

为什么不呢?我实际上怀疑“出于历史原因”是否有更好的解释。用更专业的术语来说,它可以认为变量x 是在类范围内本地定义的(因为它被分配给)应该作为词法嵌套变量从父范围传入(因为它被读取)。可以通过使用与在本地范围内查找的 LOAD_NAME 不同的字节码来实现它,如果找不到则回退到使用嵌套范围的引用。

编辑:感谢 wilberforce 对 http://bugs.python.org/issue532860 的引用。如果我们认为应该修复它,我们可能有机会重新激活提议的新字节码的一些讨论(错误报告考虑终止对x = x 的支持,但由于担心破坏太多现有代码而被关闭;取而代之的是我在这里的建议是让x = x 在更多情况下工作)。或者我可能错过了另一个要点......

EDIT2: 似乎 CPython 在当前的 3.4 主干中确实做到了这一点:http://bugs.python.org/issue17853 ... 还是没有?他们引入字节码的原因略有不同,并且没有系统地使用它......

【讨论】:

  • 一个关键的区别是函数体中的语句和表达式是在函数被调用时计算的; class 语句主体中的语句和表达式作为class 语句的一部分(即更早)执行。在函数中,some_condition 可能为真,也可能不是,因此x 每次都需要具有相同的作用域。 class 语句执行一次,所以在到达 print x 时,我们知道 x 是本地的还是全局的。
  • 我的观点是,在函数中,我们知道 x 是在字节码编译器运行时是本地的还是全局的,然后才评估任何东西。可以使用class 主体中的for 循环绘制更复杂的示例,其中相同变量x 的相同使用并不总是解析为相同的范围。
【解决方案2】:

长话短说,这是 Python 范围界定的一个极端情况,它有点不一致,但为了向后兼容必须保留(并且因为不清楚正确答案应该是什么)。在实施 PEP 227 时,您可以在 Python 邮件列表中看到很多关于它的 original discussion,并且在 bug 中可以看到一些关于此行为的修复。

我们可以使用dis 模块找出不同的原因,它可以让我们查看代码对象的内部,以查看一段代码已编译成的字节码。我使用的是 Python 2.6,所以这方面的细节可能略有不同 - 但我看到了相同的行为,所以我认为它可能足够接近 2.7。

初始化每个嵌套MyClass 的代码位于一个代码对象中,您可以通过顶级函数的属性访问该代码对象。 (我将示例 5 和示例 6 中的函数分别重命名为 f1f2。)

代码对象有一个co_consts 元组,其中包含myfunc 代码对象,而该代码对象又具有在创建MyClass 时运行的代码:

In [20]: f1.func_code.co_consts
Out[20]: (None,
 'x in f2',
 <code object myfunc at 0x1773e40, file "<ipython-input-3-6d9550a9ea41>", line 4>)
In [21]: myfunc1_code = f1.func_code.co_consts[2]
In [22]: MyClass1_code = myfunc1_code.co_consts[3]
In [23]: myfunc2_code = f2.func_code.co_consts[2]
In [24]: MyClass2_code = myfunc2_code.co_consts[3]

然后你可以使用dis.dis在字节码中看到它们之间的区别:

In [25]: from dis import dis
In [26]: dis(MyClass1_code)
  6           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  7           6 LOAD_NAME                2 (x)
              9 STORE_NAME               2 (x)

  8          12 LOAD_NAME                2 (x)
             15 PRINT_ITEM          
             16 PRINT_NEWLINE       
             17 LOAD_LOCALS         
             18 RETURN_VALUE        

In [27]: dis(MyClass2_code)
  6           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  7           6 LOAD_DEREF               0 (x)
              9 STORE_NAME               2 (y)

  8          12 LOAD_NAME                2 (y)
             15 PRINT_ITEM          
             16 PRINT_NEWLINE       
             17 LOAD_LOCALS         
             18 RETURN_VALUE        

所以唯一的区别是在MyClass1 中,x 使用LOAD_NAME 操作加载,而在MyClass2 中,它使用LOAD_DEREF 加载。 LOAD_DEREF 在封闭范围内查找名称,因此它得到'x in myfunc'。 LOAD_NAME 不遵循嵌套范围 - 因为它看不到绑定在 myfuncf1 中的 x 名称,所以它获取模块级绑定。

那么问题来了,为什么MyClass的两个版本的代码会被编译成两个不同的操作码呢?在f1 中,绑定在类范围内隐藏x,而在f2 中,它绑定了一个新名称。如果MyClass 范围是嵌套函数而不是类,则f2 中的y = x 行将被编译为相同,但f1 中的x = x 将是LOAD_FAST - 这是因为编译器将知道x 绑定在函数中,所以它应该使用LOAD_FAST 来检索局部变量。这会在调用时失败并显示 UnboundLocalError

In [28]:  x = 'x in module'
def  f3():
    x = 'x in f2'
    def myfunc():
        x = 'x in myfunc'
        def MyFunc():
            x = x
            print x
        return MyFunc()
    myfunc()
f3()
---------------------------------------------------------------------------
Traceback (most recent call last)
<ipython-input-29-9f04105d64cc> in <module>()
      9         return MyFunc()
     10     myfunc()
---> 11 f3()

<ipython-input-29-9f04105d64cc> in f3()
      8             print x
      9         return MyFunc()
---> 10     myfunc()
     11 f3()

<ipython-input-29-9f04105d64cc> in myfunc()
      7             x = x
      8             print x
----> 9         return MyFunc()
     10     myfunc()
     11 f3()

<ipython-input-29-9f04105d64cc> in MyFunc()
      5         x = 'x in myfunc'
      6         def MyFunc():
----> 7             x = x
      8             print x
      9         return MyFunc()

UnboundLocalError: local variable 'x' referenced before assignment

这会失败,因为 MyFunc 函数然后使用 LOAD_FAST

In [31]: myfunc_code = f3.func_code.co_consts[2]
MyFunc_code = myfunc_code.co_consts[2]
In [33]: dis(MyFunc_code)
  7           0 LOAD_FAST                0 (x)
              3 STORE_FAST               0 (x)

  8           6 LOAD_FAST                0 (x)
              9 PRINT_ITEM          
             10 PRINT_NEWLINE       
             11 LOAD_CONST               0 (None)
             14 RETURN_VALUE        

(顺便说一句,作用域与类主体中的代码和函数中的代码交互的方式应该有所不同,这并不奇怪。您可以告诉这一点,因为类级别的绑定不可用在方法中 - 方法范围不像嵌套函数那样嵌套在类范围内。您必须通过类显式访问它们,或者使用self.(如果没有也将回退到类实例级绑定)。)

【讨论】:

  • 伙计,这一次运气不好——我把所有这些工作都放在了一个答案中,还有谁会加入?最快的 Python 实现的主要开发人员之一,为了兼容性,他不得不复制许多像这样的 CPython 怪癖。还有拥有 182k 的 Martijn Pieters,因为他大概总是给出像他在这里所做的那样出色的答案。哦,好吧!
【解决方案3】:

在一个理想的世界里,你是对的,而你发现的一些不一致是错误的。但是,CPython 已经优化了一些场景,特别是函数局部变量。这些优化,以及编译器和评估循环如何交互以及历史先例,导致了混乱。

Python 将代码翻译成字节码,然后由解释器循环解释。访问名称的“常规”操作码是LOAD_NAME,它可以像在字典中一样查找变量名称。 LOAD_NAME 将首先查找本地名称,如果失败,则查找全局名称。找不到名称时,LOAD_NAME 会抛出 NameError 异常。

对于嵌套范围,在当前范围之外查找名称是使用闭包实现的;如果名称未分配给但在嵌套(非全局)范围内可用,则此类值将作为闭包处理。这是必需的,因为父作用域可以在不同时间为给定名称保存不同的值;对父函数的两次调用可能导致不同的闭包值。所以对于这种情况,Python 有 LOAD_CLOSUREMAKE_CLOSURELOAD_DEREF 操作码;前两个操作码用于加载和创建嵌套作用域的闭包,LOAD_DEREF 将在嵌套作用域需要时加载封闭值。

现在,LOAD_NAME 比较慢;它将查阅两个字典,这意味着它必须首先对密钥进行哈希处理并运行一些相等性测试(如果名称没有被实习)。如果名称不是本地名称,则必须为全局名称再次执行此操作。对于可能被调用数万次的函数,这可能会很快变得乏味。所以函数局部变量有特殊的操作码。加载本地名称由LOAD_FAST 实现,它在特殊的本地名称数组中按索引 查找本地变量。这要快得多,但它确实要求编译器首先必须查看名称是否是本地的而不是全局的。为了仍然能够查找全局名称,使用了另一个操作码 LOAD_GLOBAL。编译器针​​对这种情况显式优化以生成特殊操作码。当名称还没有值时,LOAD_FAST 将抛出 UnboundLocalError 异常。

另一方面,类定义体虽然被视为函数,但没有进行此优化步骤。类定义并不意味着经常被调用。大多数模块在导入时创建类一次。嵌套时也不计算类范围,因此规则更简单。因此,当您开始稍微混合作用域时,类定义主体的行为不像函数。

因此,对于非函数作用域,LOAD_NAMELOAD_DEREF 分别用于局部变量和全局变量以及闭包。对于函数,使用LOAD_FASTLOAD_GLOBALLOAD_DEREF 代替。

请注意,一旦 Python 执行 class 行,类主体就会被执行!因此,在示例 1 中,class A 中的 class B 会在 class A 执行后立即执行,也就是您导入模块的时候。在示例 2 中,C 在调用 f() 之前不会执行,而不是之前。

让我们来看看你的例子:

  1. 您已将类 A.B 嵌套在类 A 中。类体不形成嵌套作用域,因此即使在执行类A 时执行了A.B 类体,编译器也会使用LOAD_NAME 查找xA.B().f() 是一个函数(绑定到B() 实例作为方法),所以它使用LOAD_GLOBAL 来加载x。我们将在这里忽略属性访问,这是一个非常明确的名称模式。

  2. 这里f().C.z 是在类范围内,所以函数f().C().g() 将跳过C 范围,而是使用LOAD_DEREF 查看f() 范围。

  3. 这里 var 被编译器确定为本地,因为您在范围内分配给它。函数进行了优化,所以使用LOAD_FAST查找本地并抛出异常。

  4. 现在事情变得有点奇怪了。 class A 在类范围内执行,因此正在使用 LOAD_NAMEA.x 已从范围的本地字典中删除,因此对 x 的第二次访问会导致找到全局 xLOAD_NAME 先找了一个本地的,没找到,回退到全局查找。

    是的,这似乎与文档不一致。 Python 语言和 CPython 实现在这里有点冲突。但是,您正在推动动态语言的可能性和实用性的界限;检查 x 是否应该是 LOAD_NAME 中的本地是可能的,但对于大多数开发人员永远不会遇到的极端情况会花费宝贵的执行时间。

  5. 现在你混淆了编译器。您在类范围内使用了x = x,因此您正在从范围之外的名称设置 local。编译器发现 x 在这里是一个本地的(你分配给它),所以它从不认为它也可能是一个作用域名称。编译器使用LOAD_NAME 表示此范围内对x 的所有引用,因为这不是优化的函数体。

    在执行类定义时,x = x 首先要求您查找x,因此它使用LOAD_NAME 来执行此操作。没有定义 xLOAD_NAME 没有找到本地,所以找到了 global x。结果值存储为本地值,恰好也被命名为xprint x 再次使用LOAD_NAME,现在找到新的本地x 值。

  6. 在这里您没有混淆编译器。您正在创建一个本地 yx 不是本地的,因此编译器将其识别为父函数 f2().myfunc() 的作用域名称。 x 从闭包中与 LOAD_DEREF 一起查找,并存储在 y 中。

您可以将 5 和 6 之间的混淆视为错误,尽管在我看来不值得修复。它肯定是这样归档的,请参阅 Python 错误跟踪器中的 issue 532860,它已经存在 10 多年了。

编译器可以检查范围名称x,即使x也是本地的,对于示例5中的第一个赋值。或者LOAD_NAME可以检查名称是否意味着成为本地人,真的,如果没有找到本地人,则抛出UnboundLocalError,以牺牲更多性能为代价。如果这是在函数范围内,LOAD_FAST 将被用作示例 5,并且将立即抛出 UnboundLocalError

但是,正如引用的错误所示,由于历史原因,该行为被保留。如果修复了这个错误,今天可能有代码会中断。

【讨论】:

  • 感谢您的解释。但是,我仍然不明白为什么在示例 6 中, x 被认为是一个作用域名称。 x 是从一个类中引用的,我的理解是,在这种情况下,它应该在本地查找,然后是全局查找。此外,PEP 227 应该仅适用于函数,而不适用于类,我们在示例 1 中看到类不使用封闭范围查找。示例 6 中有什么特别之处可以解释类中的引用变量被视为作用域变量?
  • 不,PEP 227 也适用于类定义;唯一的例外是类定义不构成嵌套代码块的范围。所以在示例 1 中,类 A 定义不计为类 B 中名称的父作用域,这就是为什么 B 中的 x 不使用 A.x 而是查看全局 x .在示例 6 中,父函数 确实 形成了一个范围,因此从该范围查找 x。这里唯一的例外是示例 5,其中 x 实际上是 B 中的本地,而 LOAD_NAME 操作码用于处理此问题。 x = x完全起作用这一事实可能是一个错误。
  • 好吧,我错过了这个微妙之处,并错误地认为如果类不形成嵌套代码块的范围,它们会完全逃避范围名称解析。这肯定既不简单、直观,也没有很好的文档记录。这些问题值得举例,我希望我的帖子能在这方面有所帮助。感谢您的所有澄清。
猜你喜欢
  • 1970-01-01
  • 2013-08-24
  • 1970-01-01
  • 2012-06-28
  • 2021-07-17
  • 1970-01-01
相关资源
最近更新 更多