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

raf*_*elc 36 python list-comprehension list python-2.7 python-3.x

我想知道为什么列表理解比附加到列表要快得多.我认为差异只是表达,但事实并非如此.

>>> 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
Run Code Online (Sandbox Code Playgroud)

列表理解速度提高了50%.为什么?

Kas*_*mvd 72

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

请考虑以下示例:

# 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
Run Code Online (Sandbox Code Playgroud)

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


che*_*ner 12

即使分解查找和加载append函数所花费的时间,列表理解仍然更快,因为列表是用C创建的,而不是在Python中一次构建一个项目.

# 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)
Run Code Online (Sandbox Code Playgroud)

  • * list是用C *创建的,*迭代是用C杠杆*执行的,依此类推。这是关于Python的最普遍,完全错误的神话之一。列表理解速度更快是因为挂起和恢复函数的帧速度很慢,而不是因为列表理解没有什么特别之处。 (4认同)
  • @chepner 如果你想说的话,Python 的 CPython 实现中的所有内容都是用 C 编写的。关于列表理解的错误假设是,人们认为它以某种方式神奇地直接在 C 中执行循环,这是不正确的。它不像之前在 C 中定义的内置函数。 (3认同)
  • “Slow”和“Faster”代码示例创建了包含 `100000000` 项的列表(如果我们修复了缩进错误)(`number` 循环不会重复 `setup`)。列表推导式创建了一个包含 10000 个项目 10000 次的列表。您可能指的是 `python -mtimeit "t=[]" "for i in range(10000): t.append(i)"` 与 `python -mtimeit "t=[]" "t_append=t.append" "for i in range(10000): t_append(i)"` vs. `python -mtimeit "t=[i for i in range(10000)]"`,虽然它不会改变结论(慢,快,快点)。 (2认同)

ada*_*rsh 6

引用这篇文章,这是因为它的append属性list没有被查找,加载和作为一个函数调用,这需要花费时间并且会增加迭代次数.