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

Posted

技术标签:

【中文标题】为啥列表理解比附加到列表要快得多?【英文标题】:Why is a list comprehension so much faster than appending to a list?为什么列表理解比附加到列表要快得多? 【发布时间】:2015-07-26 13:05:33 【问题描述】:

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

>>> 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%。为什么?

【问题讨论】:

相关问题:Are list-comprehensions and functional functions faster than “for loops”?、Python list comprehension expensive 为什么列表理解速度更快令人惊讶?这不是存在列表推导的主要原因吗? 最前沿的答案是,python 使用 C 的 list,而列表理解是 python 的内置功能。 【参考方案1】:

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

考虑以下示例:

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

In [3]: import dis                                                                                                                                                                                          

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

  3           4 LOAD_GLOBAL              0 (range)
              6 LOAD_CONST               1 (5)
              8 CALL_FUNCTION            1
             10 GET_ITER
        >>   12 FOR_ITER                14 (to 28)
             14 STORE_FAST               1 (i)

  4          16 LOAD_FAST                0 (l)
             18 LOAD_METHOD              1 (append)
             20 LOAD_FAST                1 (i)
             22 CALL_METHOD              1
             24 POP_TOP
             26 JUMP_ABSOLUTE           12
        >>   28 LOAD_CONST               0 (None)
             30 RETURN_VALUE

In [5]:                                                                                                                                                                                                     

In [5]: dis.dis(f2)                                                                                                                                                                                         
  8           0 LOAD_CONST               1 (<code object <listcomp> at 0x7f397abc0d40, file "<ipython-input-1-45c11e415ee9>", line 8>)
              2 LOAD_CONST               2 ('f2.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_GLOBAL              0 (range)
              8 LOAD_CONST               3 (5)
             10 CALL_FUNCTION            1
             12 GET_ITER
             14 CALL_FUNCTION            1
             16 POP_TOP
             18 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Disassembly of <code object <listcomp> at 0x7f397abc0d40, file "<ipython-input-1-45c11e415ee9>", line 8>:
  8           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 8 (to 14)
              6 STORE_FAST               1 (i)
              8 LOAD_FAST                1 (i)
             10 LIST_APPEND              2
             12 JUMP_ABSOLUTE            4
        >>   14 RETURN_VALUE

In [6]:   

您可以看到,在第一个函数的偏移 18 处,我们有一个 append 属性,而在使用列表理解的第二个函数中没有这样的东西。所有这些额外的字节码将使附加方法变慢,因为在这种情况下,您将在 每次迭代 中加载 append 属性,最终它将使代码花费大约两倍比仅使用列表理解的第二个函数慢。

【讨论】:

我相信第二个函数的反汇编没有显示实际 list-comp 函数的字节码,这令人困惑。 @guyarad 是的,但我仍然用 3.8 版本更新了代码。也许您的 Python 版本不同,因为 dis 在不同版本中可能会产生略有不同的结果。【参考方案2】:

即使考虑到查找和加载 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)

【讨论】:

“Slow”和“Faster”代码示例创建带有 100000000 项的列表(如果我们修复缩进错误)(setup 不会在 number 循环中重复)。列表推导式创建一个包含 10000 项目 10000 次的列表。你的意思可能是python -mtimeit "t=[]" "for i in range(10000): t.append(i)" vs. 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)]",虽然它不会改变结论(慢,快,快)。 list 是在 C 中创建的iteration 在 C 杠杆上执行,等等。这是关于 Python 的最普遍、完全错误的神话之一。列表推导更快是因为暂停和恢复函数的帧很慢,而不是因为列表推导有什么特别之处。 @Kasrâmvd hm,所以列表推导式有这个C 潜在收益是错误的说法吗?在谈论列表推导时,这是一个反复出现的陈述,以至于我将其作为绝对真理吸收了,而没有实际进行更深入的研究。这方面有什么好的参考吗? 它在某种意义上是“C 语言”,因为列表解析使用LIST_APPEND 指令将元素添加到列表中,而不必调用函数。 @chepner 如果你想说得好,Python 的 CPython 实现中的所有内容都在 C 中。关于列表理解的错误假设是人们以某种​​方式神奇地认为它直接在 C 中执行循环,这是不正确的。它不像以前在 C 中定义的内置函数。【参考方案3】:

引用this 文章,这是因为listappend 属性没有作为函数进行查找、加载和调用,这需要时间并且在迭代中累加。

【讨论】:

链接已失效。 是的,这个答案应该被更新或删除。目前,即使它包含的相关信息也被其他答案引用,所以我建议将其删除。 人们仍然可以使用该链接的网络存档,在这种情况下,这里是(但我仍然建议从该网站引用):web.archive.org/web/20100507015024/http://blog.cdleary.com/2010/…

以上是关于为啥列表理解比附加到列表要快得多?的主要内容,如果未能解决你的问题,请参考以下文章

为啥使用声明的变量作为参数调用 UDF 比使用硬编码参数调用要快得多

为啥性能测试显示我的代码列表比数组快得多? [复制]

react-native 渲染速度比从 firebase 加载数据要快得多

为啥加入从视图中选择前 N 比加入视图要快得多?

Android:为什么本机代码比Java代码要快得多

sh 将MySQL数据库克隆到同一服务器上的新数据库,而不使用转储文件。这比使用mysqldump要快得多。