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

时间:2018-04-03 13:27:16

标签: python list-comprehension itertools flatten

this question的评论讨论的背景下,有人提到虽然连接一串字符串只需''.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构造函数可以用来预分配必要的内存,但事实并非如此(不能调用lenitertools.chain个对象上)。是某些自定义itertools.chain - 到 - list转换是以某种方式发生还是itertools.chain利用其他一些机制?

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

编辑:

这似乎是最快的方法,就像@zwer提议的那样,每个列表调用.extend一个空列表,这可能是因为它适用于" chunks"数据而不是每个元素。

1 个答案:

答案 0 :(得分:4)

这是itertools.chain.from_iterable。即使您不了解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,可读性超速。

编辑:

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

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

因此,解释器在运行由列表解压缩的生成器表达式时有相似的步骤数,但正如您所料,让list解包生成器的python级开销(而不是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