python标准库中的随机数种子选取策略
太长不看:python优先使用系统硬件随机噪声(urandom)作为随机数种子,如果无法获得urandom则使用系统时间;随机数序列使用梅森旋转算法(Mersenne Twister)实现,以生成足够随机的数字。
一、起因
某天在修改一个python编写的模块时,遇到了一个问题。该模块使用python标准库 random
生成随机序列,每次调用时都会使用 random.seed()
通过默认参数创建一个实例化对象。我怀疑这里通过默认参数得到的随机数不够随机,因此对python标准库中的随机数底层实现进行了探究。
二、一些需要知道的背景知识
(一)硬件随机数熵池(或urandom)
python中相关的接口是 os.urandom
。其调用的底层函数在Linux系统上是/dev/urandom
,在windows系统上是BCryptGenRandom()
。
/dev/random
和 /dev/urandom
是 Linux 上的字符设备文件,它们是 随机数生成器 ,为系统提供随机数;在Windows上,它使用的是CryptGenRandom
或BCryptGenRandom
(取决于Windows版本)。
随机数生成器会收集系统环境中各种数据,比如:鼠标的移动,键盘的输入, 终端的连接以及断开,音视频的播放,系统中断,内存 CPU 的使用等等;生成器把收集到的各种环境数据放入一个池子 ( 熵池 ) 中,然后将这些数据进行去偏、漂白,主要目的也是使得数据更加无序,更加难以猜测或者预料得到。
有了大量的环境数据之后,每次获取随机数时,从池子中读取指定的字节序列,这些字节序列就是生成器生成的随机数。
urandom或者说熵池,存储的是真随机数。
(二)梅森旋转算法(Mersenne Twister)
参考:谈谈梅森旋转:算法及其爆破 - 孟晨的文章 - 知乎 以及 Mersenne Twister 梅森旋转算法笔记 。更通俗易懂的解释见博客 你没听过的梅森旋转算法 。以下是对算法的简单介绍,而不是详细的原理分析。
这是一种由松本眞(Makoto Matsumoto)和西村拓士(Takuji Nishimura)在 1997 年提出的伪随机数生成算法。这个旋转算法实际上是对一个19937级的二进制序列作变换。在数学上我们可以证明一个定理,即对于一个长度为n的二进制序列,它的排列长度最长为 $2^n$ (实际上可能因为某些操作不当,没挺到 $2^n$ 个就开始循环了)。那么如何将这个序列的排列撑满 $2^n$ 个,就是这个旋转算法的精髓。
以下是梅森旋转算法的步骤:
- 利用 seed 初始化寄存器状态
- 对寄存器状态进行旋转
- 根据寄存器状态提取伪随机数
绝大多数标准库当中内置的随机数发生器,就是对梅森旋转算法的实现。而要生成不同的随机数序列,关键问题就在于seed(随机数种子)的选择。
好了,终于到了我们关心的环节:随机数种子的选择原理。
三、python标准库中random.seed()
的实现
(一)文档中的描述
为了搞清楚原理,我去查阅了官方文档和源代码。
下面是官方文档中的描述,其中提到seed()函数接受一个参数a作为随机数种子,但是用户不提供a也是可以的,python会选取一个默认值作为随机数种子。根据这里的描述,默认(a=None时),优先使用操作系统的随机熵源(如Linux的/dev/urandom
),若不可用,则回退到使用系统时间。
1 | random.seed(a=None, version=2) |
(二)表面上的源代码Lib/random.py
但其实这里的解释有些模糊,于是我去查询了源代码。首先查到的是 Lib/random.py
(但实际上这个并非真正的源代码)。Lib/random.py
的代码如下
1 | def seed(self, a=None, version=2): |
请注意,此处源代码中似乎只有将用户参数a转换为int类型的操作,并没有涉及到对当前时间的调用或对urandom的调用(甚至也没有实现梅森旋转算法)。seed函数的最后,调用了 super().seed(a)
这个函数接口,我也同样没有找到这个函数的定义。
有些沮丧,于是我去求助了chatGPT和Gemini。在它们的帮助之下,我找到了真正的源代码,并解决了最初的那个问题。
(三)真正的源代码 Modules/_randommodule.c
AI告诉我,前面查到的代码是random.Random.seed
函数的实现,而最后的 super().seed(a)
调用的是父类的seed()
方法。解决问题的关键就在这个父类上。
Random
类的继承关系是这样的:
random.Random
(Python实现)random._random.Random
(C语言实现,作为Python的内置模块)
大多数情况下,Python会用 C 实现的 _random.Random
,而这个C语言实现的源码位置在Modules/_randommodule.c
。
于是我去寻找了这个源代码,其内容如下:
1 | static int |
其中即涉及到seed默认值的选取,又涉及到梅森旋转算法的实现。我们要关注的是前者,对应的代码片段如下:
1 | if (arg == NULL || arg == Py_None) { |
这段代码的意思是这样:
- 程序首先尝试调用
random_seed_urandom(self)
。这个函数内部会尝试从os.urandom()
读取足够多的(通常是32字节)高质量随机字节来初始化Mersenne Twister(梅森旋转算法)的状态。 - 如果
os.urandom()
不可用或读取失败(< 0
),程序会清除这个错误,然后fall back到random_seed_time_pid(self)
,即获取当前的高精度时间戳和当前进程的ID(Process ID),将它们混合起来作为种子。这是一种质量较低但兼容性很强的后备方案。
因此,我们的最终结论是:默认种子的选择策略是 os.urandom
优先,失败则回退到 时间 + 进程ID
。