关于python:为什么列表理解比附加到列表要快得多?

Why is a list comprehension so much faster than appending to a list?

本问题已经有最佳答案,请猛点这里访问。

我在想,为什么列表理解要比添加到列表中快得多。我认为差异只是表达性的,但不是。

1
2
3
4
5
6
7
8
9
>>> import timeit
>>> timeit.timeit(stmt='''\
t = []
for i in range(10000):
    t.append(i)'''
, number=10000)
9.467898777974142

>>> timeit.timeit(stmt='t= [i for i in range(10000)]', number=10000)
4.1138417314859

列表理解速度快50%。为什么?


列表理解基本上只是常规for循环的"句法糖"。在这种情况下,它性能更好的原因是它不需要加载列表的append属性并在每次迭代时将其作为函数调用。换句话说,一般来说,列表理解执行得更快,因为挂起和恢复一个函数的框架,或者在其他情况下多个函数,比按需创建一个列表慢。

请考虑以下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# Python-3.6

In [1]: import dis

In [2]: def f1():
   ...:     l = []
   ...:     for i in range(5):
   ...:         l.append(i)
   ...:        

In [3]: def f2():
   ...:     [i for i in range(5)]
   ...:    

In [4]: dis.dis(f1)
  2           0 BUILD_LIST               0
              3 STORE_FAST               0 (l)

  3           6 SETUP_LOOP              33 (to 42)
              9 LOAD_GLOBAL              0 (range)
             12 LOAD_CONST               1 (5)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
        >>   19 FOR_ITER                19 (to 41)
             22 STORE_FAST               1 (i)

  4          25 LOAD_FAST                0 (l)
             28 LOAD_ATTR                1 (append)
             31 LOAD_FAST                1 (i)
             34 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             37 POP_TOP
             38 JUMP_ABSOLUTE           19
        >>   41 POP_BLOCK
        >>   42 LOAD_CONST               0 (None)
             45 RETURN_VALUE

In [5]: dis.dis(f2)
  2           0 LOAD_CONST               1 (<code object <listcomp> at 0x7fe48b2265d0, file"<ipython-input-3-9bc091d521d5>", line 2>)
              3 LOAD_CONST               2 ('f2.<locals>.<listcomp>')
              6 MAKE_FUNCTION            0
              9 LOAD_GLOBAL              0 (range)
             12 LOAD_CONST               3 (5)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               0 (None)
             26 RETURN_VALUE

您可以看到,在偏移量22处,我们在第一个函数中有一个append属性,因为在第二个函数中没有使用列表理解的属性。所有这些额外的字节码将使附加方法变慢。另外请注意,在每次迭代中也会加载append属性,这使得您的代码比使用列表理解的第二个函数慢大约2倍。


即使考虑到查找和加载append函数所需的时间,列表理解仍然更快,因为列表是在C中创建的,而不是在python中一次构建一个项目。

1
2
3
4
5
6
7
8
9
10
11
12
# Slow
timeit.timeit(stmt='''
    for i in range(10000):
        t.append(i)'''
, setup='t=[]', number=10000)

# Faster
timeit.timeit(stmt='''
    for i in range(10000):
        l(i)'''
, setup='t=[]; l=t.append', number=10000)

# Faster still
timeit.timeit(stmt='t = [i for i in range(10000)]', number=10000)


引用本文,这是因为listappend属性没有作为函数进行查找、加载和调用,这需要时间,并且会随着迭代而累积。