别再只写 np.random.seed() 了:default_rng 与 np.random、random 到底差在哪?

每个写过 NumPy 的人,几乎都写过这两行:

python 复制代码
np.random.seed(42)
x = np.random.rand(100)

它简单、好用、看起来"随机又可控"。但如果你最近翻过 NumPy 官方文档,会看到一句明确的建议:新代码请使用 np.random.default_rng(),而不是 np.random 里的那些全局函数。

于是问题来了:同样是"生成随机数",np.randomnp.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)       # 继续消耗同一个全局状态

这种设计有三个绕不开的问题:

  1. 全局状态会"串台"。 你在 A 函数里 np.random.seed(42),B 函数里也悄悄调了 np.random.*,两者会共用、互相消耗同一个随机流。在库代码里,这会让结果变得不可复现、极难调试。
  2. 不是线程安全的。 多个线程同时调用 np.random.*,会争抢同一个全局状态,可能得到错误或损坏的随机数。
  3. 算法偏旧、偏慢。 底层用的是 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.randomdefault_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)

注意两个易错点:

  1. np.random.rand / np.random.randnGenerator 上没有对应同名方法,要改用 rng.random() / rng.standard_normal()
  2. 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) 吧。

相关推荐
Patrick在香港1 小时前
Claude Agent 进阶:4 种工具调用编排模式,让 0820 那 13 个 endpoint 并行起来
python·ai·ai编程
qq_513728042 小时前
StaleElementReferenceException(陈旧元素引用异常)
python
千里码aicood2 小时前
基于Python的电商数据采集系统(大数据+爬虫)
开发语言·python
战略性的菠萝2 小时前
研究生基础-Python快速入门(终)——面向对象
python
陈皮波比茶2 小时前
Python项目实战-网络机器人(爬虫)
开发语言·网络·爬虫·python·机器人
winfredzhang3 小时前
从零构建家庭照片管理系统:Django 模块化单体实战,以及我踩到的 4 个真实坑
python·postgresql·django·sqlite·bootsrap
jyOverQ3 小时前
LangGraph 流式执行详解:stream、astream 与 stream_mode 怎么选?
python·langchain
闲猫3 小时前
LangGraph / Capabilities / Stores
python·agent·langgraph
pjj198543 小时前
机器学习-NumPy2
人工智能·python·机器学习