为什么 itertools.chain 比扁平列表理解更快?

2024-04-28

在评论中的讨论中这个问题 https://stackoverflow.com/questions/49630581/why-does-python-forbid-the-use-of-sum-with-strings有人提到,虽然连接字符串序列只需要''.join([str1, str2, ...]),连接一系列列表就像list(itertools.chain(lst1, lst2, ...)),尽管您也可以使用列表理解,例如[x for y in [lst1, lst2, ...] for x in y]。令我惊讶的是,第一种方法始终比第二种方法快:

import random
import itertools

random.seed(100)
lsts = [[1] * random.randint(100, 1000) for i in range(1000)]

%timeit [x for y in lsts for x in y]
# 39.3 ms ± 436 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit list(itertools.chain.from_iterable(lsts))
# 30.6 ms ± 866 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit list(x for y in lsts for x in y)  # Proposed in comments
# 62.5 ms ± 504 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
# Loop-based methods proposed in the comments
%%timeit
a = []
for lst in lsts: a += lst
# 26.4 ms ± 634 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%%timeit
a = []
for lst in lsts: a.extend(lst)
# 26.7 ms ± 728 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

虽然不是一个数量级的差异,但也不容忽视。我想知道情况如何,因为列表理解通常是解决给定问题的最快方法之一。起初我以为也许itertools.chain对象会有一个len认为list构造函数可以用来预分配必要的内存,但事实并非如此(无法调用len on itertools.chain对象)。是一些定制的itertools.chain-to-list转换以某种方式发生或正在发生itertools.chain利用其他机制?

如果相关的话,已在 Windows 10 x64 上的 Python 3.6.3 中进行测试。

EDIT:

毕竟调用似乎是最快的方法.extend每个列表都有一个空列表,如建议的@zwer https://stackoverflow.com/users/7553525/zwer,可能是因为它适用于数据“块”,而不是基于每个元素。


Here is itertools.chain.from_iterable https://github.com/python/cpython/blob/aa0735f597b072c0eb00404c4d7df359ddc26755/Modules/itertoolsmodule.c#L1854。即使您不懂 C,它也不难阅读,并且您可以知道一切都发生在 C 级别(在用于在代码中生成列表之前)。

列表推导式的字节码如下所示:

def f(lsts):
    return [x for y in lsts for x in y]

dis.dis(f.__code__.co_consts[1])
  2           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                18 (to 24)
              6 STORE_FAST               1 (y)
              8 LOAD_FAST                1 (y)
             10 GET_ITER
        >>   12 FOR_ITER                 8 (to 22)
             14 STORE_FAST               2 (x)
             16 LOAD_FAST                2 (x)
             18 LIST_APPEND              3
             20 JUMP_ABSOLUTE           12
        >>   22 JUMP_ABSOLUTE            4
        >>   24 RETURN_VALUE

这些是创建列表理解所涉及的所有 Python 解释器操作。只需将所有操作都放在 C 级别(在chain)而不是让解释器逐步执行每个字节代码步骤(在理解中),这将为您带来性能提升。

不过,这种提升很小,我不会担心。这是Python,可读性高于速度。


Edit:

对于列表包装的生成器理解

def g(lists):
    return list(x for y in lsts for x in y)

# the comprehension
dis.dis(g.__code__.co_consts[1])
  2           0 LOAD_FAST                0 (.0)
        >>    2 FOR_ITER                20 (to 24)
              4 STORE_FAST               1 (y)
              6 LOAD_FAST                1 (y)
              8 GET_ITER
        >>   10 FOR_ITER                10 (to 22)
             12 STORE_FAST               2 (x)
             14 LOAD_FAST                2 (x)
             16 YIELD_VALUE
             18 POP_TOP
             20 JUMP_ABSOLUTE           10
        >>   22 JUMP_ABSOLUTE            2
        >>   24 LOAD_CONST               0 (None)
             26 RETURN_VALUE

因此,解释器在运行按列表解包的生成器表达式时需要执行相似数量的步骤,但正如您所期望的那样,Python 级别的开销list打开生成器的包装(与 C 相对)LIST_APPEND指令)是减慢速度的原因。

dis.dis(f)
  2           0 LOAD_CONST               1 (<code object <listcomp> at 0x000000000FB58B70, file "<ipython-input-33-1d46ced34d66>", line 2>)
              2 LOAD_CONST               2 ('f.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_FAST                0 (lsts)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE

dis.dis(g)
  2           0 LOAD_GLOBAL              0 (list)
              2 LOAD_CONST               1 (<code object <genexpr> at 0x000000000FF6F420, file "<ipython-input-40-0334a7cdeb8f>", line 2>)
              4 LOAD_CONST               2 ('g.<locals>.<genexpr>')
              6 MAKE_FUNCTION            0
              8 LOAD_GLOBAL              1 (lsts)
             10 GET_ITER
             12 CALL_FUNCTION            1
             14 CALL_FUNCTION            1
             16 RETURN_VALUE
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么 itertools.chain 比扁平列表理解更快? 的相关文章

随机推荐