fix(core): 补齐 get_rng 类型校验,set_seed 覆盖 numpy 旧全局
get_rng 的"共享单例生成器"设计由用户本人提出,本提交只在其上修 bug。 - random_state 判断从 isinstance(x, int) 放宽到 (int, np.integer): np.int64 不是 int 的子类,之前会静默退回全局流、种子被无声忽略 - 非预期类型改成抛 TypeError(含 bool / float / str / list),不再静默降级 - get_rng 签名补上 np.integer 与 Generator,与 docstring 的承诺一致 - set_seed 同时调用 np.random.seed:sklearn / statsmodels 走的是 numpy 旧全局状态,不覆盖它们"提交能重跑"就有洞(带 noqa: NPY002 与理由) - 测试:TestRandom 由 4 条增到 7 条(np.integer、非法类型、共享生成器推进) - 新增 docs/dev/personal-pitfalls.md:个人易错点清单,只追加、不通读, 首批记录本轮 4 个点;docs/README.md 补上 05 与该文件的索引 - STATUS.md:L0 进度说明更新,决策 24 改写(共享单例)、新增决策 25 (src/ 实现默认由用户本人写)
This commit is contained in:
1 parent
658b266614
commit
c93810ffb1
6 files changed
+158
-35
No files matched your search
@@ -4,7 +4,7 @@
|
||||
|
||||
* 所有含随机的公开函数签名都带 `random_state=None`,内部第一行取 rng。
|
||||
* 禁止使用 `np.random.*` 旧式全局接口,也禁止裸用标准库 `random`。
|
||||
(唯一例外是 :func:`set_seed` 内部的 `np.random.seed`,见该函数说明。)
|
||||
(唯一例外是 :func:`set_seed` 里的 `np.random.seed`,见该函数说明。)
|
||||
* 论文附录必须写明 seed —— 提交要能复现。
|
||||
"""
|
||||
|
||||
@@ -14,25 +14,32 @@ import numpy as np
|
||||
|
||||
__all__ = ["get_rng", "set_seed"]
|
||||
|
||||
# set_seed 记下来的全局种子;None 表示用户没设过。
|
||||
_GLOBAL_SEED: int | None = None
|
||||
# 模块级共享生成器:set_seed 换掉它,无参 get_rng 复用它。
|
||||
_GLOBAL_RNG: np.random.Generator | None = None
|
||||
|
||||
|
||||
def get_rng(random_state: int | np.random.Generator | None = None) -> np.random.Generator:
|
||||
def get_rng(
|
||||
random_state: int | np.integer | np.random.Generator | None = None,
|
||||
) -> np.random.Generator:
|
||||
"""取一个随机数生成器。
|
||||
|
||||
Parameters
|
||||
----------
|
||||
random_state:
|
||||
`int` 时返回固定种子的独立生成器(可复现);
|
||||
`None` 时跟随 :func:`set_seed` 设过的全局种子,没设过就交给系统熵源;
|
||||
`int` / `np.integer` 时返回固定种子的独立生成器(可复现);
|
||||
`None` 时返回共享的全局生成器(由 :func:`set_seed` 设定,**会被推进**);
|
||||
已经是 :class:`np.random.Generator` 时原样返回(便于函数间串联)。
|
||||
|
||||
Raises
|
||||
------
|
||||
TypeError
|
||||
传进来的既不是整数、也不是 Generator、也不是 None。
|
||||
|
||||
Notes
|
||||
-----
|
||||
同一个全局种子下,每次 `get_rng()` 拿到的都是**同一条**流。这保证了
|
||||
"脚本重跑一遍结果一样",但也意味着两个都省略 `random_state` 的函数会共用
|
||||
同一串随机数。想避开,就显式传不同的 `random_state`。
|
||||
无参调用返回的是**同一个**生成器对象,所以连续两次 `get_rng()` 取到的随机数
|
||||
不同(流在推进)。这既保证"重跑一遍结果一样",又避免两个都省略 `random_state`
|
||||
的函数抽到完全相同的数。
|
||||
|
||||
Examples
|
||||
--------
|
||||
@@ -42,23 +49,40 @@ def get_rng(random_state: int | np.random.Generator | None = None) -> np.random.
|
||||
"""
|
||||
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)
|
||||
|
||||
# bool 是 int 的子类,但 True 被当成种子 1 几乎总是笔误,宁可报错也不静默接受。
|
||||
if isinstance(random_state, bool):
|
||||
raise TypeError("random_state 不接受 bool:True 会被当成种子 1,通常是笔误")
|
||||
|
||||
# np.int64 这类 numpy 标量**不是** python int 的子类,必须一起判断;
|
||||
# 只写 int 的话,传进来的 numpy 整数会静默掉进下面的全局分支,种子被无声忽略。
|
||||
if isinstance(random_state, (int, np.integer)):
|
||||
return np.random.default_rng(int(random_state))
|
||||
|
||||
if random_state is not None:
|
||||
raise TypeError(
|
||||
"random_state 只接受 int / np.integer / np.random.Generator / None,"
|
||||
f"收到的是 {type(random_state).__name__}"
|
||||
)
|
||||
|
||||
global _GLOBAL_RNG
|
||||
if _GLOBAL_RNG is None:
|
||||
_GLOBAL_RNG = np.random.default_rng()
|
||||
return _GLOBAL_RNG
|
||||
|
||||
|
||||
def set_seed(seed: int) -> None:
|
||||
def set_seed(seed: int | np.integer) -> None:
|
||||
"""设置全局随机种子。
|
||||
|
||||
除了记住种子,还会一并喂给 numpy 的旧全局状态:sklearn / statsmodels
|
||||
内部仍在用它(`check_random_state(None)` 那条路),只设自己的
|
||||
`_GLOBAL_SEED` 的话这些库照样不可复现。
|
||||
除了换掉本包的共享生成器,还会一并设置 numpy 的**旧全局状态**:
|
||||
sklearn / statsmodels 内部走 `check_random_state(None)` 时用的就是它,
|
||||
只管自己的生成器的话,这些库照样不可复现。
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> set_seed(42)
|
||||
"""
|
||||
global _GLOBAL_SEED
|
||||
_GLOBAL_SEED = int(seed)
|
||||
# 这里是全项目唯一允许碰旧接口的地方:它是"设种子",不是"取随机数"。
|
||||
np.random.seed(_GLOBAL_SEED) # noqa: NPY002 - 故意的,见本函数 docstring
|
||||
global _GLOBAL_RNG
|
||||
_GLOBAL_RNG = np.random.default_rng(seed)
|
||||
# 全项目唯一允许碰旧接口的地方:这里是"设种子",不是"取随机数"。
|
||||
np.random.seed(seed) # noqa: NPY002
|
||||
Reference in new issue
Block a user