类属性评估与生成器

17 投票
2 回答
1329 浏览
提问于 2025-04-15 16:13

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]

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

我猜测,列表推导式是在那一行就计算出来的,而生成器表达式是在类结束后才计算,这时候作用域已经改变了。但我不明白为什么生成器表达式不作为闭包来存储对基础属性的引用。

这有什么原因吗?如果有,我应该怎么理解类属性的评估机制呢?

2 个回答

1

来自 PEP 289 的内容:

经过多次探讨,大家达成了一个共识:绑定问题很难理解,因此应该强烈建议用户在那些立即使用参数的函数中使用生成器表达式。对于更复杂的应用,完整的生成器定义在作用域、生命周期和绑定方面总是更清晰。

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

这段话主要是讲生成器表达式的作用域问题,尽我所能理解就是这个意思。

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表达式来解决这个问题,方法和以前在没有嵌套作用域的日子里一样:

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

撰写回答