Python random.Random 隔离状态:同一个种子,为什么调试一次结果就变了

10-01 3阅读

测试数据固定了随机种子,结果却在加一行调试代码后变化,常见原因是多个功能共用同一个随机状态。每次抽样都会推进状态,调试代码多取一次,后面的业务就从下一个位置开始。固定种子只固定起点,不会固定所有调用者各自拿到哪几个值。

把生成器对象显式传入函数,可以让状态归属变得清楚。测试夹具、页面演示和模拟实验分别持有自己的 Random 实例,就不会因为另一个模块顺手调用了一次随机函数而互相影响。这里的独立指软件状态隔离,不是对统计独立性的额外证明。

Python random.Random 隔离状态:同一个种子,为什么调试一次结果就变了

AI概念配图,非真实界面:以抽象物件说明本文主题,不代表运行结果。

一次创建生成器,连续取完整批次

下面程序用 Python 3 运行。draw 只负责从传入生成器取数,batch 才负责为一次独立实验创建起点。两个相同种子的实例产生相同批次;同一实例多取一个值之后,其后续序列则应与参考序列向后错开一位。

测试还故意在列表推导里每次重新创建同种子生成器。这样每次都回到同一初始状态,三个结果完全相同。这不是一种可靠的“每条记录随机化”方法;应该把创建放到循环外,再让状态随抽样自然前进。

from random import Random

def draw(rng, count):
    if not isinstance(count, int) or isinstance(count, bool) or count < 0:
        raise ValueError("count must be a nonnegative integer")
    return [rng.random() for _ in range(count)]

def batch(seed, count):
    return draw(Random(seed), count)

assert batch(41, 5) == batch(41, 5)
reference = batch(41, 6)
shared = Random(41)
assert shared.random() == reference[0]
assert draw(shared, 5) == reference[1:]

first, second = Random(41), Random(41)
draw(first, 100)
assert draw(second, 5) == batch(41, 5)
restarted = [Random(41).random() for _ in range(3)]
assert restarted == [reference[0]] * 3
assert batch(41, 0) == []
try:
    batch(41, -1)
except ValueError:
    pass
else:
    raise AssertionError("negative count accepted")

rng = Random(73)
rng.random()
snapshot = rng.getstate()
a = draw(rng, 4)
rng.setstate(snapshot)
b = draw(rng, 4)
assert a == b
print("independent streams and one-time seeding checks passed")
print("state restore:", a == b)

记录调用计划,而不只记录种子

输出的两组检查分别对应状态隔离与恢复。示例没有断言具体小数文本,而是对比相同操作产生的序列关系,因此重点是验证状态所有权。新增一个抽样调用、改变分支条件或更换抽样函数,都可能改变后续取值,即使种子没有变化。

如果测试需要重放失败场景,至少记录种子、输入数据、运行版本和抽样步骤。数据的遍历顺序也会影响随机值最终分配给哪条记录。把无序来源转换成明确顺序,再进行抽样,才能避免“随机数一样,分配对象却不一样”的误会。

getstate 与 setstate 可以在同一运行环境中保存和恢复当前位置。代码先快照,再读取四次,恢复后重复读取并核对相等。这个状态用于调试局部算法很方便,但不应把它随意当作跨语言、跨版本永久存储格式来交换。

可复现有边界,也有不同目标

Python 文档对兼容播种器配合 random 方法的序列作出有限保证,但多数抽样算法可以随版本变化。若长期实验使用 shuffle、sample 或其他高级方法,应固定并记录环境,升级后重跑代表性案例,不能把一个种子当作整个实验环境的替代品。

并发任务共享生成器时,哪个任务先取值还会受到调度影响。即使数据结构允许安全调用,也不代表每个任务会稳定获得相同序列。需要可重放任务时,提前制定每个任务的种子或状态分配方案,并保持任务标识与分配关系固定。

伪随机的可重现性适合测试和模拟,却不适合生成密码重置凭据等需要不可预测性的内容。那类需求应使用 secrets 等专门接口,并遵守凭据生命周期要求。不要为了让线上问题容易复现,就把真实凭据生成器换成固定种子的 Random。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。