如何在Python中解析对变量的引用
这个消息有很多例子,但我希望它能帮助我和其他人更好地理解Python 2.7中变量和属性查找的全部内容。
我使用PEP 227(http://www.python.org/dev/peps/pep-0227/)的术语来描述代码块(例如模块,类定义,函数定义等)和变量绑定(比如作为赋值,参数声明,类和函数声明,for循环等)
我使用变量来表示可以不带点的名称,以及需要使用对象名称限定的名称的属性(例如对象obj的属性x的obj.x)。
Python中有三个范围用于所有代码块,但功能如下:
Python中只有四个块用于功能(根据PEP 227):
将变量绑定到块中并在块中查找的规则非常简单:
让我知道验证这条规则的例子,并展示许多特殊情况。 对于每个例子,我会给我理解。 如果我错了,请纠正我。 对于最后一个例子,我不明白结果。
例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引用在最内层函数中查找?
换句话说,例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
在更多情况下工作)。 或者我可能会错过另一个优点...
编辑2:似乎CPython正是在目前的3.4主干中做到的:http://bugs.python.org/issue17853 ...或者不是? 他们引入了字节码的原因稍有不同,不要系统地使用它...
在理想的世界中,你会是对的,而且你发现的一些不一致是错误的。 但是,CPython已经优化了一些场景,特别是功能本地人。 这些优化以及编译器和评估循环如何相互作用以及历史先例会导致混淆。
Python将代码转换为字节码,然后由解释器循环解释。 用于访问名称的“常规”操作码是LOAD_NAME
,它会像在字典中那样查找变量名称。 LOAD_NAME
将首先查找本地名称,如果失败,则查找全球名称。 找不到名称时, LOAD_NAME
将引发NameError
异常。
对于嵌套范围,在当前范围外查找名称是使用闭包实现的; 如果名称未分配给嵌套(不是全局)范围,而是可用,则这些值将作为闭包处理。 这是必需的,因为父范围可以在不同时间为给定名称保存不同的值; 对父函数的两个调用可能导致不同的闭包值。 所以Python对于这种情况具有LOAD_CLOSURE
, MAKE_CLOSURE
和LOAD_DEREF
操作码; 前两个操作码用于加载和创建嵌套范围的闭包,当嵌套范围需要时, LOAD_DEREF
将加载闭包值。
现在, LOAD_NAME
相对较慢; 它会查询两个字典,这意味着它必须首先对关键字进行哈希处理,然后运行一些相等测试(如果名称未被限制)。 如果名称不是本地的,那么它必须为全局再次执行此操作。 对于可能被称为成千上万次的函数,这可能会很快乏味。 所以功能本地人有特殊的操作码。 加载本地名称由LOAD_FAST
实现, LOAD_FAST
通过在特殊本地名称数组中的索引查找局部变量。 这要快得多,但它确实要求编译器首先必须查看名称是否是本地名称而不是全局名称。 为了仍然能够查找全局名称,使用了另一个操作码LOAD_GLOBAL
。 编译器明确优化这种情况来生成特殊的操作码。 当名称没有值时, LOAD_FAST
将引发UnboundLocalError
异常。
另一方面,类定义体,尽管它们看起来很像一个函数,但不会得到这个优化步骤。 类定义并不意味着经常被调用; 大多数模块在导入时会创建一次类。 当嵌套时,类作用域不计算,所以规则更简单。 因此,当您开始混合示波器时,类定义体不会像函数那样工作。
因此,对于非函数范围, LOAD_NAME
和LOAD_DEREF
分别用于本地和全局,以及用于关闭。 对于函数,则使用LOAD_FAST
, LOAD_GLOBAL
和LOAD_DEREF
。
请注意,只要Python执行class
行,就会执行class
! 所以实例1中, class B
中class A
被尽快执行class A
执行,这是当您导入模块。 在例2中,直到调用f()
之前C
才会被执行,而不是之前。
让我们来看看你的例子:
你在A
类中嵌套了一个AB
类。 类体不构成嵌套范围,因此即使在执行类A
时执行了AB
类体,编译器也将使用LOAD_NAME
来查找x
。 AB().f()
是一个函数(作为方法绑定到B()
实例),因此它使用LOAD_GLOBAL
来加载x
。 我们在这里忽略属性访问,这是一个非常明确的名称模式。
这里f().Cz
在类范围内,所以函数f().C().g()
会跳过C
范围并使用LOAD_DEREF
来查看f()
范围。
这里var
被编译器确定为本地的,因为你在范围内赋值了它。 函数进行了优化,因此LOAD_FAST
用于查找本地并抛出异常。
现在事情变得有点怪异。 class A
在类作用域执行,因此正在使用LOAD_NAME
。 Ax
从该范围的locals字典中删除,因此第二次访问x
导致找到全局x
; LOAD_NAME
首先寻找本地并且在那里找不到它,然后回到全局查找。
是的,这看起来与文档不一致。 Python-the-language和CPython-实现在这里有点冲突。 然而,你正在以动态的语言推动可能的和实用的界限; 检查x
是否应该是LOAD_NAME
的本地代码将是可能的,但是对于大多数开发人员永远不会遇到的角落案例来说,这需要宝贵的执行时间。
现在你很困惑编译器。 您在类作用域中使用了x = x
,因此您正在从作用域之外的名称设置本地。 编译器发现x
是本地的(你赋值给它),所以它从不认为它也可以是一个作用域名。 编译器在此作用域中对所有对x
引用使用LOAD_NAME
,因为这不是优化的函数体。
在执行类定义时, x = x
首先需要查找x
,因此它使用LOAD_NAME
来执行此操作。 没有定义x
, LOAD_NAME
找不到本地,因此找到全局x
。 结果值存储为本地,并且恰好也命名为x
。 print x
再次使用LOAD_NAME
,现在找到新的本地x
值。
在这里你不会混淆编译器。 您正在创建一个本地y
, x
不是本地的,因此编译器会将它识别为来自父函数f2().myfunc()
的作用域名称。 x
用闭包中的LOAD_DEREF
,并存储在y
。
你可以看到5和6之间的混淆作为一个错误,虽然这在我看来是不值得修复的。 它肯定是这样提交的,请参阅Python bug跟踪器中的问题532860,它已经存在了10多年了。
编译器可以检查范围名称x
即使x
也是本地的,例如第5个例子中的第一个任务。或者LOAD_NAME
可以检查名称是否是本地的,真的,如果没有找到本地的话就抛出一个UnboundLocalError
,而牺牲更多的性能。 如果这是在函数范围内, LOAD_FAST
将用于示例5,并且将立即抛出UnboundLocalError
。
但是,正如所引用的错误所示,由于历史原因,行为仍然存在。 今天可能有代码在那个问题修复后会破解。
长话短说,这是Python范围界定的一个有点不一致的情况,但必须保持向后兼容性(并且因为它不清楚正确的答案应该是什么)。 当PEP 227被实现时,你可以在Python邮件列表中看到很多关于它的原始讨论,还有一些在这种行为是修复的bug中。
我们可以计算出为什么使用dis
模块有所不同,它使我们可以查看代码对象的内部,以查看编译过的代码段的字节码。 我使用的是Python 2.6,所以细节可能会略有不同 - 但我看到了相同的行为,所以我认为它可能接近2.7。
初始化每个嵌套MyClass
的代码位于一个代码对象中,您可以通过顶级函数的属性获取该代码对象。 (我将例5和例6中的函数分别重命名为f1
和f2
)。
该代码对象有一个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
op加载,而在MyClass2
使用LOAD_DEREF
加载。 LOAD_DEREF
在封闭范围内查找名称,因此它在'myfunc'中得到'x'。 LOAD_NAME
不遵循嵌套范围 - 因为它无法看到myfunc
或f1
绑定的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.
明确地到达它们(如果没有实例级,它将回落到类中捆绑)。)
上一篇: How references to variables are resolved in Python
下一篇: Differences between class block and function block in python