很多人写了几年 Python,对 random 的用法还停在 randint(1, 100)。说实话,这个模块里藏的坑,比你想象的大得多。

先说结论
random 产生的不是真随机,是伪随机。如果你拿它做抽奖、生成验证码、或者搞加密相关的东西,大概率已经在生产环境埋雷了。
什么叫伪随机
random 模块底层跑的是 Mersenne Twister 算法。它拿一个种子值,用确定性的数学公式推算出一串“看起来随机”的数字序列。
关键点来了:只要种子一样,输出永远一样。
import random
random.seed(42)
print(random.randint(1, 100)) # 82
random.seed(42)
print(random.randint(1, 100)) # 还是82
这不是 bug,是设计。做调试、做测试、做科学实验的时候,可复现的随机性反而是刚需。但你要拿它当加密工具用,那就是在给自己挖坑。
日常最常用的几个函数
random.choice() 从序列里随机抽一个,比写 randint 再取下标干净多了。一行代码搞定,不用先算索引再去取,可读性直接上一个档次。
random.shuffle() 原地打乱列表。不少人踩过的坑:这玩意儿是原地操作,改变原有列表,不返回新列表。你以为它在返回打乱后的结果,其实返回的是 None。这个坑在行业里反复出现。
random.sample() 做不放回抽样,公司年会抽奖就用它。指定抽几个人,它帮你搞定,不用自己写去重逻辑。
random.uniform(a, b) 生成区间内的浮点数,造测试数据很实用。
三个容易翻车的地方
第一个坑是全局状态。random 模块内部维护了一个全局 Random 实例,整个进程共享。多线程环境下,几个线程同时调 random.randint(),状态竞争的问题就来了。解决办法是用 random.Random() 自己建实例,各用各的,互不干扰。
第二个坑是安全性。Mersenne Twister 是可预测的。攻击者收集足够多的输出值,理论上能反推出种子。Python 专门提供了 secrets 模块来处理这类需求。验证码、密码重置链接、会话 ID,全部上 secrets,别偷懒用 random。
第三个坑是不传 seed 的行为。不传参数的话,random 用系统时间做种子。听起来随机吧?但在容器化环境里,如果一堆进程同时启动,时间戳可能完全一样,种子也就撞了。这种场景下,你以为每个进程都有独立的随机序列,实际上可能大家都在输出同一串数字。
场景对应方案
日常开发和测试数据,random 随便用。需要可复现的科学实验,用 random.seed 固定种子。密码、token、验证码,切换到 secrets 模块。大批量高性能随机数,上 numpy.random。加密级别的随机数,用 os.urandom()。
写在最后
random 模块本身没问题,问题是用错了地方。把伪随机当加密随机用,是不少线上事故的直接原因。搞清楚每个工具的边界在哪,非常重要!
