Python - append与extend的效率对比

24

这是我使用Python编写的一些代码:

from math import sqrt
abundant_list = []

for i in range(12,28123+1):
    dividor_list = [1]
    for j in range(2, int(sqrt(i))+1):
        if i%j == 0:
            dividor_list.extend([i/j,j])
    if sum(dividor_list) > i:
        abundant_list.append(i)

print abundant_list

正如您所见,代码确实尽可能地追求效率。

如果我使用 list.append 两次,或者仅使用一次 list.extend 是否有任何区别呢?我知道这可能是微小的差异,但我真的很想知道:)


9
如果你想知道,就去衡量。 - NPE
1
如果extend不比2个.appends更快,我会感到非常惊讶。 - mgilson
1
如果我要进行优化,我会使用埃拉托斯特尼筛法来找到小于sqrt(28123)的质数,然后对于每个i,我会将其分解因式并使用itertools.product获取将因子组合成除数的所有方式,最后将它们相加。 - Lauritz V. Thaulow
4个回答

26
import timeit

def append2x(foo):
    foo.append(1)
    foo.append(1)

def extend_lst(foo):
    foo.extend([1,1])

def extend_tup(foo):
    foo.extend((1,1))


l1 = []
l2 = []
l3 = []

print timeit.timeit('append2x(l1)',setup = 'from __main__ import append2x,l1')
print timeit.timeit('extend_lst(l2)',setup = 'from __main__ import extend_lst,l2')
print timeit.timeit('extend_tup(l3)',setup = 'from __main__ import extend_tup,l3')

这里有一个简单的基准测试。我的结果(os-X,10.5.8,core2duo,FWIW):

0.520906925201  #append
0.602569103241  #extend-list
0.357008934021  #extend-tuple

同样的顺序在我的Linux桌面电脑(Ubuntu,x86-64核心i7)中得出:

0.307395935059  #append
0.319436073303  #extend-list
0.238317012787  #extend-tuple

对我来说,这句话的意思是extendappend更快,但与创建list相比,创建tuple相对昂贵


编辑

在下面的评论中指出,由于元组的不可变性,解释器可以优化元组的创建(它只创建一次元组并反复使用它)。如果我们改变代码为:

def extend_lst(foo):  
    v = 1
    foo.extend([v,v]) 

def extend_tup(foo):
    v = 1
    foo.extend((v,v))

时间几乎完全相同:

0.297003984451  #append
0.344678163528  #extend-list
0.292304992676  #extend-tuple
尽管我所做的所有试验中,tuple 仍然始终胜过列表版本,并且在几乎不及 append 版本的情况下略微领先。从中我得到了一个结论,如果您正在迭代由所有文字组成的对象,请选择 tuple 而不是 list。如果它不完全由文本组成,则选择 listtuple 都无所谓。

5
元组版本更快,因为您使用的元组是一个文字,因此可以重复使用(与字节码相比),因此不必反复构建。如果使用变量(例如将对象传递给所有函数的附加项),则差异将减小。 - user395760
1
伟大的答案,谢谢大家。总是值得打开新问题,不断学习新知识 :) - Yam Mesicka
2
@delnan -- 我正在处理这个问题。我已经添加了一个编辑来解释这些内容。通常我不会在答案中保留时间差的信息,但在这种情况下,我觉得将两个版本都保留下来以显示字面元组比字面列表快得多,但使用变量时它们相对接近是很有启发性的。 - mgilson
@Infinity -- time.clock 不是计时代码的正确方法。在尝试对事物进行基准测试时,您需要考虑很多微妙之处(正如您可以在我的原始答案中看到的错误)。timeit 模块使您无需担心其中 90% 的微妙之处 - SO 用户通常会捕捉到另外的 10% ;-) - mgilson
好的,下次会知道使用timeit :) 真的很感谢你们的回答,从中学到了很多 :) - Yam Mesicka
显示剩余5条评论

24
编辑:正如其他评论中所指出的,Python中没有元组推导,因为该语法已经用于生成器表达式。我已经澄清了描述。
值得指出的是,这个问题的答案取决于每次迭代添加的列表/元组的大小。对于较大的列表,使用extend明显优于append(而列表与元组之间没有区别)。从mgilson的答案开始,我检查了包含600个项目的集合的行为,而不是2个项目: 调用append 600次所需的时间是使用手动定义的列表/元组(即[v,v,v,v,v,v,v...])使用extend()的8倍:
42.4969689846
5.45146393776
5.38034892082

