从类定义中的列表理解访问类variables
如何从类定义中的列表理解中访问其他类variables? 以下Python 2中的工作,但在Python 3中失败:
class Foo: x = 5 y = [x for i in range(1)]
Python 3.2给出的错误:
NameError: global name 'x' is not defined
尝试Foo.x
也不起作用。 任何想法如何在Python 3中做到这一点?
一个更复杂的激励例子:
from collections import namedtuple class StateDatabase: State = namedtuple('State', ['name', 'capital']) db = [State(*args) for args in [ ['Alabama', 'Montgomery'], ['Alaska', 'Juneau'], # ... ]]
在这个例子中, apply()
将是一个不错的解决方法,但是很遗憾从Python 3中删除了它。
类范围和列表,集或字典parsing,以及生成器expression式不混合。
为什么; 或者,这个官方的话
在Python 3中,列表parsing被赋予了一个适当的范围(本地名称空间),以防止它们的局部variables渗透到周围的范围(参见Python列表理解重新命名,即使在理解范围之后,是这样吗? 在模块或函数中使用这样一个列表理解是很好的,但是在类中,范围是有点呃, 奇怪的 。
这在文件227中有logging :
类范围中的名称不可访问。 名称在最内层的封闭函数范围中parsing。 如果在嵌套作用域链中出现类定义,则parsing过程将跳过类定义。
并在class
复合语句文档中 :
然后使用新创build的本地名称空间和原始全局名称空间,在新的执行框架中执行类的套件(请参见命名和绑定一节)。 (通常,套件只包含函数定义。)当类的套件完成执行时, 其执行框被丢弃,但是其本地名称空间被保存 。 [4]然后,使用基类的inheritance列表和属性字典的已保存本地名称空间创build类对象。
强调我的; 执行框架是临时范围。
因为作用域被重用为类对象的属性,所以它被用作作用域也会导致未定义的行为; 如果一个类方法将x
引用为嵌套的作用域variables,那么会发生什么,然后操纵Foo.x
,例如? 更重要的是, Foo
子类是什么意思呢? Python 必须以不同的方式处理类作用域,因为它与函数作用域非常不同。
最后,但绝对不是不重要的,执行模型文档中的链接命名和绑定部分明确提到了类作用域:
类块中定义的名称范围仅限于类块; 它不会扩展到方法的代码块 – 这包括理解和生成器expression式,因为它们是使用函数作用域实现的。 这意味着以下将失败:
class A: a = 42 b = list(a + i for i in range(10))
所以,总结一下:你不能从函数,列表理解或者范围内的生成器expression式访问类作用域; 他们的行为就好像这个范围不存在一样。 在Python 2中,列表parsing是使用快捷方式实现的,但是在Python 3中,它们有自己的函数范围(因为它们本来就应该有),因此你的例子中断了。
在引擎盖下看 或者,比你想要的更详细
你可以使用dis
模块来看到这一切。 我在下面的例子中使用了Python 3.3,因为它添加了合适的名字 ,可以很好地识别我们想要检查的代码对象。 生成的字节码在function上与Python 3.2相同。
为了创build一个类,Python本质上需要构成类体的整个套件(所以所有东西比class <name>:
line都缩进一层),并且像执行一个函数一样执行:
>>> import dis >>> def foo(): ... class Foo: ... x = 5 ... y = [x for i in range(1)] ... return Foo ... >>> dis.dis(foo) 2 0 LOAD_BUILD_CLASS 1 LOAD_CONST 1 (<code object Foo at 0x10a436030, file "<stdin>", line 2>) 4 LOAD_CONST 2 ('Foo') 7 MAKE_FUNCTION 0 10 LOAD_CONST 2 ('Foo') 13 CALL_FUNCTION 2 (2 positional, 0 keyword pair) 16 STORE_FAST 0 (Foo) 5 19 LOAD_FAST 0 (Foo) 22 RETURN_VALUE
第一个LOAD_CONST
在那里为Foo
类体加载一个代码对象,然后把它变成一个函数,然后调用它。 那个调用的结果然后用来创build类的名字空间,它的__dict__
。 到现在为止还挺好。
这里要注意的是,字节码包含一个嵌套的代码对象; 在Python中,类定义,函数,理解和生成器都被表示为不仅包含字节码的代码对象,而且还包含表示局部variables,常量,从全局variables中取出的variables以及从嵌套范围取得的variables的结构。 编译的字节码指的是那些结构,python解释器知道如何访问给出的字节码。
这里要记住的重要一点是Python在编译时创build这些结构。 class
套件是已编译的代码对象( <code object Foo at 0x10a436030, file "<stdin>", line 2>
)。
让我们检查一下创build类体的代码对象; 代码对象有一个co_consts
结构:
>>> foo.__code__.co_consts (None, <code object Foo at 0x10a436030, file "<stdin>", line 2>, 'Foo') >>> dis.dis(foo.__code__.co_consts[1]) 2 0 LOAD_FAST 0 (__locals__) 3 STORE_LOCALS 4 LOAD_NAME 0 (__name__) 7 STORE_NAME 1 (__module__) 10 LOAD_CONST 0 ('foo.<locals>.Foo') 13 STORE_NAME 2 (__qualname__) 3 16 LOAD_CONST 1 (5) 19 STORE_NAME 3 (x) 4 22 LOAD_CONST 2 (<code object <listcomp> at 0x10a385420, file "<stdin>", line 4>) 25 LOAD_CONST 3 ('foo.<locals>.Foo.<listcomp>') 28 MAKE_FUNCTION 0 31 LOAD_NAME 4 (range) 34 LOAD_CONST 4 (1) 37 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 40 GET_ITER 41 CALL_FUNCTION 1 (1 positional, 0 keyword pair) 44 STORE_NAME 5 (y) 47 LOAD_CONST 5 (None) 50 RETURN_VALUE
上面的字节码创build了类的主体。 该函数被执行,并且生成的包含x
和y
locals()
命名空间被用于创build类(除非它不工作,因为x
没有被定义为全局的)。 请注意,在x
存储5
之后,它会加载另一个代码对象; 这是列表理解; 它被封装在一个函数对象中,就像类的主体一样; 所创build的函数采用位置参数, range(1)
可循环用于其循环代码,投射到迭代器。
由此可以看出,函数或生成器的代码对象与理解的代码对象之间的唯一区别在于后者在父代码对象执行时立即执行; 字节码只是简单地创build一个函数,并在几个小步骤中执行它。
Python 2.x使用内联字节码,这里是Python 2.7的输出:
2 0 LOAD_NAME 0 (__name__) 3 STORE_NAME 1 (__module__) 3 6 LOAD_CONST 0 (5) 9 STORE_NAME 2 (x) 4 12 BUILD_LIST 0 15 LOAD_NAME 3 (range) 18 LOAD_CONST 1 (1) 21 CALL_FUNCTION 1 24 GET_ITER >> 25 FOR_ITER 12 (to 40) 28 STORE_NAME 4 (i) 31 LOAD_NAME 2 (x) 34 LIST_APPEND 2 37 JUMP_ABSOLUTE 25 >> 40 STORE_NAME 5 (y) 43 LOAD_LOCALS 44 RETURN_VALUE
没有加载代码对象,而是FOR_ITER
循环FOR_ITER
联方式运行。 所以在Python 3.x中,列表生成器被赋予了一个合适的代码对象,这意味着它有自己的范围。
然而,当解释器第一次加载模块或脚本时,理解与其余的python源代码一起被编译,并且编译器不认为类套件是有效的作用域。 列表理解中的任何引用variables都必须recursion地在类定义的范围内查找。 如果编译器找不到该variables,则将其标记为全局variables。 列表理解代码对象的反汇编显示x
确实被加载为全局:
>>> foo.__code__.co_consts[1].co_consts ('foo.<locals>.Foo', 5, <code object <listcomp> at 0x10a385420, file "<stdin>", line 4>, 'foo.<locals>.Foo.<listcomp>', 1, None) >>> dis.dis(foo.__code__.co_consts[1].co_consts[2]) 4 0 BUILD_LIST 0 3 LOAD_FAST 0 (.0) >> 6 FOR_ITER 12 (to 21) 9 STORE_FAST 1 (i) 12 LOAD_GLOBAL 0 (x) 15 LIST_APPEND 2 18 JUMP_ABSOLUTE 6 >> 21 RETURN_VALUE
这块字节码加载了第一个参数( range(1)
迭代器),就像Python 2.x版本使用FOR_ITER
来遍历它并创build它的输出。
如果我们在foo
函数中定义了x
, x
将是一个单元variables(单元格指的是嵌套的作用域):
>>> def foo(): ... x = 2 ... class Foo: ... x = 5 ... y = [x for i in range(1)] ... return Foo ... >>> dis.dis(foo.__code__.co_consts[2].co_consts[2]) 5 0 BUILD_LIST 0 3 LOAD_FAST 0 (.0) >> 6 FOR_ITER 12 (to 21) 9 STORE_FAST 1 (i) 12 LOAD_DEREF 0 (x) 15 LIST_APPEND 2 18 JUMP_ABSOLUTE 6 >> 21 RETURN_VALUE
LOAD_DEREF
将间接从代码对象单元格对象中加载x
:
>>> foo.__code__.co_cellvars # foo function `x` ('x',) >>> foo.__code__.co_consts[2].co_cellvars # Foo class, no cell variables () >>> foo.__code__.co_consts[2].co_consts[2].co_freevars # Refers to `x` in foo ('x',) >>> foo().y [2]
实际引用从当前帧数据结构查找值,这些数据结构是从函数对象的.__closure__
属性初始化的。 由于为理解代码对象创build的函数再次被丢弃,所以我们不检查函数的闭包。 要看到一个闭包,我们必须检查一个嵌套函数:
>>> def spam(x): ... def eggs(): ... return x ... return eggs ... >>> spam(1).__code__.co_freevars ('x',) >>> spam(1)() 1 >>> spam(1).__closure__ >>> spam(1).__closure__[0].cell_contents 1 >>> spam(5).__closure__[0].cell_contents 5
所以,总结一下:
- 列表parsing在Python 3中获得它们自己的代码对象,而对于函数,生成器或理解,代码对象之间没有区别; 理解代码对象被包装在一个临时函数对象中并立即调用。
- 代码对象是在编译时创build的,任何非本地variables都被标记为全局variables或自由variables,基于代码的嵌套范围。 类体不被视为查找这些variables的范围。
- 在执行代码时,Python只需要查看全局variables或当前正在执行的对象的closures。 由于编译器没有将作为范围的类主体包含在内,因此不考虑临时函数名称空间。
解决方法; 或者,该怎么办
如果要为x
variables创build一个明确的作用域,就像在一个函数中一样, 可以使用类作用域variables作为列表理解:
>>> class Foo: ... x = 5 ... def y(x): ... return [x for i in range(1)] ... y = y(x) ... >>> Foo.y [5]
“临时”函数可以直接调用; 我们用它的返回值replace它。 它的范围在parsingx
时被考虑:
>>> foo.__code__.co_consts[1].co_consts[2] <code object y at 0x10a5df5d0, file "<stdin>", line 4> >>> foo.__code__.co_consts[1].co_consts[2].co_cellvars ('x',)
当然,阅读你的代码的人会对此稍作挠头, 你可能想在这里解释你为什么这样做的一个大胖子的评论。
最好的解决办法是只用__init__
来创build一个实例variables:
def __init__(self): self.y = [self.x for i in range(1)]
并避免所有的头部划伤和问题来解释你自己。 以你自己的具体例子来说,我甚至不会把namedtuple
存储在课堂上。 直接使用输出(不要存储生成的类),或使用全局:
from collections import namedtuple State = namedtuple('State', ['name', 'capital']) class StateDatabase: db = [State(*args) for args in [ ('Alabama', 'Montgomery'), ('Alaska', 'Juneau'), # ... ]]
基本上这是Python 3中的问题。我希望他们改变它。
Bugged(在2.7中工作):
x = 4 y = [x+i for i in range(1)]
要解决它(以3+工作):
x = 4 y = (lambda x=x: [x+i for i in range(1)])()