任务
你想从一个列表中随机获取元素,就像random.choice 所做的一样,但同时必须根据另一个列表指定的各个不同元素的概率来获取元素,而不是用等同的概率撷取元素
解决方案
Python 标准库中的 random 模块提供了生成和使用伪随机数的能力,但是它并没有提供这样特殊的功能,所以,我们必须得自己写一个函数:
import random
def random_pick(some_list,probabilities):
x = random.uniform(0, 1)
cumulative_probability = 0.0
for item,item_probability in zip(some_list,probabilities):
cumulative_probability += item_probability
if x < cumulative_probability:break
return item
讨论
Python 标准库中的 random 模块并没有提供根据权重做出选择的功能,这种功能在游戏、模拟和随机测试中是很常见的需求,所以,本节的目标是提供此功能的实现。解决方案使用了random 模块的 uniform函数获得了一个在0.0和1.0 之间分布的伪随机数,之后同时循环元素及其概率,计算不断增加的累积概率,直到这个概率值大于伪随机数。
本节假设(但并未检查)概率序列probabilities具有和some_list一样的长度,其所有元素都在 0.0和 1.0之间,且相加之和为 1.0;如果违反了这个假设,仍能进行一些随机的撷取,但不能完全地遵循(不连贯)函数的参数所规定的行为。可能想在函数开头加上一些 assert 语句以确保参数的有效性:
assert len(some_list) == len(probabilities)
assert 0 <= min(probabilities) and max(probabilities) <= 1
assert abs(sum(probabilities)-1.0) < 1.0e-5
不过,这些检查会消耗一些时间,所以我通常都不这么做,在正式的解决方案中我也没有把它们纳入。
正如前面提到的,这个任务要求每一项都有一个应对的概率,这些概率分布在0和1之间,且总和相加为1。另一个有点类似的任务是根据一个非负整数的序列所定义的权重进行随机撷取——基于机会,而不是概率。对于这个问题,最好的解决方案是使用生成器,其内部结构和解决方案中的 random_pick 函数差异很大:
import random
def random_picks(sequence,relative_odds):
table = [z for x,y in zip(sequence,relative_odds) for z in [x]*y ]
while True:
yield random.choice(table)
生成器首先准备一个 table,它的元素的数目是 sum(relative_odds)个,sequence 中的每个元素都可以在 table 中出现多次,出现的次数等于它在relative_odds序列中所对应的非负整数。一旦 table 被制作完毕,生成器的主体就可以变得又小又快,因为它只需要将随机撷取的工作委托给 random.choice。举个例子,关于这个random_picks 的典型应用:
>>>x = random_picks('ciao',[1,1,3,2])
>>>for two_chars in zip('boo',x):print ''.join(two_chars),
bc oa oa
>>>import itertools
>>>print ''.join(itertools.islice(x,8))
icacaoco