这五秒钟的大部分时间实际上是用于创建列表/元组。在调用timeit之前准备它可以将extend的时间降低到最低。
1.42491698265
0.657584905624

对于列表和元组,分别使用。
对于更加真实(和公平)的情况,可以在函数调用中动态生成数据。
import timeit

def append_loop(foo, reps):
    for i in range(reps):
        foo.append(i)

def append_comp(foo, reps):
    [foo.append(i) for i in range(reps)]

def extend_lst(foo, reps):
    foo.extend([i for i in range(reps)])

def extend_genexp(foo, reps):
    foo.extend((i for i in range(reps)))

repetitions = 600

print timeit.timeit('append_loop([], repetitions)', setup='from __main__ import append_loop, repetitions')
print timeit.timeit('append_comp([], repetitions)', setup='from __main__ import append_comp, repetitions')
print timeit.timeit('extend_lst([], repetitions)', setup='from __main__ import extend_lst, repetitions')
print timeit.timeit('extend_genexp([], repetitions)', setup='from __main__ import extend_genexp, repetitions')

(通过for循环和列表推导式来实现附加,以消除两种循环方式之间的效率差异。)
(计时如下:)
53.8211231232
57.1711571217
19.8829259872
28.5986201763

正如我们所看到的,使用列表推导的扩展仍然比追加快两倍以上。生成器表达式比列表推导明显慢。`append_comp`只会引入不必要的列表创建开销。

4
extend_tup实际上是一个生成器表达式而不是元组,这可以解释其缓慢性。 - yoch
1
你关于输出类型的说法是正确的,我的错误。不过,我刚刚测试了元组推导式,它提供了与生成器表达式相同的速度。列表推导式仍然更快。显然,如果元组预先计算,调用会更快,但对于预先计算的列表也是如此。 - Marc Schulder
1
你的基准测试有偏差,因为它包括构建列表和元组所需的时间。如果不计算这一点,在我的情况下(使用Python 3.5),将列表扩展为元组会更快一些。 - yoch
你的测试结果是误导性的,正确的测试结果表明append方法是最快的。你使用的两个append测试都是逐个添加项目,而extend()方法是用于整个列表的。当我改变了append_comp代码,使得append不在推导列表中(就像最后两个测试中一样),在我的慢机器上使用Jupyter lab解释器进行测试时,最后3个测试的时间分别为:91、104、148秒。 - Zvi
1
讲解得很清楚,我想纠正一下,(i for i in range(reps))是一个生成器,而不是元组。 - Paras Gupta
显示剩余2条评论

0

它们花费的时间完全相同。

这是您的代码所需的时间:

使用dividor_list.extend([i/j,j])

>>> 
0:00:00.410000
>>> 
0:00:00.383000
>>> 
0:00:00.389000

使用 dividor_list.append(i/j); dividor_list.append(j)

>>> 
0:00:00.400000
>>> 
0:00:00.390000
>>> 
0:00:00.381000

5
你需要包含你的计时代码,因为这很容易搞砸。而且既然显然你没有使用 timeit,你需要解释一下为什么不使用它;-) - user395760
mgilsonзҡ„еҹәеҮҶжөӢиҜ•иЎЁжҳҺdividor_list.extend((i/j,j))жҜ”е…¶д»–дёӨз§Қж–№жі•жӣҙжңүж•ҲпјҢеӣ дёәе®ғдёҚдјҡеҲӣе»әдёӯй—ҙеҲ—иЎЁгҖӮеҲӣе»әдёӯй—ҙе…ғз»„зҡ„жҲҗжң¬жӣҙдҪҺгҖӮ - Steven Rumbalski
@StevenRumbalski 不,它只是表明一个 LOAD_CONST 比 2 到 3 个 LOAD_FAST、一些其他操作和一个 BUILD_LIST 更快。;-) - user395760
1
@delnan。收到。使用元组版本并按照您的建议进行修改后,在我的系统上仍然可以稍微提高一些速度。但这种改进太小了,实际上无关紧要。 - Steven Rumbalski

0

只是另一个基准测试

这次在ipython中进行。如果你懒得创建一个新的Python脚本,可能会有用。

In [12]: %%time
    ...: l = []
    ...: for ii in range(10000):
    ...:     l += [ii]
    ...: 
CPU times: user 886 µs, sys: 0 ns, total: 886 µs
Wall time: 889 µs

In [13]: %%time
    ...: l = []
    ...: for ii in range(10000):
    ...:     l.append(ii)
    ...: 
CPU times: user 669 µs, sys: 92 µs, total: 761 µs
Wall time: 763 µs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接