连接字符串。生成器还是列表推导式?

8
考虑从一个巨大的字符串中提取字母的问题。
一种解决方法是:
''.join([c for c in hugestring if c.isalpha()])

机制很清晰:列表推导式生成一个字符列表。join方法通过访问列表长度来知道需要连接多少个字符。另一种方法是:
''.join(c for c in hugestring if c.isalpha())

这里的生成器推导式会得到一个生成器。join方法不知道要连接多少个字符,因为生成器没有len属性。所以这种连接方法应该比列表推导式慢。
但是在Python中进行测试表明它并不慢。为什么呢? 有人能解释一下join如何在生成器上工作吗?
需要明确的是:
sum(j for j in range(100))

由于可以通过跟踪累加和来实现,因此不需要了解100的任何知识。它可以使用生成器上的next方法访问下一个元素,然后添加到累加总和中。 但是,由于字符串是不可变的,每次迭代都会创建一个新的字符串,因此逐步连接字符串需要很长时间。

3个回答

14

当您调用str.join(gen)时,其中gen是一个生成器,Python会在继续检查结果序列的长度之前执行list(gen)的等效操作。

具体而言,如果您查看CPython中实现str.join的代码,您将看到这个调用:

    fseq = PySequence_Fast(seq, "can only join an iterable");

调用PySequence_Fast函数将seq参数转换为列表,如果它原来不是列表或元组。

因此,你的两个调用版本几乎完全相同。在列表推导中,您自己构建列表并将其传递给join。在生成器表达式版本中,您传入的生成器对象会在join的开始处转换为list,代码的其余部分对两个版本都是相同的。


那么,OP注意到的速度差异应该纯粹是偶然的,对吧? - Ma0
@Ev.Kounis:提问者说两个版本的速度相似(“不是更慢”), 如果他们同时测量了join和列表推导式所花费的时间,这是有道理的。如果你只测量join的时间,那么生成器表达式版本会更慢,因为它必须先将整个生成器转储到一个列表中,然后才能进行任何字符串连接。这将花费与在另一个版本中构建列表推导式所需的时间大致相同的时间。 - Blckknght
有人可能会被欺骗认为,对于大型字符串来说使用发生器应该更加节省内存... :( - pabouk - Ukraine stay strong

1

至少在我的电脑上,对于我测试的情况下,列表推导式更快,可能是由于''.join能够优化内存分配。这很可能取决于你正在测试的具体例子(例如,如果你正在测试的条件出现得不那么频繁,CPython因不知道长度而支付的代价可能会更小):

In [18]: s = ''.join(np.random.choice(list(string.printable), 1000000))

In [19]: %timeit ''.join(c for c in s if c.isalpha())
10 loops, best of 3: 69.1 ms per loop

In [20]: %timeit ''.join([c for c in s if c.isalpha()])
10 loops, best of 3: 61.8 ms per loop

1
这是列表推导式被超级优化的结果(它们直接构建list,而生成器表达式只是yield必须使用通用迭代器协议消耗的值的结果),与''.join的工作方式无关。运行相同的测试,但将''.join替换为list(在第二种情况下,可以完全省略它,因为它是多余的)。生成器表达式周围的list构造函数要慢得多,并且对于这么大的输入,它显然与与list相关的查找或函数调用成本无关。 - ShadowRanger

1

join()不需要将序列的元素顺序逐个附加到长字符串中以形成结果(对于长序列而言,这确实非常慢);它只需要产生相同的结果。因此,join()可能只是将字符附加到某些内部存储器缓冲区,并在结束时从中创建一个字符串。另一方面,列表推导构造需要先构造列表(通过遍历hugestring的生成器),然后才让join()开始工作。

另外,我怀疑join()并没有查看列表的长度,因为它无法知道每个元素是否是单个字符(在大多数情况下,它不是),它可能只是从列表获取一个生成器。


3
参考解释器 C 层代码提供了完整(但私有的)_PyUnicodeWriter API,用于此目的(以及其他类似的“逐步构建字符串”情况)。与 Java 的 StringBuilder 类进行比较。 - ShadowRanger
2
话虽如此,看起来@Blckknight是正确的;如果输入不是listtuple,它会在内部将其转换为list。然后,它似乎进行了一次预计算,以计算最终值的长度,以便精确地预分配所需的空间,而不使用_PyUnicodeWriter - ShadowRanger

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