Python随机数生成方法详解:从random到secrets再到numpy

📅 发布时间:2026/10/11 13:51:06
Python随机数生成方法详解:从random到secrets再到numpy
做后端和数据处理的朋友应该没少和随机数打交道。不管是给用户发一个随机的验证码还是在算法里做采样、在量化回测里模拟价格走势Python里最绕不开的就是生成随机数这个基础动作。很多人一开始就是一句random.random()用到底真到需要复现结果、需要安全随机、需要批量生成的时候才发现里面有挺多门道。这篇文章就以“Python生成随机数的方法”为主线把最常用的random模块、面向安全的secrets模块、面向批量计算的numpy随机接口都过一遍顺带聊几个我实际踩过的坑。适合刚入门Python的新手也适合写了几年代码但没仔细研究过随机数细节的工程师。1. Python随机数的底层原理先搞清楚再动手1.1 计算机里的随机数其实是“伪随机”先说一个很多人容易忽略的事实计算机没有办法凭空产生真正的随机数除非你接入了专门的硬件比如读取CPU指令周期、系统时钟漂移、热噪声这类物理信号。日常开发里我们用到的random模块生成的都是伪随机数。什么叫伪随机简单说就是通过一个固定的数学公式从一个初始值开始不断迭代生成一个看起来毫无规律、但实际完全确定的数列。我习惯把这个机制类比成一本很厚的“随机数手册”手册里每个数字都是预排好的表面上看不出规律但只要你从同一页开始往后读读出来的序列永远是一样的。那个“从第几页开始读”的页码就是种子seed。Python的random模块默认采用的是梅森旋转算法Mersenne TwisterMT19937它有非常长的周期2的19937次方减1统计特性也相当好能满足绝大多数非安全场景的需求。另一个经典的算法是线性同余法LCG公式就一行Xₙ₊₁ (aXₙ c) mod mC语言早期的rand()基本就是这类实现。LCG的周期短、低位随机性差所以现代语言基本都不拿它当主力了。这里真正的重点是伪随机序列是可预测的。只要你知道算法和当前状态就能推算出下一个“随机数”。这对日常业务没影响但一旦涉及Token、密钥、验证码这类安全敏感场景就千万不能再用random模块后面第3章我会专门说安全随机数的做法。拿“Python生成随机数的方法”来说第一步不是记API而是先判断你这个随机数用来干什么。1.2 种子seed为什么是复现的关键种子这个概念太重要了但很多教程一句话就带过了。我详细说一下。random模块没有指定种子时它会用系统时间、进程ID、系统熵等作为初始化依据所以每次运行程序的随机序列都不同。这在大多数场景下是好事但在下面这两种情况里就是个麻烦写单元测试时测试结果不能每次都不一样否则没法断言。做实验或机器学习模型时划分训练集和测试集如果每次随机模型结果的对比就没有意义因为数据分布变了。解决办法就是显式设置种子。看代码import random random.seed(42) print([random.randint(1, 100) for _ in range(5)]) # 输出固定无论运行多少遍都是 [81, 14, 3, 94, 35] random.seed(42) print([random.randint(1, 100) for _ in range(5)]) # 再次用同一个种子又得到完全相同的列表只要种子相同序列就完全一致。理解了这一点再去看任何带random功能的库比如sklearn里的train_test_split你会发现它也有个random_state参数本质上就是往里传一个种子。这也是为什么很多开源项目的代码里能看到固定的random.seed(2024)这种写法。不过要提醒一点设置了种子不等于“锁死随机”。它只是让你在需要复现的时候能复现如果你的业务逻辑天然要求每次结果不同就别画蛇添足。我见过有同事为了让抽奖结果“稳定可控”在产品代码里写了seed结果每个用户抽到的奖品顺序都一样这种事故就是没分清“测试用种子”和“业务用随机”造成的。2. random模块的实用方法按场景挑着用2.1 常用API速查与选择逻辑random模块是Python标准库自带的不用额外安装任何包这也是它成为最常见的“Python生成随机数”方式的原因。我把日常最常用的几个方法整理成一个速查表方法返回值场景示例random.random()[0.0, 1.0) 的浮点数概率判断、归一化采样random.uniform(a, b)[a, b] 的浮点数生成一段范围内的连续值random.randint(a, b)[a, b] 的整数闭区间抽号码、随机索引random.randrange(start, stop, step)区间内整数步长偏移生成等差随机索引random.choice(seq)序列中的单个元素从列表里随机挑一个random.choices(population, weights, k)k个元素列表可重复带权重的抽奖random.sample(population, k)k个不重复元素抽样、划分数据random.shuffle(list)原地打乱列表洗牌、打乱顺序random.getrandbits(k)k位随机整数生成大范围随机数这里有几个特别容易搞错的点我一个个说。第一randint和randrange的边界不一样。random.randint(1, 10)返回1到10之间的整数包括了10这是闭区间但random.randrange(1, 10)返回的是1到9包前不包后这是半开区间。很多新手在这上面吃了亏写了个抽奖逻辑以为randrange是闭的结果抽到10的概率永远为0还查了半天。第二random.uniform(a, b)在实际返回时可能因为浮点误差返回b本身虽然概率极低但如果你拿这个值去和边界做严格比较比如if value 1.0这种判断就要小心了。安全和严谨的做法是只要求范围近似即可。第三random.sample和random.choices的区别。sample是“不放回抽样”抽出来的元素不会重复而且要求population的长度大于等于kchoices是“有放回抽样”允许重复还可以通过weights参数指定每个元素被抽中的权重。理解这个区别用起来就不会混淆。2.2 实战写一个带权重的抽奖程序抽奖应该是随机数最经典的应用场景了。很多人第一反应是写一堆if判断比如生成一个0到1的随机数落在哪个区间就发哪个奖。这样没问题但代码很难看。用random.choices可以一行搞定import random prizes [一等奖, 二等奖, 三等奖, 谢谢参与] # 权重分别为1%、9%、30%、60% weights [1, 9, 30, 60] # 抽1次 result random.choices(prizes, weightsweights, k1)[0] print(result) # 模拟抽10万次看分布是否合理 from collections import Counter results random.choices(prizes, weightsweights, k100000) print(Counter(results))我实测跑了一次10万次的结果大致是一等奖约1000次二等奖约9000次三等奖约30000次谢谢参与约60000次和设定的权重比例完全对得上。这就叫“用数据验证逻辑”写随机逻辑时特别推荐这种做法——不是写完就完了而是抽样跑个大批量看分布是否符合预期能提前发现权重配错的低级错误。还有一个细节如果只是从列表里随机挑一个用choice而不是自己算索引。我看到过有人这么写lst[random.randint(0, len(lst) - 1)]功能没错但比直接random.choice(lst)更啰嗦而且容易漏掉边界。代码是给人读的能一句话说明白的事不要绕。2.3 一个容易被忽略的细节shuffle是原地操作随机打乱列表用random.shuffle。但要注意它直接修改原列表返回的是None。很多人刚上手时这么写import random cards [1, 2, 3, 4, 5] new_cards random.shuffle(cards) print(new_cards) # None print(cards) # 已经被打乱了想保留原顺序就要先拷贝一份再shuffle。与之对应的是sample如果你要的是“从列表里随机取一部分且顺序随机”可以直接用random.sample(lst, len(lst))它返回的是新列表不会动原数据。这种小细节遇到一次就会记住但能提前知道就少踩一次坑。3. 安全随机数与批量随机数别只会random模块3.1 secrets模块密码学级随机数的正确打开方式如果你生成的是验证码、口令重置Token、API Key这一类东西千万不要用random模块。原因前面提过random是伪随机、可预测的。攻击者只要拿到几个连续的输出配合已知算法就能推算出内部状态从而预测后续的随机数。这是真实存在的攻击路径不是杞人忧天。Python标准库针对安全场景提供了secrets模块。它的底层是操作系统提供的安全随机源在Linux上主要读/dev/urandom在Windows上使用CryptGenRandom输出的随机性来自系统级别的熵池无法从结果反推算法状态。用法也很直白import secrets # 生成16字节随机字节串转成16进制字符串常用于Token token secrets.token_hex(16) print(token) # 例如f6a4c2e1d9b84e2a9f3d0e5b7a6c1d8e # 从列表中安全地随机选一个抽奖用这个也行但性能略低 winner secrets.choice([A, B, C]) # 生成0到99之间的安全随机整数 num secrets.randbelow(100)在实际业务里我见过太多人用random.choice(string.ascii_letters string.digits)生成重置密码的验证码这在安全审计里是过不去的。正确的做法是换一行secrets.choice。代码改动量几乎为零安全等级完全不同。一句话总结凡是和“别人可能猜到”挂钩的随机数一律用secrets。3.2 numpy随机数批量生成、多维数组与指定分布如果你在做数据分析、机器学习特征工程或者像量化交易回测这种需要大量随机数的场景random模块的效率和多维度支持就不够用了。这时候主角是numpy.random模块。先说一个现在更推荐的写法。早期教程里常见np.random.seed(0)配np.random.rand()这种老接口但在新版numpy里官方更推荐用default_rng生成一个独立的随机数生成器对象好处是你每次new出来的rng实例有自己的独立状态不会污染全局。import numpy as np rng np.random.default_rng(42) # 传种子保证可复现 # 生成5个[0, 1)之间的浮点数 print(rng.random(5)) # 生成3x4的整数矩阵范围[0, 100) print(rng.integers(0, 100, size(3, 4))) # 生成服从正态分布的1000个样本 samples rng.normal(loc0.0, scale1.0, size1000)numpy一次生成十万条数据都是毫秒级这个量级用random模块逐个生成会慢很多。而且numpy支持很多概率分布比如正态分布normal、均匀分布uniform、泊松分布poisson、指数分布exponential等等只需要指定参数即可。做模拟、画分布直方图的时候非常方便。我用一个量化回测里常见的小例子来说明假设某个策略基于随机价格波动做模拟用numpy生成一条简单的几何布朗运动价格路径就很顺手。import numpy as np rng np.random.default_rng(7) mu, sigma 0.0002, 0.01 n_days 252 returns rng.normal(mu, sigma, n_days) price 100 * np.exp(np.cumsum(returns))这只是一个示意真实回测远远更复杂。这里想表达的是当你需要“一整批随机数配合数学运算”时把随机数生成交给向量化的numpy代码简洁、性能也好这算是“Python生成随机数的方法”里性能要求较高时的标准答案。3.3 蒙特卡洛模拟一个例子串起随机数的威力顺着随机模拟的思路继续往下走蒙特卡洛方法(Monte Carlo method)是用随机数解决确定性问题的一类方法。它的核心思想是与其试图解析推导一个复杂系统的精确结果不如大量随机采样统计近似结果。当采样量足够大时结果会收敛到真实值。最经典的入门例子是估算圆周率。在一个边长为2的正方形内随机撒点统计落在内切圆里的点的比例这个比例乘以4就应该接近π。写出来非常直观import random N 1000000 inside 0 for _ in range(N): x random.uniform(-1, 1) y random.uniform(-1, 1) if x * x y * y 1: inside 1 pi_estimate 4 * inside / N print(pi_estimate) # 大约3.1415左右我跑过一次100万点结果在3.1415附近偏差通常在小数点后第3位以内。加大到1000万点能稳定到3.1415x。这就是随机模拟的典型套路不需要理解复杂的数值积分公式只要写一个点的判别条件让随机数替你“试”出答案。网上还有一个很火的“李白打酒”python题目也能用随机模拟去验证递推结果。题目大意是李白提着酒壶出门遇店加一倍、遇花喝一斗最后壶中酒刚好喝光的顺序有多少种。严格解法是用递推或搜索但如果你只是想快速验证某个答案对不对完全可以随机生成大量的“遇店/遇花”顺序筛选出满足条件的再统计数量。样本足够大时比例的期望乘上总的组合数就会逼近正确答案。这种思路在复杂问题的预研阶段非常有用哪怕最后还是要用精确算法随机模拟也能给你一个快速的数量级认知。4. 常见问题与排查技巧实录4.1 为什么“随机数不随机”seed引发的灵异现象我在技术社区见过不少这类求助帖自己写的抽奖程序每次运行结果一模一样或者同一个用户每次打开页面看到的推荐都一样最后排查下来都是seed惹的祸。一个典型的低级错误是代码里有一个全局的random.seed(0)或者某个第三方库在import的时候偷偷设置了全局种子。如果你自己不确定可以在怀疑的位置前打印几个随机数看看是否固定import random # 在关键逻辑前临时加一行观察每次运行是否一致 print(before:, [random.random() for _ in range(3)])如果每次运行打印出的都一样那就是有谁在初始化阶段设置了全局seed。排查顺序一般是先全局搜索seed关键字再看有没有调用numpy的np.random.seed有些库会把numpy和random的全局状态都动了。4.2 多线程并发环境下的随机数性能问题random模块的实例不是线程安全的。Python在执行随机数生成时内部有锁来保护共享状态所以在多线程环境下它会强制串行化导致你以为很轻量的random.random()调用在高并发下成为瓶颈。我做过一个粗略测试8个线程同时疯狂调用random.randint整体吞吐量并不比单线程好多少甚至因为GIL和锁的竞争还有损耗。解决办法有两个方向。一是每个线程自己创建一个random.Random实例互不共享import random import threading def worker(seed): local_rng random.Random(seed) for _ in range(1000): local_rng.random() threads [threading.Thread(targetworker, args(i,)) for i in range(8)]另一个方向是换numpy的default_rng每个线程单独传入种子生成批量随机数效率比逐条调用random高一个量级。这里顺带说一句并行计算的复现性是个大问题如果每个worker用相同的种子会产生完全相同的随机序列用不同种子结果又不可复现。实际工程里通常的做法是每个worker从同一个主种子派生自己的子种子比如主seed加线程编号这样整体虽是确定的但各worker之间数据不重复。4.3 浮点数边界与“奇数字节后跟着随机数”的流言先说浮点边界。random.random()返回[0.0, 1.0)注意是左闭右开。如果你需要包含1.0的区间要么自己用uniform(0, 1)并接受理论上有极小概率返回1.0要么生成整数再除以一个除数。反正在做概率判断时边界问题要特别留意别写出大于等于这种和文档语义冲突的判断。再说一个网上高频搜索的问题“为什么socket接收到奇数字节后面会补一个随机数”。这类问题看起来像随机数造成的其实根本和random模块无关。常见原因有两个一个是struct模块打包时默认对齐填充。比如你用struct.pack(I, data)打包一个整数如果数据格式或对方协议要求某些字段按4字节对齐不足时就会填充字节。这些填充字节如果来自未初始化的内存或旧缓冲表现就像“一堆奇怪的数据”肉眼看起来和随机数无异。另一个原因是recv和send的长度没对好。recv(1024)表示最多读1024字节但对方可能只发了200字节缓冲区剩余部分是旧数据或零值读出来之后你怎么解析都会看到“尾部多了垃圾”。这个问题在网络编程初学者的提问里出现频率极高很多人第一反应是“是不是随机数掺进来了”真相往往是协议设计时没定义消息边界或者没按实际接收到的字节数切片。import socket import struct # 错误示范直接按固定大小解析接收缓冲区 # data conn.recv(1024) # value struct.unpack(I, data) # 可能因为数据长度不对而报错 # 更稳妥先收4字节头部再根据头部长度收主体 header conn.recv(4) msg_len struct.unpack(!I, header)[0] body b while len(body) msg_len: chunk conn.recv(msg_len - len(body)) if not chunk: break body chunk这个问题之所以会被人和随机数联系起来就是因为在调试时输出原始的bytes看到一堆无规律的补位字节。想明白背后的缓冲区机制你就知道应该去检查协议和struct格式而不是去改随机数生成逻辑。4.4 画图时横坐标太密集也能跟随机数相关还有一个热搜词是“python画图横坐标太密集”。这个问题往往出现在生成大量随机数据直接画散点图或折线图时x轴刻度全堆在一起。最常见的解法是旋转刻度或设置步长import matplotlib.pyplot as plt plt.xticks(rotation45) # 或者手动指定稀疏的刻度位置 plt.xticks(range(0, 1000, 100))这其实反映了一个通用思路随机生成的测试数据量大了以后图表呈现和数据处理都会暴露新的问题需要你同步调整展示方式和抽样粒度。随机数不只是生成出来就完事后续的使用、抽样、可视化都需要配套处理。5. 随机数选型建议与实战模板5.1 什么时候用哪个一张表说清楚讲了这么多最关键的问题其实是选型。我根据自己的实战经验整理了一个简单判断逻辑场景推荐方案为什么要这么选普通业务逻辑、抽奖、随机索引random模块标准库自带简单稳定满足需求需要固定种子保证可复现random.seed / Random(seed) / np.random.default_rng(seed)复现实验的唯一路径验证码、Token、临时口令secrets密码学安全无法预测海量随机数、多维数组、指定分布numpy.random.default_rng向量化性能高接口丰富多线程并发大量随机调用每线程独立Random实例或numpy rng避免共享实例的锁竞争蒙特卡洛模拟numpy 自定义判别逻辑批量采样效率高数值收敛快这段选型逻辑可以用一句话概括先挑“用途”再挑“工具”。安全性要求高的用secrets量大的用numpy只是偶尔用一次random够用就别折腾。5.2 三个开箱即用的随机数场景模板为了让你看完能直接抄作业我给出三个高频场景的模板。生成6位数字验证码import random code f{random.randint(0, 999999):06d} print(code)这里用randint加字符串格式化好处是能保证6位不足补0。如果是对安全性要求高的场景把random换成secrets即可。随机划分数据集import random data list(range(100)) # 模拟100条样本 random.shuffle(data) train data[:80] test data[80:]注意shuffle是原地操作需要先拷贝原始数据再打乱否则原始顺序就没了。机器学习场景更推荐sklearn的train_test_split它内部帮你处理好了分层和复现逻辑。生成随机测试数据时顺便踩了“横坐标太密集”的坑import random import matplotlib.pyplot as plt x [i for i in range(1000)] y [random.uniform(0, 1) for _ in range(1000)] plt.plot(x, y) plt.xticks(range(0, 1000, 200)) # 设置刻度步长 plt.show()这三个模板看着简单但覆盖了很多初学者最容易卡住的地方。代码能跑只是第一步明白每一步为什么这么写才是真正的收获。最后再分享一个我自己的习惯每次写完随机数相关逻辑我都会跑一次大批量采样看一眼输出分布是否符合直觉。这个方法很简单但帮我发现过好多次权重配错、边界算错的问题。随机数看起来是“随便”的恰恰越是随便的东西越需要你用数据去验证它的确定性。