54

这条消息有点长,有很多例子,但我希望它能帮助我和其他人更好地掌握 Python 2.7 中变量和属性查找的完整故事。

对于代码块(例如模块、类定义、函数定义等)和变量绑定(例如作为赋值、参数声明、类和函数声明、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

3 回答 3

22

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

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

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

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

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

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

请注意,一旦 Python 执行该class行,类体就会执行!所以在示例 1 中,class Binside一class A执行就class A执行,也就是你导入模块的时候。在示例 2 中,在调用之前C不执行,而不是之前。f()

让我们看看你的例子:

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

  2. f().C.z是在类范围内,因此该函数f().C().g()将跳过C范围并改为查看f()范围,使用LOAD_DEREF.

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

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

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

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

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

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

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

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

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

于 2013-11-27T19:27:21.570 回答
20

简而言之,示例 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 ... 他们出于稍微不同的原因引入了字节码,并且没有系统地使用它......

于 2013-11-27T18:58:29.263 回答
7

长话短说,这是 Python 范围界定的一个极端情况,它有点不一致,但必须保留以实现向后兼容性(并且因为不清楚正确答案应该是什么)。当 PEP 227 被实施时,你可以在 Python 邮件列表中看到很多关于它的原始讨论,以及一些在修复此行为的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        

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

那么问题来了,为什么两个版本的代码MyClass会被编译成两个不同的操作码呢?在f1绑定中是在类范围内的影子x,而在f2它中绑定一个新名称。如果MyClass作用域是嵌套函数而不是类,则y = xinf2将被编译为相同的行,但x = xinf1将是 a LOAD_FAST- 这是因为编译器会知道x函数中绑定了它,因此它应该使用 theLOAD_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.(如果没有实例级绑定,它将回退到类) .)

于 2013-11-27T19:18:37.370 回答