为什么 itertools.chain 比展平列表理解更快?
Why is itertools.chain faster than a flattening list comprehension?
在 this question 的评论中的讨论上下文中提到,虽然连接字符串序列只需要 ''.join([str1, str2, ...])
,但连接列表序列就像 [=13] =],尽管你也可以使用像 [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
在 itertools.chain
个对象上)。某种自定义 itertools.chain
到 list
的转换是以某种方式发生的还是 itertools.chain
利用了其他机制?
在 Python 3.6.3 的 Windows 10 x64 上测试过,如果相关的话。
编辑:
似乎最快的方法毕竟是调用 .extend
每个列表的空列表,正如 @zwer 所建议的,可能是因为它适用于 "chunks" 数据而不是基于每个元素。
这里是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
因此解释器在 运行 生成器表达式被列表解包时需要执行类似数量的步骤,但是正如您所期望的那样, 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
在 this question 的评论中的讨论上下文中提到,虽然连接字符串序列只需要 ''.join([str1, str2, ...])
,但连接列表序列就像 [=13] =],尽管你也可以使用像 [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
在 itertools.chain
个对象上)。某种自定义 itertools.chain
到 list
的转换是以某种方式发生的还是 itertools.chain
利用了其他机制?
在 Python 3.6.3 的 Windows 10 x64 上测试过,如果相关的话。
编辑:
似乎最快的方法毕竟是调用 .extend
每个列表的空列表,正如 @zwer 所建议的,可能是因为它适用于 "chunks" 数据而不是基于每个元素。
这里是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
因此解释器在 运行 生成器表达式被列表解包时需要执行类似数量的步骤,但是正如您所期望的那样, 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