python算术表达式生成列表_Python之父:从列表推导式到生成器表达式

本系列文章译自Python之父 Guido van Rossum 的系列博客“The History of Python”。这个博客系列对我们理解Python及其演变很有帮助,在这里翻译推荐给大家,希望大家喜欢,也请大家多多指教!

1. 列表推导式的起源

列表推导式是在Python2.0中新增的特性,最初来自于Greg Ewing提交的patchs,Skip Montanaro和Thomas Wouters也有贡献(如果我记得没错的话,Tim Peter也非常喜欢这个创意)。本质上说,列表推导式是数学中一种“集合”的Python版本。比如说:

{ x | x > 10 }

代表大于10的所有x的集。在数学中,这个表达式代表一个全集(不同语境下,这个集可以有不同的意思,比如所有大于10的实数、所有大于10的整数等)。Python中并没有一个全集的概念,而在Python2.0中,甚至还没有集合(元组)的概念(Python中的元组是另一个有趣的故事,会在之后的博文中为大家介绍)。

另外也出于一些其它因素的考虑,就有了Python中的以下表达式:

[ f(x) for x in S if P(x) ]

这个表达式会生成一个列表,由序列 S 中符合 P 条件,并经 f 函数处理得到的值组成。其中条件 P 是可选的, for 循环可以嵌套,每一个 for 循环都有一个可选的 P 条件(实际上很少用到,因为列表推导式一般用于将一个多维对象转换成一维列表)。

2. 列表推导式的优势

列表推导式为Python内置函数 map() 和 filter() 提供了替代方案。

map(f, S) 等价于

[ f(x) for x in S ]

而 filter(P, S) 等价于

[ x for x in S if P(x) ]

有些人可能会觉得,map() 和 filter() 函数似乎更简洁,完全没有理由使用列表推导式啊!

然而,在实际工作中并非如此。比如说,如果要为列表中的每个元素加 1 并返回一个新的列表,采用列表推导式写法是

[ x + 1 for x in S ]

而使用 map() 函数的写法是

map(lambda x: x+1, S)

其中的 `lambda x: x+1` 是Python中的匿名函数写法。

有些人可能会说,就这个例子而言, map() 函数写法之所以比列表推导式复杂,主要是因为Python中的匿名函数太繁琐,如果有一个更简洁的匿名函数写法,大家就会使用 map() 函数了。

我个人并不同意这种看法——我认为列表推导式比函数写法的可读性要好得多,尤其是在处理函数变得更加复杂的情况下。另外,列表推导式的速度也要比函数表达式快得多,因为调用 lambda 函数需要创建新的堆栈,而列表推导式不需要。

3. 生成器表达式

由于列表推导式的成功,以及生成器的引入(关于生成器,会在之后的文章中作更多介绍),Python2.4 中新增了一种类似推导式的写法,用于表示一个处理结果序列,而不用预先生成整个列表,这个新增的特性叫“生成器表达式”。比如说:

sum(x**2 for x in range(1, 11))

这行代码调用内置的 sum() 函数,使用的参数是一个生成器表达式,生成从 1 到 10 的平方。因此,这个 sum() 函数的意思就是 1 到 10 的平方之和,结果是 385。

在类似例子中,使用生成器表达式的好处很明显,如果生成列表再求和,这个列表就会在函数执行完成前一直占用内存空间,如果列表比较长,所占用的内存是非常可观的。

4. 列表推导式与生成器表达式的区别

不得不说,列表推导式与生成器表达式之间的区别非常微妙,比如说,在Python2中,下面这个推导式是成立的:

[ x**2 for x in 1, 2, 3 ]

但在生成器表达式中不成立:

( x**2 for x in 1, 2, 3 )

在生成器表达式中必须这样写:

( x**2 for x in (1, 2, 3))

当然,在Python3中,列表推导式也必须要加括号了:

[ x**2 for x in (1, 2, 3) ]

然而,在“一般”或者说“明确”的 for 循环中,你依然可以省略括号:

for x in 1, 2, 3:

print(x**2)

为什么推导式和生成器表达式之间要有区别呢?为什么在 Python3 中列表推导式也使用更严格的写法呢?这两个问题涉及到的因素包括向后兼容、表达歧义、统一写法以及语言演进等。

在最初版本的 Python 中(未对外发布前 :-),只有语义明确的 for 循环写法,在关键字in 之后使用逗号是没有歧义的,我想为了省事,当然不写括号的好。这也让我想起,在Algol-60语言中,你可以直接写:

for i := 1, 2, 3 do Statement

并且在这个语言中,你还可以用 step-until 语句替换循环对象,比如说:

for i := 1 step 1 until 10, 12 step 2 until 50, 55 step 5 until 100 do Statement

(事后回想,如果Python也能同时迭代多个序列该多好啊,然而……)

当我们在 Python2.0 中增加列表推导式的时候,列表推导式之后只可能有 ] ,或者关键字 for 或 if,不会有歧义,因此,不使用括号是没问题的。

但是当我们在 Python2.4 中增加生成器表达式的时候,就碰到了表达歧义的问题:生成器表达式前后的括号有可能不是生成器表达式的一部分。比如说:

sum(x**2 for x in range(10))

这行代码外层的括号是 sum() 函数的一部分,而生成器表达式只是这个函数的第一个参数。因此,有些代码可能会有两种以上的语义,比如说:

sum(x**2 for x in a, b)

有可能可以理解为:

sum(x**2 for x in (a, b))

也可能理解为:

sum((x**2 for x in a), b)

在纠结了很久之后(如果我记得没错的话),我们决定,生成器表达式的 in 关键字之后必须带括号。不过当时我们并不想改动(超受欢迎的)列表推导式的写法。

在之后设计 Python3 的过程中,我们决定,列表推导式的写法:

[ f(x) for x in S if P(x) ]

应该与使用内置 list() 函数通过生成器表达式生成列表的写法完全一致:

list(f(x) for x in S if P(x))

因此,列表推导式也被要求使用与生成器表达式一样的更严格的写法。

5. 变量泄露问题

另外,在Python3中,为了加强列表推导式和生成器表达式写法之间的统一性,我们还做了一些改动。在Python2中,列表推导式会把内层变量“泄露”到外层中,比如说:

x = 'before'

a = [x for x in 1, 2, 3]

print x # 这里会打印 3 而不是 'before'

这是最初版本的列表推导式所带来的影响,一直以来,都属于Python的“暗黑小秘密”之一。一开始,这个设计是为了加快列表推导式的运算速度所做的有意妥协。一般来说,新手不会经常踩到这个坑,但不论如何,这个问题还是时不时让人抓狂。

生成器表达式不会有这个问题,它继承自生成器,在单独的栈帧中执行指令。也因为这个原因,生成器表达式(特别用在一个短序列的时候)会比列表推导式效率更低。

在Python3中,我们决定修复这个“暗黑小秘密”,让列表推导式和生成器表达式采用同样的继承策略。因此,在Python3中,因为列表推导式中的 x 不覆盖外层 x,上面的代码会打印 before 而不是 3 。

也不用担心Python3中列表推导式的运行效率:由于在Python3的整体运行速度方面投入了巨大的努力,不论列表推导式还是生成器表达式,在Python3中都比Python2中运行效率要高!(而且它们在运行效率上也不再有什么分别)

更新:当然,我忘了提到,Python3还支持元组推导式和字典推导式,它们都是列表推导式这个概念的直接扩展。

欢迎关注个人公众号:读书录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值