feat(core): 实现 get_rng 与 set_seed

L0 第 2 项落地。

- get_rng(int) 固定种子可复现;get_rng(Generator) 原样透传;
  get_rng() 跟随 set_seed 设过的全局种子,没设过则交给系统熵源
- set_seed(seed) 除记录 _GLOBAL_SEED 外,还调用 np.random.seed,
  让 sklearn / statsmodels 这些走 numpy 旧全局状态的库也能复现;
  这是全项目唯一碰旧接口的位置,标注了 noqa: NPY002 与理由
- 已知取舍写进 docstring:同一全局种子下多次 get_rng() 是同一条流
- tests/test_core.py:TestRandom 去掉 skip;把原来那条自证式的
  test_set_seed_makes_global_reproducible 换成两条真断言
  (不传 random_state 也要可复现、旧全局状态被覆盖)
- STATUS.md:L0 进度 2/12,新增决策 23(允许 set_seed 碰旧接口)
  与 24(不做 SeedSequence.spawn 拆分);CHANGELOG.md 同步
This commit is contained in:
guzhujushi committed 2026-10-01 19:19:15 +08:00
1 parent cb96c847dd
commit 658b266614
4 files changed
+62 -20

No files matched your search

+25 -6
View File
@@ -4,6 +4,7 @@
* 所有含随机的公开函数签名都带 `random_state=None`,内部第一行取 rng。
* 禁止使用 `np.random.*` 旧式全局接口,也禁止裸用标准库 `random`。
(唯一例外是 :func:`set_seed` 内部的 `np.random.seed`,见该函数说明。)
* 论文附录必须写明 seed —— 提交要能复现。
"""
@@ -13,33 +14,51 @@ import numpy as np
__all__ = ["get_rng", "set_seed"]
# set_seed 记下来的全局种子;None 表示用户没设过。
_GLOBAL_SEED: int | None = None
def get_rng(random_state: int | None = None) -> np.random.Generator:
def get_rng(random_state: int | np.random.Generator | None = None) -> np.random.Generator:
"""取一个随机数生成器。
Parameters
----------
random_state:
`int` 时返回固定种子的独立生成器(可复现);
`None` 时返回基于全局种子的生成器;
`None` 时跟随 :func:`set_seed` 设过的全局种子,没设过就交给系统熵源;
已经是 :class:`np.random.Generator` 时原样返回(便于函数间串联)。
Notes
-----
同一个全局种子下,每次 `get_rng()` 拿到的都是**同一条**流。这保证了
"脚本重跑一遍结果一样",但也意味着两个都省略 `random_state` 的函数会共用
同一串随机数。想避开,就显式传不同的 `random_state`。
Examples
--------
>>> rng = get_rng(42)
>>> bool(rng.random() == get_rng(42).random())
True
"""
# TODO(L0)
raise NotImplementedError
if isinstance(random_state, np.random.Generator):
return random_state
if random_state is None:
random_state = _GLOBAL_SEED # 没设过就是 None,交给系统熵源
return np.random.default_rng(random_state)
def set_seed(seed: int) -> None:
"""设置全局随机种子。
除了记住种子,还会一并喂给 numpy 的旧全局状态:sklearn / statsmodels
内部仍在用它(`check_random_state(None)` 那条路),只设自己的
`_GLOBAL_SEED` 的话这些库照样不可复现。
Examples
--------
>>> set_seed(42)
"""
# TODO(L0)
raise NotImplementedError
global _GLOBAL_SEED
_GLOBAL_SEED = int(seed)
# 这里是全项目唯一允许碰旧接口的地方:它是"设种子",不是"取随机数"。
np.random.seed(_GLOBAL_SEED) # noqa: NPY002 - 故意的,见本函数 docstring