Python Cookbook-5.6 以随机顺序处理列表的元素
任务
你想以随机的顺序处理一个很长的列表。
解决方案
一如既往的,在 Python 中最简单的方法常常是最好的。如果我们允许修改输入列表中的元素的顺序,那么下面的函数就是最简单和最快的:
def process_all_in_random_order(data,process):
#首先,将整个列表置于随机顺序
random.shuffle(data)
#然后,根据正常顺序访问
for elem in data:process(elem)
如果我们需要保证输入列表不变,或者输人列表可能是其他可选代对象而不是列表可以在函数主体开头加上一条赋值语句data = list(data)。
讨论
虽然过度关心速度常常是个错误,但我们也不能忽略不同算法的性能。假设我们必须以随机顺序处理一个不重复的长列表的元素。第一个想法可能会是这样:我们可以反复地、随机地挑出元素(通过random.choice 函数),并将原列表中被挑选的元素删除以避免重复挑选:
import random
def process_random_removing(data,process):
while data:
elem = random.choice(data)
data.remove(elem)
process(elem)
然而,这个函数慢得可怕,即使输入列表只有几百个元素。每个data.remove 调用都会线性地搜索整个列表以获取要删除的元素。由于第n步的时间消耗是O(n),因此整个处理过程的消耗时间是〇(n2),正比于列表长度的平方(而且要乘上一个很大的常数)。
对第一个想法的一点提高是将注意力集中在获取随机索引上,并使用列表的pop方法来同时获取和删除元素,这种更底层的方式避免了较大的消耗,尤其是在某些情况下比如要被挑选的元素位于列表的最后,或者使用的压根不是列表,而是字典或集合。若我们面对的是字典或集合,一条思路是寄希望于使用dict的popitem方法(或者sets.Set 或 Python 2.4 内建类型 set 的等价的 pop 方法),看上去这个函数好像被设计为随机选择一个元素并删除之,但是,小心上当。
dict.popitem 的文档指出,它返回并删除字典中的任意一个元素,但这和真正的随机元素还差得很远。看看这个:
>>> d = dict(enumerate('ciao'))
>>> while d: print d.popitem()
你可能会很吃惊,在大多数的 Python 实现中,这个代码片段都将以看上去不太随机的一句话,如果需要 Python 中的方式打印d的元素,通常是(0,‘c’),然后(1,‘i’),等等。伪随机行为,需要的是标准库的randompopitem 模块。
如果你考虑使用字典而不是列表,那么你肯定在“Python 式思维”的路上又前进了一步,虽然字典并不会针对这个特定问题提供什么性能优势。但相比于选择正确的数据结构,更具有Python 风格的方式是:总是利用标准库。Python标准库是个庞大、丰富的库,塞满了各种有用的、强健的、快速的函数和类,可满足各种应用的需求。在这个前提下,最关键的一点是要意识到,想要以随机的顺序访问序列,最简单的方法是首先将序列转化成随机的顺序(也被称为对序列洗牌,是对扑克洗牌的类比),然后再线性的访问洗完牌的序列即可。random.shufle 函数就可以执行洗牌操作,本节解决方案正是利用了这个函数。
实际性能总是需要测试,而不是猜测出来的,那也正是标准库模块 timeit 存在的原因。使用一个空的 process 函数和一个长度为1000的列表作为data,process_all in_randomorder 能比 process random removing快大约10倍;对于长度为2000 的列表,这个比例变成了 20。如果提升仅仅是 25%,或者是一个常数因子2,那么通常这个性能差异是可以忽略的,因为这不会对你的整体应用产生什么性能影响,但如果算法慢了10或20倍,情况就不同了。这种可怕的低效会成为整个程序的瓶颈。当我们谈到(n2)和O(n)的行为对比时,问题的严重性根本无法忽视:对于这两种大 0的行为,随着输入数据的增长,它们消耗时间的差异可以无限地递增下去。