Python中无需使用 [ ] 的列表推导式

111

连接列表:

>>> ''.join([ str(_) for _ in xrange(10) ])
'0123456789'

join 必须接收一个可迭代对象。

显然,join 的参数是 [ str(_) for _ in xrange(10) ],这是一个列表生成式

看这个例子:

>>>''.join( str(_) for _ in xrange(10) )
'0123456789'

现在,join的参数只是str(_) for _ in xrange(10),没有[],但结果相同。

为什么?str(_) for _ in xrange(10)也会产生一个列表或可迭代对象吗?


1
我想象中join最可能是用C语言编写的,因此比列表推导式运行速度快得多...测试时间! - Joel Cornett
显然,我完全误读了你的问题。对我来说,它似乎返回一个生成器... - Joel Cornett
22
请注意:'_'没有特殊的含义,它只是一个普通的变量名。虽然它经常被用作临时变量,但这不是此处的情况(您正在使用该变量)。我建议在代码中避免使用它(至少不要像这样使用)。 - rplnt
7个回答

166
其他回答者已经正确地回答了你发现了一个生成器表达式(它的符号表示类似于列表推导,但没有方括号包围)。
一般来说,genexps(亲切地称为genexps)比列表推导更节省内存且更快。
然而,在 ''.join()的情况下,列表推导既更快又更节省内存。原因是join需要对数据进行两次处理,因此它实际上需要一个真正的列表。如果你给它一个列表,它就可以立即开始工作。如果你给它一个genexp,它就必须通过运行genexp直到耗尽来在内存中建立一个新列表才能开始工作。
~ $ python -m timeit '"".join(str(n) for n in xrange(1000))'
1000 loops, best of 3: 335 usec per loop
~ $ python -m timeit '"".join([str(n) for n in xrange(1000)])'
1000 loops, best of 3: 288 usec per loop

当比较itertools.imapmap时,结果相同:

~ $ python -m timeit -s'from itertools import imap' '"".join(imap(str, xrange(1000)))'
1000 loops, best of 3: 220 usec per loop
~ $ python -m timeit '"".join(map(str, xrange(1000)))'
1000 loops, best of 3: 212 usec per loop

6
你的第二个计时做了太多的工作。不要将生成器表达式包装在列表推导中,直接使用生成器表达式即可。难怪你得到了奇怪的计时结果。 - Raymond Hettinger
13
为什么 ''.join() 需要对迭代器进行两次扫描才能构建字符串? - ovgolovin
34
我猜测第一遍是为了求出字符串的长度之和,以便能够为连接后的字符串分配正确数量的内存,而第二遍则是将各个字符串复制到已分配的空间中。 - Lauritz V. Thaulow
25
那个猜测是正确的。那正是 str.join 做的事情 :-) - Raymond Hettinger
2
Python 3.3 在这些方面似乎要慢得多(比如慢40-50%)。 - Andy Hayden
显示剩余11条评论

77
>>>''.join( str(_) for _ in xrange(10) )

这被称为 生成器表达式,并在PEP 289中有解释。

生成器表达式和列表推导式的主要区别在于前者不会在内存中创建列表。

请注意,还有第三种编写表达式的方式:

''.join(map(str, xrange(10)))

1
据我所知,生成器可以通过类似于元组的表达式来创建,例如( str(_) for _ in xrange(10) )。但是我很困惑,为什么在join中可以省略(),也就是说,代码应该像''.join( (str(_) for _ in xrange(10)) ),对吗? - Alcott
3
@Alcott 我理解元组是由逗号分隔的表达式列表定义的,而不是由括号定义;括号只是为了在赋值时将值进行视觉分组或者在元组要进入其他逗号分隔的列表(如函数调用)中实际分组。这通常可以通过运行像 tup = 1, 2, 3; print(tup) 这样的代码来演示。考虑到这一点,使用 for 作为表达式的一部分会创建生成器,而括号只是为了将其与错误编写的循环区分开来。 - Eric Ed Lohmar

7
你的第二个例子使用了生成器表达式而不是列表推导式。区别在于,使用列表推导式会完全构建一个列表并传递给.join(),而使用生成器表达式,则是逐个生成项目,并由.join()消耗。后者使用的内存更少,通常速度更快。
碰巧的是,列表构造函数可以轻松地消耗任何可迭代对象,包括生成器表达式。所以:
[str(n) for n in xrange(10)]

只是"语法糖"的代名词:

list(str(n) for n in xrange(10))

换句话说,列表推导式只是生成器表达式被转换为列表的过程。

2
你确定它们在底层是等价的吗?Timeit显示:[str(x) for x in xrange(1000)]:262微秒,list(str(x) for x in xrange(1000)):304微秒。 - Lauritz V. Thaulow
2
@lazyr 你是对的。列表推导式更快。这就是为什么在 Python 2.x 中列表推导式会泄漏的原因。这是 GVR 写的:""这是列表推导式最初实现的一个副作用;多年来它一直是 Python 的“肮脏小秘密”之一。它最初是为了使列表推导式变得极快而有意妥协的,虽然它不是初学者常见的陷阱,但它确实偶尔会让人感到痛苦。" http://python-history.blogspot.com/2010/06/from-list-comprehensions-to-generator.html - ovgolovin
4
列表推导式更快的原因是因为join在开始工作之前必须创建一个列表。你提到的“泄漏”不是一个速度问题--它只是意味着循环归纳变量在列表推导式之外被暴露出来。 - Raymond Hettinger
1
@RaymondHettinger 那么这些话“它起初是一种有意的妥协,使列表理解速度惊人地快”是什么意思?据我所知,它们的泄漏与速度问题有关。GVR还写道:“对于生成器表达式,我们无法做到这一点。生成器表达式使用生成器实现,其执行需要一个单独的执行框架。因此,生成器表达式(尤其是如果它们遍历一个短序列)比列表理解效率低。” - ovgolovin
5
你从列表推导式的实现细节错误地推断出str.join表现方式的原因。str.join代码中的第一行是“seq = PySequence_Fast(orig, "")”,这就是调用str.join()时迭代器比列表或元组运行更慢的唯一原因。如果你想进一步讨论,可以开始一个聊天(我是PEP 289的作者,LIST_APPEND操作码的创建者,以及优化list()构造函数的人,所以我对这个问题有一定的熟悉)。 - Raymond Hettinger
显示剩余7条评论

7

如上所述,这是一个生成器表达式

根据文档:

当只有一个参数时,可以在调用时省略括号。详情请参见Calls部分。


4
如果在括号内,但不是方括号,那么就技术上是生成器表达式。生成器表达式最早出现在Python 2.4中。 http://wiki.python.org/moin/Generators 在join后面的部分( str(_) for _ in xrange(10) )本身就是一个生成器表达式。你可以这样做:
mylist = (str(_) for _ in xrange(10))
''.join(mylist)

这意味着与您在上面第二种情况中编写的内容完全相同。

生成器具有一些非常有趣的特性,其中最重要的是当您不需要列表时,它们不会分配整个列表。相反,像join这样的函数逐个“泵出”生成器表达式中的项目,并对小的中间部分进行处理。

在您特定的示例中,列表和生成器可能没有太大的性能差异,但通常情况下,我更喜欢在尽可能使用生成器表达式(甚至生成器函数),主要是因为生成器很少比完整列表实现速度慢。


2

这是一个生成器,而不是列表推导式。生成器也是可迭代的,但它不像先创建整个列表再传递给join函数那样,它会逐个传递xrange中的每个值,这种方式更加高效。


0

你第二个join调用的参数是一个生成器表达式。它确实会产生一个可迭代对象。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接