类属性评估和生成器


问题内容

Python如何精确评估类属性?我偶然发现了一个我想解释的有趣的怪癖(在Python 2.5.2中)。

我有一个带有一些根据其他先前定义的属性定义的属性的类。当我尝试使用生成器对象时,Python会引发错误,但是如果我使用普通的普通列表理解,就没有问题。

这是精简的示例。请注意,唯一的区别是Brie使用生成器表达式,而Cheddar使用列表推导。

# Using a generator expression as the argument to list() fails
>>> class Brie :
...     base = 2
...     powers = list(base**i for i in xrange(5))
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in Brie
  File "<stdin>", line 3, in <genexpr>
NameError: global name 'base' is not defined

# Using a list comprehension works
>>> class Cheddar :
...     base = 2
...     powers = [base**i for i in xrange(5)]
... 
>>> Cheddar.powers
[1, 2, 4, 8, 16]

# Using a list comprehension as the argument to list() works
>>> class Edam :
...     base = 2
...     powers = list([base**i for i in xrange(5)])
...
>>> Edam.powers
[1, 2, 4, 8, 16]

(我的实际情况比较复杂,我正在创建一个字典,但这是我能找到的最小示例。)

我唯一的猜测是列表推导是在该行计算的,但是生成器表达式是在类结束后计算的,此时范围已更改。但是我不确定生成器表达式为什么不充当闭包并将引用存储在范围中的作用域中。

这是有原因的吗?如果是这样,我应该如何考虑类属性的评估机制?


问题答案:

是的,这有点狡猾。一个类并没有真正引入新的作用域,只是看起来有点像它。这样的结构暴露出差异。

这个想法是,当您使用生成器表达式时,它等同于使用lambda:

class Brie(object):
    base= 2
    powers= map(lambda i: base**i, xrange(5))

或明确地作为函数语句:

class Brie(object):
    base= 2

    def __generatePowers():
        for i in xrange(5):
            yield base**i

    powers= list(__generatePowers())

在这种情况下,很显然,这base并不适用__generatePowers;两者都会导致异常(除非您很不幸也有一个base全局变量,在这种情况下您会得到错误)。

对于列表推导这不会发生,这是由于一些内部细节来评估它们的方式,但是在Python
3中这种行为消失了,在两种情况下都会同样失败。这里有一些讨论。

可以使用lambda的一种解决方法,该技术与我们在nested_scopes出现之前的糟糕年代所依赖的技术相同:

class Brie(object):
    base= 2
    powers= map(lambda i, base= base: base**i, xrange(5))