如何在Python中解析对变量的引用

How references to variables are resolved in Python

这个消息有很多例子,有点长,但我希望如此将帮助我和其他人更好地掌握变量的完整故事以及python 2.7中的属性查找。

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

我使用变量这个术语来命名那些不需要需要用对象限定的名称的点和属性名称(例如对象obj的属性x的obj.x)。

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

  • 局部的
  • 全球的
  • 内建

在python中,只有四个块用于函数(根据PEP 227):

  • 局部的
  • 封闭函数
  • 全球的
  • 内建

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

  • 变量与块中对象的任何绑定都会使该变量除非变量声明为全局变量(在如果变量属于全局范围)
  • 使用规则lgb(local,全局,内置)用于所有块,但函数除外
  • 使用规则legb(local,封闭、全局、内置)仅用于函数。

让我知道,举个例子来验证这个规则,并展示许多特殊情况。对于每个例子,我将给出我的理解。拜托如果我错了就纠正我。最后一个例子,我不理解结果。

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
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:

1
2
3
4
5
6
7
8
9
10
11
12
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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"如果一个名字绑定操作发生在代码块内的任何位置,所有名称的使用在块内被视为对当前块的引用。"is当代码块是类时出错。而且,对于班级来说本地名称绑定不是在编译时进行的,而是在使用类命名空间执行。在这方面,PEP227和python文档中的执行模型具有误导性,并且有些部分出错。

例5:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 in module"。那么myClass的本地属性x是在类字典中创建并指向字符串对象。

例6:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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引用在最里面的功能?


在一个理想的世界里,你是对的,你发现的一些矛盾是错误的。然而,cpython优化了一些场景,特别是函数局部变量。这些优化,连同编译器和评估循环如何交互以及历史先例,都会导致混淆。好的。

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

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

现在,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中,只有在调用f()之后,才执行C,而不是之前。好的。

让我们来看看你的例子:好的。

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

  • 这里f().C.z在类范围内,因此函数f().C().g()将跳过C范围,而使用LOAD_DEREF来查看f()范围。好的。

  • 这里var被编译器确定为本地的,因为您在作用域内分配给它。函数被优化,因此使用LOAD_FAST查找本地函数,并引发异常。好的。

  • 现在事情变得有点奇怪了。class A是在类范围内执行的,因此正在使用LOAD_NAMEA.x从本地字典中删除了作用域,因此第二次访问x会导致找到全局xLOAD_NAME首先查找本地,但没有在那里找到,返回到全局查找。好的。

    是的,这似乎与文档不一致。python语言和cpython实现在这里有点冲突。但是,您正在推动动态语言中可能的和实际的边界;检查x是否应该是LOAD_NAME中的一个本地实例是可能的,但对于大多数开发人员永远不会遇到的角点案例来说,需要宝贵的执行时间。好的。

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

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

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

  • 你可以把5和6之间的混淆看作一个bug,尽管在我看来这不值得修正。它当然是这样归档的,参见第532860期的python bug tracker,它已经存在了10多年了。好的。

    编译器可以检查作用域名称x,即使x也是本地的,对于示例5中的第一个赋值。或者,LOAD_NAME可以检查名称是否是本地的,如果没有找到本地的,可以抛出UnboundLocalError,代价是性能更高。如果它在一个函数范围内,那么会使用LOAD_FAST,例如5,并且会立即抛出UnboundLocalError。好的。

    但是,正如引用的bug所示,出于历史原因,行为被保留。如果修复了这个错误,今天可能有代码会被破解。好的。好啊。


    换句话说,例5和例6之间的区别在于,在例5中,变量x也被分配到相同的范围内,而不是在例6中。这引发了一种可以用历史原因来理解的差异。

    这将引发UnboundLocalError:

    1
    2
    3
    4
    5
    x ="foo"
    def f():
        print x
        x = 5
    f()

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

    1
    2
    3
    4
    5
    6
    x ="foo"
    def f():
        if some_condition:
            x = 42
        print x
    f()

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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的参考。如果我们认为新的字节码终究应该被修复,那么我们可能有机会重新进行一些讨论(bug报告考虑取消对x = x的支持,但由于担心破坏太多现有代码而被关闭;相反,我在这里建议在更多情况下使x = x工作)。或者我可能错过了另一个要点…

    edit2:似乎cpython在当前的3.4主干中做到了:http://bugs.python.org/issue17853…或不是?他们引入字节码的原因略有不同,并且没有系统地使用它…


    长话短说,这是Python范围界定的一个小例子,有点不一致,但为了向后兼容必须保持(因为还不清楚正确的答案应该是什么)。在执行PEP 227时,您可以在python邮件列表中看到很多关于它的原始讨论,并且在bug中可以看到一些这样的行为是可以修复的。

    我们可以使用dis模块来解决为什么会有差异,该模块允许我们查看代码对象,以查看编译后的一段代码的字节码。我在python 2.6上,所以细节可能稍有不同——但我看到了相同的行为,所以我认为它很可能接近2.7。

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    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的字节码中看到它们之间的区别:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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_NAMEop加载,而在MyClass2中,它使用LOAD_DEREF加载。LOAD_DEREF在一个封闭范围中查找一个名称,所以它在myfunc中得到了'x'。LOAD_NAME不遵循嵌套作用域—因为它看不到myfuncf1中绑定的x名称,所以它得到模块级绑定。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    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而失败:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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.来显式地访问它们(如果没有实例级绑定,则返回到类中)。