类范围和列表、集合或字典理解以及生成器表达式不混合。
原因;或者,官方的说法
在Python3中,列表理解被赋予了一个适当的作用域(本地名称空间),以防止它们的本地变量溢出到周围的作用域中(请参见
Python list comprehension rebind names even after scope of comprehension. Is this right?
)。在模块或函数中使用这样的列表理解非常好,但是在类中,范围界定有点,嗯,
奇怪的
.
这记录在
pep 227
:
类作用域中的名称不可访问。名称在中解析
最里面的封闭函数作用域。如果类定义
在嵌套作用域链中发生,解析过程将跳过
类定义。
而在
class
compound statement documentation
:
然后在新的执行框架中执行classs套件(参见
Naming and binding
,使用新创建的本地命名空间和原始全局命名空间。(通常,该套件只包含函数定义。)当类的套件完成执行时,
其执行帧被丢弃,但其本地命名空间被保存
.
[4]
然后使用基类的继承列表和属性字典保存的本地命名空间创建类对象。
重点挖掘;执行框架是临时范围。
因为作用域被重新用作类对象的属性,允许它用作非本地作用域也会导致未定义的行为;如果类方法引用
x
作为嵌套范围变量,然后操纵
Foo.x
还有,比如说?更重要的是,这对
Foo
?蟒蛇
有
以不同的方式对待类作用域,因为它与函数作用域非常不同。
最后,但绝对不是最不重要的是
Naming and binding
执行模型文档中的部分明确提到了类作用域:
类块中定义的名称范围仅限于类块;它不扩展到方法的代码块——这包括理解和生成器表达式,因为它们是使用函数范围实现的。这意味着以下操作将失败:
class A:
a = 42
b = list(a + i for i in range(10))
因此,总结一下:不能从包含在该作用域中的函数、列表理解或生成器表达式访问类作用域;它们的作用就像该作用域不存在一样。在Python2中,列表理解是使用快捷方式实现的,但在Python3中,它们有自己的函数作用域(它们应该一直都有),因此示例中断。不管python版本如何,其他理解类型都有自己的作用域,因此在python 2中会出现一个带有set或dict理解的类似示例。
# Same error, in Python 2 or 3
y = {x: x for i in range(1)}
(小)例外;或者,为什么一部分
可以
仍然工作
理解或生成器表达式的一部分在周围的范围内执行,而不管python版本如何。这将是最外层iterable的表达式。在你的例子中,是
range(1)
:
y = [x for i in range(1)]
# ^^^^^^^^
因此,使用
X
在该表达式中不会引发错误:
# Runs fine
y = [i for i in range(x)]
这只适用于最外层的iterable;如果一个理解有多个
for
子句,内部的iterables
对于
从句在理解范围内进行评估:
# NameError
y = [i for i in range(1) for j in range(x)]
这个设计决策是为了在genexp创建时抛出错误,而不是在创建生成器表达式的最外层iterable时抛出错误,或者在最外层iterable结果不是iterable时抛出错误。理解共享这种行为以保持一致性。
从引擎盖下面看;或者,比你想要的要详细得多
您可以使用
dis
module
. 我在下面的示例中使用Python3.3,因为它添加了
qualified names
能够清楚地识别我们要检查的代码对象。生成的字节码在功能上与Python3.2相同。
到
创造
作为一个类,python基本上采用了构成类体的整个套件(因此所有的东西都比
class <name>:
,并将其作为函数执行:
>>> 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
这里为
福
类body,然后将其转换为函数并调用它。这个
结果
然后使用该调用创建类的命名空间
__dict__
. 到现在为止,一直都还不错。
这里要注意的是,字节码包含一个嵌套的代码对象;在python中,类定义、函数、理解和生成器都表示为代码对象,这些对象不仅包含字节码,还包含表示局部变量、常量、从g中获取的变量的结构。从嵌套作用域获取的lobals和变量。编译的字节码引用那些结构,而python解释器知道如何访问给定字节码的那些结构。
这里要记住的重要一点是,python在编译时创建这些结构;
班
suite是一个代码对象(
<code object Foo at 0x10a436030, file "<stdin>", line 2>
)已经编译好了。
让我们检查创建类主体本身的代码对象;代码对象有
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
上面的字节码创建类体。函数被执行,结果是
locals()
命名空间,包含
X
和
y
用于创建类(但它不起作用,因为
X
不定义为全局)。注意,储存后
5
在里面
X
,它加载另一个代码对象;这是列表理解;它像类主体一样被包装在函数对象中;创建的函数接受一个位置参数,
范围(1)
可用于循环代码,转换为迭代器。如字节码所示,
范围(1)
在类作用域中求值。
从中可以看出,函数或生成器的代码对象与理解的代码对象之间的唯一区别是后者是执行的
立即
当父代码对象被执行时;字节码只是动态地创建一个函数,然后用几个小步骤执行它。
python 2.x在那里使用内联字节码,这里是python2.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
循环以内联方式运行。所以在Python3.x中,列表生成器被赋予了一个自己的适当代码对象,这意味着它有自己的作用域。
然而,当解释器第一次加载模块或脚本时,理解是与python源代码的其余部分一起编译的,而编译器确实
不
将类套件视为有效范围。列表理解中的任何引用变量都必须在作用域中查找
周围的
类定义,递归地。如果编译器找不到该变量,则将其标记为全局变量。对list comprehension code对象的反汇编表明
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
这个字节码块加载传入的第一个参数
范围(1)
迭代器),就像python 2.x版本使用
福里斯特
循环它并创建它的输出。
我们确定了吗
X
在
foo
改为函数,
X
将是单元格变量(单元格引用嵌套作用域):
>>> 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__
属性。由于为comprehension code对象创建的函数再次被丢弃,我们无法检查该函数的闭包。要查看闭包的运行情况,我们必须检查嵌套函数:
>>> 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
所以,总结一下:
-
列表理解在python 3中获得自己的代码对象,函数、生成器或理解的代码对象之间没有区别;理解代码对象包装在临时函数对象中并立即调用。
-
代码对象是在编译时创建的,并且基于代码的嵌套作用域,任何非局部变量都标记为全局变量或自由变量。班集体是
不
被认为是查找这些变量的范围。
-
在执行代码时,python只需查看globals或当前执行对象的闭包。由于编译器没有将类体作为作用域包含,因此不考虑临时函数命名空间。
解决办法;或者,该怎么办
如果要为
X
变量,就像在函数中一样,你
可以
使用类范围变量进行列表理解:
>>> class Foo:
... x = 5
... def y(x):
... return [x for i in range(1)]
... y = y(x)
...
>>> Foo.y
[5]
“临时的”
Y
函数可以直接调用;当我们使用它的返回值时替换它。其范围
是
解决时考虑
X
:
>>> 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__
要改为创建实例变量,请执行以下操作:
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'),
# ...
]]