如何从类定义中的列表推导中访问其他类变量?以下适用于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中删除了。
类范围和列表,集合或字典理解以及生成器表达式不会混合使用。
在Python 3中,列表推导给出了它们自己的适当范围(本地命名空间),以防止它们的局部变量渗透到周围的范围内(参见Python list comprehension rebind names even after scope of comprehension. Is this right?)。当在模块或函数中使用这样的列表推导时,这很好,但在类中,范围确定有点,嗯,奇怪。
这在pep 227中有记录:
无法访问类范围中的名称。名称在最里面的封闭函数范围内解析。如果类定义出现在嵌套作用域链中,则解析过程将跳过类定义。
在class
compound statement documentation:
然后使用新创建的本地命名空间和原始全局命名空间在新的执行框架中执行类的套件(请参阅Naming and binding一节)。 (通常,套件仅包含函数定义。)当类的套件完成执行时,其执行帧将被丢弃,但其本地名称空间将被保存。 [4]然后使用基类的继承列表和属性字典的已保存本地名称空间创建类对象。
强调我的;执行帧是临时范围。
因为范围被重新用作类对象的属性,所以允许它用作非局部范围也会导致未定义的行为;如果一个类方法将x
称为嵌套范围变量,然后操纵Foo.x
,会发生什么?更重要的是,这对于Foo
的子类意味着什么? Python必须以不同的方式处理类范围,因为它与函数范围非常不同。
最后,但绝对不是最不重要的,执行模型文档中的链接Naming and binding部分明确提到了类范围:
类块中定义的名称范围仅限于类块;它没有扩展到方法的代码块 - 这包括了解和生成器表达式,因为它们是使用函数作用域实现的。这意味着以下内容将失败:
class A: a = 42 b = list(a + i for i in range(10))
因此,总结一下:您无法从该范围内的函数,列表推导或生成器表达式访问类范围;他们的行为就像那个范围不存在一样。在Python 2中,列表推导是使用快捷方式实现的,但在Python 3中,它们有自己的功能范围(因为它们应该一直都有),因此您的示例会中断。无论Python版本如何,其他理解类型都有自己的范围,因此在Python 2中会出现类似于set或dict理解的示例。
# Same error, in Python 2 or 3
y = {x: x for i in range(1)}
无论Python版本如何,都在周围范围内执行理解或生成器表达式的一部分。这将是最外层迭代的表达式。在你的例子中,它是range(1)
:
y = [x for i in range(1)]
# ^^^^^^^^
因此,在该表达式中使用x
不会抛出错误:
# Runs fine
y = [i for i in range(x)]
这仅适用于最外面的可迭代;如果一个理解有多个for
子句,内部for
子句的迭代在评估范围内进行评估:
# NameError
y = [i for i in range(1) for j in range(x)]
这个设计决策是为了在genexp创建时而不是迭代时间抛出一个错误,当创建生成器表达式的最外层迭代时抛出错误,或者当最外面的iterable结果不可迭代时。理解分享此行为是为了保持一致性。
你可以使用dis
module看到这一切。我在下面的例子中使用Python 3.3,因为它添加了qualified names,它巧妙地识别了我们想要检查的代码对象。生成的字节码在功能上与Python 3.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
为Foo
类主体加载一个代码对象,然后将其转换为函数,并调用它。然后,该调用的结果用于创建类的名称空间,即__dict__
。到现在为止还挺好。
这里要注意的是字节码包含嵌套的代码对象;在Python中,类定义,函数,理解和生成器都表示为代码对象,它们不仅包含字节码,还包含表示局部变量,常量,从全局变量中获取的变量以及从嵌套范围中获取的变量的结构。编译后的字节码指的是那些结构,python解释器知道如何访问给定字节码的那些。
这里要记住的重要一点是Python在编译时创建这些结构; class
套件是一个已编译的代码对象(<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
后,它会加载另一个代码对象;这就是列表理解;它被包装在一个函数对象中,就像类体一样;创建的函数采用位置参数,range(1)
可迭代用于其循环代码,强制转换为迭代器。如字节码所示,range(1)
在类范围内进行评估。
由此可以看出,函数或生成器的代码对象与理解的代码对象之间的唯一区别是后者在执行父代码对象时立即执行;字节码只是简单地创建一个函数并在几个小步骤中执行它。
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
循环。所以在Python 3.x中,列表生成器被赋予了自己的适当代码对象,这意味着它有自己的范围。
但是,当解释器首次加载模块或脚本时,理解与其他python源代码一起编译,并且编译器不认为类套件是有效范围。列表推导中的任何引用变量都必须以递归方式查看类定义的范围。如果编译器找不到该变量,则将其标记为全局变量。列表推导代码对象的反汇编表明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
循环它并创建其输出一样。
如果我们在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__
属性初始化的。由于为理解代码对象创建的函数再次被丢弃,我们无法检查该函数的闭包。要查看操作中的闭包,我们必须检查嵌套函数:
>>> 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
所以,总结一下:
如果要为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'),
# ...
]]
在我看来,这是Python 3中的一个缺陷。我希望他们改变它。
Old Way(在2.7中工作,在3+中抛出NameError: name 'x' is not defined
):
class A:
x = 4
y = [x+i for i in range(1)]
注意:简单地用A.x
确定它并不能解决它
新方式(适用于3+):
class A:
x = 4
y = (lambda x=x: [x+i for i in range(1)])()
因为语法太丑了我通常只是在构造函数中初始化我的所有类变量
接受的答案提供了很好的信息,但这里似乎还有一些其他的皱纹 - 列表理解和生成器表达之间的差异。我玩过的演示:
class Foo:
# A class-level variable.
X = 10
# I can use that variable to define another class-level variable.
Y = sum((X, X))
# Works in Python 2, but not 3.
# In Python 3, list comprehensions were given their own scope.
try:
Z1 = sum([X for _ in range(3)])
except NameError:
Z1 = None
# Fails in both.
# Apparently, generator expressions (that's what the entire argument
# to sum() is) did have their own scope even in Python 2.
try:
Z2 = sum(X for _ in range(3))
except NameError:
Z2 = None
# Workaround: put the computation in lambda or def.
compute_z3 = lambda val: sum(val for _ in range(3))
# Then use that function.
Z3 = compute_z3(X)
# Also worth noting: here I can refer to XS in the for-part of the
# generator expression (Z4 works), but I cannot refer to XS in the
# inner-part of the generator expression (Z5 fails).
XS = [15, 15, 15, 15]
Z4 = sum(val for val in XS)
try:
Z5 = sum(XS[i] for i in range(len(XS)))
except NameError:
Z5 = None
print(Foo.Z1, Foo.Z2, Foo.Z3, Foo.Z4, Foo.Z5)
这是Python中的一个错误。广告被理解为等同于for循环,但在类中不是这样。至少在Python 3.6.6中,在类中使用的理解中,只有一个来自理解之外的变量可以在理解中访问,并且它必须用作最外层的迭代器。在函数中,此范围限制不适用。
为了说明为什么这是一个错误,让我们回到原始的例子。这失败了:
class Foo:
x = 5
y = [x for i in range(1)]
但这有效:
def Foo():
x = 5
y = [x for i in range(1)]
限制在参考指南中的this section末尾说明。
由于最外层迭代器是在周围范围内计算的,我们可以使用zip
和itertools.repeat
将依赖关系带到理解的范围:
import itertools as it
class Foo:
x = 5
y = [j for i, j in zip(range(3), it.repeat(x))]
也可以在理解中使用嵌套的for
循环,并在最外层的iterable中包含依赖项:
class Foo:
x = 5
y = [j for j in (x,) for i in range(3)]
对于OP的具体示例:
from collections import namedtuple
import itertools as it
class StateDatabase:
State = namedtuple('State', ['name', 'capital'])
db = [State(*args) for State, args in zip(it.repeat(State), [
['Alabama', 'Montgomery'],
['Alaska', 'Juneau'],
# ...
])]