我无法解释的列表追加和 + 运算符之间的时间差异

Difference in time between list append and + operator that I can't explain

我正在尝试在 Windows 7 上使用 Python 3.4 中的 time.clock() 来比较 append+= 的速度。 (这是 Think Python 第 10 章的练习。)这是我的代码:

import time
fin = open('words.txt')
print("Comparing the time it takes to make a list with append vs. the + operator")

timeConcatStart = time.clock()
wordsList = []
for line in fin:
    word = line.strip()
    wordsList += word
timeConcatEnd = time.clock()
concatElapsed = timeConcatEnd - timeConcatStart
print("+ operator took ",concatElapsed,"seconds for words.txt")

timeAppendStart = time.clock()
wordList = []
for line in fin:
    word = line.strip()
    wordList.append(word)
timeAppendEnd = time.clock()
appendElapsed = timeAppendEnd - timeAppendStart
print("Append took ",appendElapsed,"seconds for words.txt")

在我的系统上,我得到 + 运算符花费了大约 0.2 秒,append 花费了 0.002 秒。

当我切换顺序使追加代码块排在第一位时,append 现在需要 0.2 秒,而 + 运算符需要 0.002 秒。

如果我复制并粘贴代码,两者都会变得更快,"third" 块耗时约 8e-5 秒,"fourth" 耗时约 4e5。然后,如果我再次复制和粘贴,都需要大约 5e-5 秒。更令人困惑的是,如果我去掉所有复制和粘贴的代码,然后只是将变量名 wordList 的第二次出现更改为 otherList,则 otherList 实际上 faster。也就是说,第一个块我得到的时间是 0.2 秒,第二个块是 ~5e-5。

为什么第一个循环总是耗时最长? open 对象是否将 words.txt 文件加载到内存中,然后将其保存在那里以备下次需要时使用?我以为有一个隐含的 "close".

或者它与文件无关I/O而是与分配给列表的内存有关?也就是说,是不是因为我在第一个循环中 "pre-allocated" wordsList 然后在第二个循环中写了它?我尝试更改变量名称来检验该假设,然后我得到了更短的时间。

最后,您test/debug这些想法如何?我应该插入打印语句来获取变量的地址还是什么? Obvs 我是调试菜鸟。 请随时纠正我所做的任何术语失误,感谢您的帮助

这里发生的事情是您耗尽了文件对象。文件对象使用当前文件位置从文件中读取下一个字节,并循环读取文件直到文件结束。

你的第二个循环然后产生无结果,因为文件仍然在结束位置。您可以在第二个 for line in fin: 循环之前使用 fin.seek(0) 再次从文件开头开始读取,但这里还有其他问题。

您正在测试 OS 和您的硬件传输文件数据的速度;进行基准测试时,请始终尽可能多地消除外部因素。例如,OS 会将读取的文件数据缓存在内存中一段时间​​,这肯定会使结果发生偏差。

另一个问题是你 运行 每种方法只用一次,让你的基准对其他偏见敞开大门;您计算机上的其他进程 运行 也需要时间,并且可能会对结果产生不公平的影响。

为了避免这种偏差,Python 附带了一个名为 timeit 的基准测试模块,它通过 运行 多次测试 来避免陷阱 (默认值为 100 万),为您的 OS 选择最准确的计时机制,并禁用垃圾收集系统等潜在偏差。你应该在开始时给它完全相同的测试数据(在内存中,而不是从文件中)。

最后但同样重要的是,你比较的是错误的东西。 +=list.append() 不同。这里,+= 在功能上等同于 list.extend();列表扩展为单独添加的添加序列中的每个元素,not 作为单个元素:

>>> lst = []
>>> lst += 'foobar'
>>> lst
['f', 'o', 'o', 'b', 'a', 'r']
>>> lst.append('foobar')
>>> lst
['f', 'o', 'o', 'b', 'a', 'r', 'foobar']

因此,list.append() 会更快,因为在这种情况下一次只添加一个元素:

>>> from timeit import timeit
>>> timeit('lst.append(value)', 'lst = []; value = "foobar"')
0.07227020798018202
>>> timeit('lst += value', 'lst = []; value = "foobar"')
0.15380392200313509

第一个测试将字符串 'foobar' 添加到 lst 100 万次,而第二个测试通过添加单个元素 'f''o'、[ =24=、'b''a''r' 一百万次。毫不奇怪,第二次测试的运行速度不到一半。

如果要使用+=一个元素添加到列表中,您需要将该元素包装在另一个序列中;单例列表对象,例如:

>>> timeit('lst += [value]', 'lst = []; value = "foobar"')
0.14356198499444872

这仍然比使用 list.append() 慢得多,但至少现在您获得了相同的最终结果。