Python 使用for循环与列表理解来优化代码

Python 使用for循环与列表理解来优化代码,python,python-3.x,optimization,list-comprehension,Python,Python 3.x,Optimization,List Comprehension,为什么使用for循环的几乎全部比使用列表理解的几乎全部更有效?他们肯定都是O(n)? 编辑:另一个问题的答案是非具体的,基本上说一个比另一个更快,这取决于具体情况。那么这个案子呢 def almost_all_a(numbers): sumall = sum(numbers) rangelen = range(len(numbers)) return [sumall - numbers[i] for i in rangelen] def almost_all_b(num

为什么使用for循环的几乎全部比使用列表理解的几乎全部更有效?他们肯定都是O(n)? 编辑:另一个问题的答案是非具体的,基本上说一个比另一个更快,这取决于具体情况。那么这个案子呢

def almost_all_a(numbers):
    sumall = sum(numbers)
    rangelen = range(len(numbers))
    return [sumall - numbers[i] for i in rangelen]

def almost_all_b(numbers):
    sumall = sum(numbers)
    for i in range(len(numbers)):
        numbers[i] = sumall - numbers[i]

相关问题的答案包罗万象,但不知何故被隐藏了起来

让我们看看什么是
几乎所有的
:它构建一个与原始列表大小相同的新列表,然后返回该新列表。对于大型列表,这将使用两倍于列表所需的内存(假设此处为数字列表)。如果您这样调用函数:
nums=small\u a(nums)
,您只是在构建一个新列表,完成后会丢弃上一个列表。两个性能影响:需要(临时)内存,并且需要垃圾收集器清理旧列表

几乎所有
中,这一切都没有发生:您只是在适当地更改列表元素:没有额外的分配(内存增益)和收集(执行时间增益)

TL/DR:是什么让<代码>版本丢失,它归结为分配一个新列表,而

使用列表理解代替不构建列表的循环,无意义地累积一个无意义值的列表,然后扔掉该列表,由于创建和扩展列表的开销,通常速度较慢


您的复杂性分析是正确的:
n
计算总和的操作加上
n
在这两种情况下计算列表的操作使
O(n)

但是在我们谈论速度之前,你肯定已经注意到,
几乎所有的b
都有副作用,而
几乎所有的a
都没有副作用。更糟糕的是,
几乎所有的b
都不是幂等的。如果反复调用几乎所有的
,则每次都会修改参数
数字除非你有很好的理由,否则你应该更喜欢
几乎所有的a
而不是
几乎所有的b
,因为它更容易理解,也不容易出错

基准1 我将尝试用
timeit
确认您的断言(
small\u a
[比
small\u b
更有效):

>>> from timeit import timeit
>>> ns=list(range(100))
>>> timeit(lambda: almost_all_a(ns), number=10000)
0.06381335399782984
>>> timeit(lambda: almost_all_b(ns), number=10000)
2.3228586789991823
几乎所有的a
大约比
几乎所有的b
快35倍!!!不,那是个玩笑。你可以看到发生了什么:
几乎所有的
都被应用了10000次到
[1,…,90]
,并产生了副作用,因此数字急剧增加:

>>> len(str(ns[0])) # number of digits of the first element!
19959
好的,那只是为了说服你避免使用有副作用的函数

基准2 现在,真正的考验是:

>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_a})
5.720672591000039
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_b})
5.937547881
请注意,基准可能会在另一个列表或另一个平台上给出不同的结果。(想想如果列表占用了90%的可用RAM会发生什么。)但是让我们假设我们可以概括

Python字节码 让我们看看字节码:

>>> import dis
>>> dis.dis(almost_all_a)
  2           0 LOAD_GLOBAL              0 (sum)
              2 LOAD_DEREF               0 (numbers)
              4 CALL_FUNCTION            1
              6 STORE_DEREF              1 (sumall)

  3           8 LOAD_GLOBAL              1 (range)
             10 LOAD_GLOBAL              2 (len)
             12 LOAD_DEREF               0 (numbers)
             14 CALL_FUNCTION            1
             16 CALL_FUNCTION            1
             18 STORE_FAST               1 (rangelen)

  4          20 LOAD_CLOSURE             0 (numbers)
             22 LOAD_CLOSURE             1 (sumall)
             24 BUILD_TUPLE              2
             26 LOAD_CONST               1 (<code object <listcomp> at 0x7fdc551dee40, file "<stdin>", line 4>)
             28 LOAD_CONST               2 ('almost_all_a.<locals>.<listcomp>')
             30 MAKE_FUNCTION            8
             32 LOAD_FAST                1 (rangelen)
             34 GET_ITER
             36 CALL_FUNCTION            1
             38 RETURN_VALUE
开始几乎是一样的。然后,你有一个列表理解,就像一个黑匣子。如果我们打开该框,我们会看到:

>>> dis.dis(almost_all_a.__code__.co_consts[1])
  4           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                16 (to 22)
              6 STORE_FAST               1 (i)
              8 LOAD_DEREF               1 (sumall)
             10 LOAD_DEREF               0 (numbers)
             12 LOAD_FAST                1 (i)
             14 BINARY_SUBSCR
             16 BINARY_SUBTRACT
             18 LIST_APPEND              2
             20 JUMP_ABSOLUTE            4
        >>   22 RETURN_VALUE
你有两个不同点:

  • 在列表理解中,
    sumall
    numbers
    加载的是
    LOAD\u DEREF
    ,而不是
    LOAD\u FAST
    (这对于闭包来说是正常的),应该稍微慢一点
  • 在列表理解中,
    list\u APPEND
    将赋值替换为
    numbers[i]
    LOAD\u FAST(numbers)/LOAD\u FAST(i)/STORE\u SUBSCR
    第36-40行)
我的猜测是,开销来自于那个任务

另一个基准 您可以将
几乎所有的
重写得更加整洁,因为您不需要索引:

def almost_all_c(numbers):
    sumall = sum(numbers)
    return [sumall - n for n in numbers]
这个版本(在我的示例+平台上)比
几乎所有
更快:

>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_a})
5.755438814000172
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_b})
5.93645353099987
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_c})
4.571863283000084
(注意,在Python中,整洁的版本通常是最快的。)几乎所有的
和几乎所有的
之间的区别在于使用了对
i
-第
numbers
项的访问权(您可以对要检查的
几乎所有的
进行反编译)

结论 我觉得这里的瓶颈是访问
I
-第
项编号

  • 一次在
    几乎所有的
    
    
  • 中有两次几乎全部
    
    
  • 从不在
    几乎所有的\u c

这就是为什么
几乎所有的a
几乎所有的b
快的原因。可能的重复:嗨,谢谢你,我仍然习惯于堆栈溢出,但是我仍然看不出在这种情况下列表迭代如何更有效?第二种算法无法通过较大列表的测试。
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_a})
5.755438814000172
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_b})
5.93645353099987
>>> timeit('ns=list(range(100));almost_all(ns)', globals={'almost_all':almost_all_c})
4.571863283000084