【问题标题】:Python - append VS extend efficiencyPython - 追加 VS 扩展效率
【发布时间】:2013-01-04 22:47:41
【问题描述】:

这是我使用 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 一次,有什么区别? 我知道这可能是微小的差异,但我真的很想知道:)

【问题讨论】:

  • 如果您想知道,请测量。
  • 如果 extend 不快于 2 .appends,我会非常惊讶
  • 如果我要优化它,我会使用the sieve of Eratosthenes 来查找直到sqrt(28123) 的素数,然后对于每个i,我会对其进行因式分解并使用itertools.product 得到将因子组合成除数的所有方法,最后将它们相加。

标签: python list performance


【解决方案1】:
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 core i7)的结果排序相同:

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

对我来说,这说明extendappend 更快,但与创建tuple 相比,创建list 相对昂贵


编辑

在下面的 cmets 中指出,由于元组的不变性,解释器可以优化元组的创建(它创建一次元组并一遍又一遍地重用它)。如果我们把代码改成:

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 真的没关系。

【讨论】:

  • 嗯,刚刚学会了“time.clock”功能,出乎意料的是,这表明extend要慢得多……将编辑我的消息
  • 元组版本更快,因为您使用的元组是文字,因此可以重复使用(参见字节码),因此不必一次又一次地构造它。使用变量(例如传递一个对象追加到所有函数),差异会减少。
  • 很好的答案,谢谢。总是值得提出新问题,总是学习新东西:)
  • @delnan -- 很棒的收获!虽然,tuple 在几次重复运行后仍将其淘汰。
  • @delnan -- 我正在研究它。我添加了一个解释这些东西的编辑。我通常不会在答案中留下糟糕的时间安排,但在这种情况下,我觉得让两个版本都显示文字元组比文字列表快得多是有启发性的,但是在使用变量时它们相对接近。
【解决方案2】:

它们所用的时间完全相同。

这是您的代码所花费的时间:

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

【讨论】:

  • 你必须包含你的计时码,因为这很容易搞砸。而且由于您显然没有使用timeit,因此您必须证明不使用timeit ;-)
  • mgilson 的基准测试表明dividor_list.extend((i/j,j)) 比两者都更有效,因为它不会创建中间列表。创建中间元组更便宜。
  • @StevenRumbalski 不,它只是表明一个 LOAD_CONST 比 2 到 3 个 LOAD_FASTs、一些其他操作和一个 BUILD_LIST 快;-)
  • @delnan。取点。使用您建议的修改,元组版本在我的系统上的时钟仍然快一点。但它的改进太少了,无法真正关心使用哪个。
【解决方案3】:

还值得指出的是,这个问题的答案取决于每次迭代中添加的列表/元组的小尺寸。对于较大的列表,extend 显然更胜一筹(列表与元组没有区别)。从 mgilsonanswer 开始,我检查了包含 600 个项目而不是 2 个项目的集合的行为: 调用 append 600 次所花费的时间是使用 extend() 和手动定义的列表/元组(即 [v,v,v,v,v,v,v...])的时间的 8 倍:

42.4969689846
5.45146393776
5.38034892082

这五秒钟的大部分时间实际上是创建列表/元组。在timeit 调用之前准备它会导致时间延长到

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_tup(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_tup([], repetitions)', setup='from __main__ import extend_tup, repetitions')

(Append 是通过 for 循环和列表推导来实现的,以消除两种循环方式之间的效率差异。)

时间是:

53.8211231232
57.1711571217
19.8829259872
28.5986201763

正如我们所见,使用列表推导式扩展仍然比追加快两倍多。此外,元组理解似乎比列表理解慢得多,并且append_comp 只会引入不必要的列表创建开销。

【讨论】:

  • 后面的 (extend_tup) 实际上是一个 genexp 而不是一个元组,这就解释了缓慢的原因。
  • 你是对的输出类型,我的错。但是,我刚刚测试了元组理解,它提供了与 genexp 相同的速度。列表理解仍然更快。显然,如果元组是预先计算的,调用会更快,但对于预先计算的列表也是如此。
  • 您的基准测试有偏差,因为它包括构建列表和元组所需的时间。没有它,使用元组扩展列表会更快一些(至少对我来说,使用 python 3.5)。
  • 您的测试具有误导性,正确的测试表明 append 是最快的方法。您使用的两个附加测试都是一次附加一个项目,但 extend() 是针对整个列表的。当我更改 append_comp 的代码以使附加不在理解列表中时(如在最后 2 个测试中),最后 3 个测试的时间是(在我使用 Jupyter 实验室解释器的慢速机器上的测试中):91、104 , 148 秒。
  • @Zvi,我不确定你发现了什么误导。最初的问题询问是否多次使用追加比使用一次扩展更快或更慢,因此代码差异是设计使然。您建议的更改没有提供正确的结果,因为它会将整个代表列表作为单个项目附加,而不是附加单个元素,即您将得到 [[1,2,3...]] 而不是 [1 ,2,3...]。
猜你喜欢
  • 2014-11-10
  • 2018-06-02
  • 2011-03-12
  • 1970-01-01
  • 2015-01-10
  • 2013-05-19
  • 2019-01-19
  • 2013-05-06
  • 2012-06-03
相关资源
最近更新 更多