每个写过 NumPy 的人,几乎都写过这两行:
python
np.random.seed(42)
x = np.random.rand(100)
它简单、好用、看起来"随机又可控"。但如果你最近翻过 NumPy 官方文档,会看到一句明确的建议:新代码请使用 np.random.default_rng(),而不是 np.random 里的那些全局函数。
于是问题来了:同样是"生成随机数",np.random、np.random.default_rng(),还有 Python 自带的 random 模块,到底有什么不一样?为什么 NumPy 要在 1.17 版本(2019 年)大动干戈,引入一套全新的随机数体系?这篇文章把这三者的关系、本质区别和迁移方法一次讲清。
先给结论:random 是 Python 标准库的"裸"随机器;np.random 是 NumPy 旧版(legacy)全局随机接口,已被官方标记为不推荐;np.random.default_rng() 是当前唯一推荐的现代接口,返回一个拥有独立状态的 Generator 对象。 下面逐一拆解。
一、三句话分清三者
| 对象 | 所属 | 默认算法 | 状态管理方式 | 是否推荐 |
|---|---|---|---|---|
random(标准库) |
Python 自带 | Mersenne Twister | 模块级全局状态 | 适合简单标量场景 |
np.random.*(如 np.random.rand) |
NumPy 旧版 | MT19937 | 模块级全局状态 | 不推荐用于新代码 |
np.random.default_rng() |
NumPy 现代 | PCG64 | 每实例独立状态 | 强烈推荐 |
一句话记忆:np.random.default_rng() 不是 np.random 的"升级版函数名",而是把随机数从"全局共享状态"改成"独立可控对象"的一次设计范式迁移。
二、random:Python 自带的"底层"随机器
random 是 Python 标准库的一部分,不依赖 NumPy。它基于 Mersenne Twister(梅森旋转算法),生成的是 Python 标量浮点数,一次一个:
python
import random
random.seed(7)
print(random.random()) # 0.323832...
print(random.randint(1, 6)) # 掷骰子
它的特点是:
- 零依赖:不需要安装 NumPy,纯 Python 即可用。
- 只产标量:它没有"给我一个形状为 (1000, 1000) 的随机矩阵"这种能力,要批量就得写循环,性能差。
- 用途有限:适合抽奖、洗牌列表、小脚本里生成几个随机数这类"轻量"需求。
一旦你需要向量化、大规模采样或与 NumPy 数组打交道,random 就力不从心了------这正是 np.random 存在的意义。
三、np.random:好用但"有坑"的旧全局接口
在 NumPy 1.17 之前,大家用的就是 np.random。它的本质是:把一个全局隐藏的 RandomState 实例的方法,直接暴露成 np.random 命名空间下的函数。 也就是说,np.random.seed(42) 设置的是"整个程序共享的那一个全局状态"。
python
# 旧写法(仍可用,但不推荐新代码使用)
np.random.seed(42)
a = np.random.rand(3) # 来自全局状态
b = np.random.randn(3) # 继续消耗同一个全局状态
这种设计有三个绕不开的问题:
- 全局状态会"串台"。 你在 A 函数里
np.random.seed(42),B 函数里也悄悄调了np.random.*,两者会共用、互相消耗同一个随机流。在库代码里,这会让结果变得不可复现、极难调试。 - 不是线程安全的。 多个线程同时调用
np.random.*,会争抢同一个全局状态,可能得到错误或损坏的随机数。 - 算法偏旧、偏慢。 底层用的是 MT19937,相比现代算法在统计性质和生成速度上都略逊一筹。
正因如此,NumPy 官方明确建议:新代码迁到 Generator。不过为了向后兼容,np.random 这套旧接口目前仍然保留、仍能跑出和从前完全一致的结果------只是"不推荐",而非"已删除"。
四、np.random.default_rng:推荐的现代 Generator
NumPy 1.17 引入了"位生成器(BitGenerator)+ 生成器(Generator)"的分层架构,并用 np.random.default_rng() 作为推荐的构造函数:
python
# 新写法(推荐)
rng = np.random.default_rng(42)
a = rng.random(3) # [0,1) 均匀浮点
b = rng.standard_normal(3) # 标准正态
c = rng.integers(1, 7, size=3) # 掷三次骰子 [1,6]
它的核心设计是**"每个 rng 都是独立个体"**:
- 独立状态 :
default_rng()返回的是一个Generator实例,自带状态,不碰全局。你创建几个就有几条互不干扰的随机流。 - 分层架构 :底层
BitGenerator(默认 PCG64)只负责吐出原始随机比特;上层Generator把这些比特变成各种分布(正态、均匀、泊松等)。想换算法?把PCG64/Philox/SFC64/MT19937传给Generator即可。 - 更好的种子处理 :内部用
SeedSequence把任意大小的整数种子"揉"成适合算法的初始状态,还支持并行派生子流(SeedSequence.spawn),这对大规模并行模拟非常关键。 - 更快、统计更优:默认 PCG64 比旧 MT19937 更快,正态采样用 Ziggurat 方法可快 2--10 倍。
打印 rng 你会看到它的真实身份:
python
>>> rng = np.random.default_rng(12345)
>>> print(rng)
Generator(PCG64)
五、最本质的区别:全局状态 vs 独立实例
把 np.random 和 default_rng 放在一起跑,最能看清差异:
python
import numpy as np
np.random.seed(123) # 设置旧版全局状态
rng = np.random.default_rng(456) # 新建一个独立 Generator
old = np.random.random(3) # 来自全局状态,不受 rng 影响
new = rng.random(3) # 来自 rng 自己的状态
这两套系统完全独立 :你给旧 API 设的 seed,管不到新的 rng;反之亦然。这就是为什么混用两者会出 bug------你以为设了种子就稳了,结果一部分随机量来自全局、一部分来自实例,复现性直接破功。
更深一层看,这是一次**"从全局可变状态,到可组合、可隔离对象"的设计范式转变**,和 PyTorch 的 torch.Generator、TensorFlow 的随机种子思路一脉相承。新范式的工程价值很实在:
- 把
rng当参数在函数间显式传递,谁用了随机、用了哪条流,一目了然; - 不同子模块、不同线程可以持有各自的
rng,互不干扰; - 复现实验时,只需固定并保存那个
rng(或其种子)即可。
六、性能与算法的实打实差异
为什么 NumPy 费力气推新接口?因为新算法确实更好:
- PCG64 vs MT19937:PCG64 的统计性质更好(通过更多、更严格的随机性测试),且生成随机比特的速度更快。
- Ziggurat 方法 :
Generator的正态、指数等采样用 Ziggurat,比旧实现快数倍,大样本下差距明显。 - 向量化红利 :
rng.random((10000, 10000))一次产出整块数组,底层是 C 实现;而random模块要循环,慢几个数量级。
一句话:批量、向量化、对性能敏感的模拟与训练,必须用 default_rng。
七、从旧到新:迁移清单
如果你手上有旧代码,下面是常用方法的对照:
| 旧(np.random) | 新(Generator) |
|---|---|
np.random.seed(s) |
rng = np.random.default_rng(s) |
np.random.rand(*shape) |
rng.random(shape) |
np.random.randn(*shape) |
rng.standard_normal(shape) |
np.random.random() |
rng.random() |
np.random.randint(low, high) |
rng.integers(low, high)(默认 endpoint=False) |
np.random.random_integers(low, high) |
rng.integers(low, high, endpoint=True) |
np.random.normal(loc, scale, size) |
rng.normal(loc, scale, size) |
np.random.choice(a) |
rng.choice(a)(支持 axis 参数按轴采样) |
np.random.shuffle(x) |
rng.shuffle(x) |
np.random.permutation(x) |
rng.permutation(x) / rng.permuted(x) |
注意两个易错点:
np.random.rand/np.random.randn在Generator上没有对应同名方法,要改用rng.random()/rng.standard_normal()。rng.integers(low, high)默认不含 上界high(和randint一致);想要"含上界",加endpoint=True。
八、最佳实践小结
- 一律用
rng = np.random.default_rng(seed)起手 ,然后把rng显式传给需要随机的函数。 - 新代码别再用
np.random.seed()和np.random.*模块级函数。 - 需要可复现时,固定整数种子(如 42),并把种子和 NumPy 版本一起记到实验记录里------注意:同种子只保证"随机流一致",跨 NumPy 版本、不同 BLAS/CPU 的浮点结果可能有微小差异。
- 追求更强独立性,可用
secrets.randbits(128)生成大种子;需要并行随机流,用SeedSequence.spawn。 - 随机 ≠ 加密安全 :这些伪随机数都不可用于密码学场景,那种需求请用标准库的
secrets模块。
总结
random 是 Python 自带的轻量随机器,np.random 是 NumPy 已被标记为不推荐的全局旧接口,而 np.random.default_rng() 是当前唯一推荐的现代方式------它返回一个状态独立、算法更快更优、可组合可隔离的 Generator 对象。把随机数当"对象"而不是"全局开关"来管理,是写出可复现、可维护科学计算代码的第一步。 今天起,新代码就写 rng = np.random.default_rng(42) 吧。