
前言「带权重的随机」指的是从一组候选中抽一个但每个候选被抽中的概率不相等而是按给定权重分配。典型场景是抽奖大奖权重低、谢谢参与权重高、A/B 实验分流、游戏掉落表、按比例采样测试数据。很多人第一反应是「先把权重展开成一个大列表再random.choice」——比如权重 90 和 10 就造一个含 90 个 A、10 个 B 的列表。这在小数据量下能用但权重一大就会爆内存而且完全没必要。标准库已经提供了原生的加权抽样random.choices()。另一个常见误解是把这件事写成「测试」就以为涉及自动化测试框架。本文说的「随机测试」是用带权重的随机数做抽样验证 / 模拟不涉及pytest、unittest。本文基于 Python 3.8核心的random.choices()自 Python 3.6 起可用。安全用途生成令牌、随机密码不能用random模块文末会说明该换成什么。一、核心函数random.choices它的官方签名是random.choices(population, weightsNone, *, cum_weightsNone, k1)要点逐条核对参数是否关键字说明population否位置被抽取的序列weights否位置相对权重长度必须与population相同cum_weights是仅关键字累积权重替代weights使用k是仅关键字抽取个数默认 1返回值是长度为 k 的list且是有放回with replacement抽取同一元素可能重复出现。如果population为空会抛IndexError。# 适用于 Python 3.8random.choices 自 3.6 起可用import randomprizes [特等奖, 一等奖, 二等奖, 谢谢参与]weights [1, 5, 20, 74]random.seed(42) # 固定种子结果可复现result random.choices(prizes, weightsweights, k10)print(result)print(len(result)) # 10永远是 list因为内部做了加权k10抽出来的十条里「谢谢参与」出现的次数通常最多但单次实验不保证一定如此——随机就是随机别把一次结果当规律。二、相对权重与累积权重weights是相对权重只看比例不看绝对值[1, 5, 20, 74]与[0.01, 0.05, 0.2, 0.74]等价。cum_weights是累积权重。相对权重[10, 5, 30, 5]对应的累积形式是[10, 15, 45, 50]逐个累加。官方文档明确说明内部会先把相对权重转成累积权重再抽样所以直接提供累积权重可以省掉这步转换。累积权重可以用itertools.accumulate计算# 适用于 Python 3.8import randomfrom itertools import accumulateoptions [A, B, C, D]rel [10, 5, 30, 5]cum list(accumulate(rel))print(cum) # [10, 15, 45, 50]random.seed(1)print(random.choices(options, cum_weightscum, k6))注意weights和cum_weights不能同时给否则抛TypeError。三、用 bisect 手写加权抽样如果出于教学或特殊需求要自己实现标准做法是「累积权重 bisect」这也是random.choices内部思路的简化版# 适用于 Python 3.8import bisectimport randomfrom itertools import accumulatedef weighted_choice(items, weights):cum list(accumulate(weights))r random.random() * cum[-1] # [0, total) 上的均匀随机数idx bisect.bisect(cum, r)return items[idx]random.seed(7)options [A, B, C]w [1, 2, 7]counts {A: 0, B: 0, C: 0}for _ in range(10000):counts[weighted_choice(options, w)] 1print(counts)bisect.bisect返回插入位置等价于「第一个大于r的位置」正好定位到区间。手写版的意义在于理解原理生产代码优先用random.choices它经过优化且边界处理更稳。四、几个相关函数的区别函数有放回支持权重返回random.choice(seq)单次无从谈起否单个元素random.choices(population, weights..., k...)是是listrandom.sample(population, k, *, countsNone)否用counts近似listrandom.shuffle(x)原地打乱否None选型口诀要能重复抽到就choices要各不相同就sample只要一个就choice。random.sample的counts参数自 Python 3.9 起可用语义是「每个元素在总体中出现的次数」sample([红, 蓝], counts[4, 2], k5)等价于从[红,红,红,红,蓝,蓝]里不重复地抽 5 个。五、可复现与边界条件# 适用于 Python 3.8import randomrandom.seed(123)a random.choices([x, y], weights[3, 1], k5)random.seed(123)b random.choices([x, y], weights[3, 1], k5)print(a b) # True同一种子同序列设定种子只保证同一 Python 实现、同一版本下可复现跨版本、跨平台不保证序列一致所以不要把随机结果当作跨机器的固定基准。关于边界官方文档写得很清楚权重必须是非负、有限的数值长度要与population一致长度不符抛ValueErrorTypeError用于同时给了weights和cum_weights的情况。自 Python 3.9 起所有权重都为 0会抛ValueError。常见坑点坑 1权重列表长度和候选项对不上。❌random.choices([A, B, C], weights[1, 2], k3)—— 抛ValueError: The number of weights does not match the population。✅ 保证len(weights) len(population)最好在函数入口先断言一次。坑 2同时传weights和cum_weights。❌random.choices(pop, weights[1,2], cum_weights[1,3])—— 抛TypeError。✅ 二选一。用累积权重时记得它是逐项累加的结果不是原始权重。坑 3以为choices抽出来不重复。❌ 用random.choices(ticket_ids, weightsw, k3)抽三个中奖号码结果可能抽到同一个 ID 两次。✅ 要不重复就用random.samplesample的样本量不能超过总体大小否则抛ValueError。坑 4把k写成位置参数。❌random.choices(population, weights, 10)——k是仅关键字参数会抛TypeError。✅random.choices(population, weights, k10)。坑 5所有权重为 0。❌random.choices([A, B], weights[0, 0], k1)—— Python 3.9 起抛ValueError: Total of weights must be greater than zero。✅ 保证至少有一个正权重权重可由配置驱动时加一层兜底校验。坑 6权重里塞了字符串或负数。❌weights[10, 90]或weights[-1, 2]—— 前者抛TypeError后者行为不符合预期文档要求非负。✅ 用数值类型并在投入抽样前做max(0, w)之类的清洗。坑 7拿random生成安全令牌。❌ 用random.choices(string.ascii_letters, k16)做密码重置令牌——random模块用的是可预测的伪随机算法不适合安全场景。✅ 改用secrets模块secrets.choice(alphabet)、secrets.token_urlsafe(16)。坑 8想复现却忘了在每次实验前重设种子。❌ 只random.seed(42)一次之后跑多个实验每个实验拿到的子序列取决于前面消费了多少随机数。✅ 每个独立实验前都重新random.seed(42)或改用random.Random(42)实例避免全局状态互相干扰。总结需求函数关键参数有放回单次加权抽取random.choices(pop, weightsw, k1)weights/cum_weights是k1 时无意义多次加权抽取random.choices(pop, weightsw, kn)k是关键字是不重复加权抽取random.sample(pop, k, counts...)counts需 3.9否只要一个random.choice(seq)不支持权重—可复现random.seed(n)/random.Random(n)同实现同版本才一致—把握三点就能用对weights是相对比例、k是仅关键字参数、choices有放回而sample无放回。需要安全随机时换secrets。想验证自己的权重配置是否符合预期最稳妥的做法是像本文示例那样跑大样本统计频率而不是盯着一次抽样结果下结论。