17

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]

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

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

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

4

2 回答 2

15

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

这个想法是,当您使用生成器表达式时,它等效于使用 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))
于 2009-11-20T22:46:45.153 回答
1

来自PEP 289

在探索了许多可能性之后,出现了一个共识,即绑定问题很难理解,应强烈鼓励用户在立即使用其参数的函数中使用生成器表达式。对于更复杂的应用程序,完整的生成器定义在范围、生命周期和绑定方面的明显性总是优于 [6]。

[6] (1, 2) Source Forge 上的补丁讨论和替代补丁http://www.python.org/sf/872326

据我所知,这就是生成器表达式的范围。

于 2009-11-20T22:49:50.637 回答