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

use*_*222 49 python variables scope python-2.7 python-internals

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

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

我使用的术语变量,可以在不点叫名字,并需要与对象名称限定(如obj.x的属性x对象obj的)名称属性.

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>}
Run Code Online (Sandbox Code Playgroud)

存在用于类(排除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
Run Code Online (Sandbox Code Playgroud)

这里使用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
Run Code Online (Sandbox Code Playgroud)

我们希望用动态语言如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
Run Code Online (Sandbox Code Playgroud)

但是我们在这里看到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
Run Code Online (Sandbox Code Playgroud)

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

例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
Run Code Online (Sandbox Code Playgroud)

为什么在这种情况下,在最里面的函数中查找MyClass中的x引用?

Mar*_*ers 21

在一个理想的世界里,你是对的,你发现的一些不一致是错误的.但是,CPython已经优化了一些场景,特别是功能本地.这些优化以及编译器和评估循环如何相互作用以及历史先例会导致混淆.

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

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

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

另一方面,类定义体虽然它们被视为一个函数,但是没有得到这个优化步骤.类定义并不意味着经常被调用; 导入时,大多数模块都会创建一次类.嵌套时类范围也不计算,因此规则更简单.因此,当您开始稍微混合范围时,类定义主体不会像函数那样起作用.

因此,对于非功能范围,LOAD_NAMELOAD_DEREF分别用于当地人和全局,并为关闭.对于功能LOAD_FAST,LOAD_GLOBALLOAD_DEREF来代替.

请注意,只要Python执行该class行,就会执行类主体!因此在示例1中,只要执行class B内部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-the-language和CPython-实现在这里发生了一些冲突.但是,你正在用动态语言推动可能和实际的界限; 检查是否x应该是本地的,LOAD_NAME但是需要宝贵的执行时间来处理大多数开发人员永远不会遇到的问题.

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

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

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

你可以看到5和6之间的混淆是一个错误,虽然在我看来这是不值得修复的.它确实是这样提交的,请参阅Python bug跟踪器中的问题532860,它已存在超过10年了.

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

但是,正如引用的错误所示,由于历史原因,行为仍然保留.今天可能有代码可以解决这个错误.


Arm*_*igo 18

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

这会引发UnboundLocalError:

x = "foo"
def f():
    print x
    x = 5
f()
Run Code Online (Sandbox Code Playgroud)

而不是打印"foo".它有点意义,即使它起初看起来很奇怪:函数f()在x本地定义变量,即使它在打印之后,因此x在同一函数中的任何引用都必须是该局部变量.至少它是有道理的,因为如果你错误地在本地重用了全局变量的名称,并试图同时使用全局变量和局部变量,它可以避免奇怪的惊吓.这是一个好主意,因为这意味着我们可以通过查看变量来静态地知道它意味着哪个变量.例如,我们知道在这里print x引用局部变量(因此可能引发UnboundLocalError):

x = "foo"
def f():
    if some_condition:
        x = 42
    print x
f()
Run Code Online (Sandbox Code Playgroud)

现在,这个规则不适用于类级作用域:在那里,我们希望表达式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
Run Code Online (Sandbox Code Playgroud)

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

为什么不?我实际上怀疑有一个更好的解释"出于历史原因".在更多技术术语中,它可以认为变量x在类范围内是本地定义的(因为它被赋值),并且应该作为词汇嵌套变量从父作用域传入(因为它被读取).可以通过使用与LOAD_NAME在本地范围中查找的字节码不同的字节码来实现它,并且如果找不到则使用嵌套范围的引用.

编辑:感谢wilberforce参考http://bugs.python.org/issue532860.我们可能有机会用提议的新字节码重新激活一些讨论,如果我们认为它应该被修复(错误报告认为杀死支持x = x但因为害怕破坏过多的现有代码而被关闭;而是我的'我建议在这里做x = x更多的工作).或者我可能会错过另一个好点......

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


bab*_*unk 6

长话短说,这是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]
Run Code Online (Sandbox Code Playgroud)

然后你可以看到字节码中它们之间的区别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        
Run Code Online (Sandbox Code Playgroud)

因此,唯一的区别是,在MyClass1,x使用装载LOAD_NAME运,而在,MyClass2,它使用加载LOAD_DEREF.LOAD_DEREF在封闭范围中查找名称,因此它在myfunc中获得'x'.LOAD_NAME不遵循嵌套作用域 - 因为它无法看到x绑定的名称myfunc或者f1,它获得了模块级绑定.

那么问题是,为什么两个版本的代码MyClass被编译成两个不同的操作码?在f1绑定中是x类范围中的阴影,而在f2它的绑定中有一个新名称.如果MyClass作用域是嵌套函数而不是类,则y = x行的f2编译方式相同,但是x = xin f1会是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
Run Code Online (Sandbox Code Playgroud)

这失败,因为该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        
Run Code Online (Sandbox Code Playgroud)

(顺便说一句,在函数中类和主体中的代码与范围内的代码交互方式应该存在差异并不是一个大的惊喜.你可以这样说,因为类级别的绑定在方法中不可用 - 方法范围不像嵌套函数那样嵌套在类范围内.您必须通过类或使用显式地访问它们self.(如果还没有实例级绑定,它将回退到类) .)

  • 伙计,这个运气不好 - 我把所有这些工作都放到了一个答案中,还有谁跳进来?最快的Python实现的主要开发人员之一,为了兼容性,他们不得不复制许多像这样的CPython怪癖.和Martijn Pieters一样,拥有18.2万,因为他可能总能给出像他在这里所做的那样出色的答案.那好吧! (2认同)