写Python这些年,我几乎每天都会跟"python返回随机数"这件事打交道。做数据分析要抽样、写爬虫要随机延时、搞量化要模拟行情、开发小工具要生成验证码——随机数几乎是无处不在的。很多人以为随机数就是import random之后调个random.random(),但实际上,真正用起来之后会发现:需求不同,选型完全不同;选错模块,轻则结果不可复现,重则埋下安全漏洞。这篇文章我想从实际经验出发,把python返回随机数的完整链路讲透,包括random、numpy.random、secrets三个层面的选择,以及环境准备、核心API、常见坑、真实业务场景怎么用。无论你是刚入门的新手,还是写了几年代码的朋友,都可以对照自己的场景找到答案。
1. 先搞清楚需求:python返回随机数之前,你要的是哪种"随机"
1.1 同一个需求,三种完全不同的实现
先讲一个真实例子。上周有个读者给我发消息,说"我用random.randint生成了6位数字验证码,放在线上服务里,结果被薅羊毛了"。我一看代码,他用的是random模块。这恰好就是问题的根源:random模块生成的是伪随机数,默认种子来自系统熵,但如果攻击者能拿到几个连续输出,是有可能预测后续序列的。生成验证码、密码重置token这类对安全性有要求的场景,应该用secrets模块。这不是说random没用,而是它适合的场景是模拟、抽样、测试,而不是对抗性环境。
我自己的习惯是,在动手写代码之前先问自己三个问题:
- 这个随机数是要给人看的,还是要给算法用的?
- 这个随机数结果需要可复现吗?比如测试用例、A/B实验分组。
- 这个随机数会不会被外部猜测?比如验证码、token、密码。
这三个问题决定了你该用random、numpy.random还是secrets。我常用一张表来对比:
| 模块 | 随机类型 | 常见用途 | 安全级别 |
|---|---|---|---|
| random | 伪随机 | 测试、抽样、洗牌、模拟 | 不适合安全场景 |
| numpy.random | 伪随机,向量化 | 批量数据生成、蒙特卡洛、矩阵构造 | 不适合安全场景 |
| secrets | 密码学安全随机 | 验证码、token、密码、签名 | 适合安全场景 |
你可能注意到我特意没有把"真随机"放进来。严格来说,计算机很难拿到物理真随机数,像secrets内部也是基于操作系统提供的熵源,只是它在密码学意义上足够安全,无法被实际预测。很多教程里动不动就说"这是真随机",这是不严谨的,背后其实是把"不可预测"和"无法预测"的概念混在了一起。对于绝大多数工程需求,伪随机完全够用,关键是知道边界在哪里。
1.2 伪随机不是"假随机",理解种子机制
这里稍微解释一下原理。random模块基于梅森旋转算法(Mersenne Twister),它生成的序列看起来随机,但本质上是一个确定性的递推序列。你可以把随机数生成器想象成一台"洗牌机":你给它一个初始状态,它会按照内部规则连续吐牌。这个初始状态就是seed。如果你不手动设置seed,它会从操作系统读取一个当前时刻的熵值,所以每次运行都不一样;如果你手动设置seed,比如random.seed(42),那么之后每次调用得到的序列就完全一致。
用一段极简代码来验证:
import random random.seed(42) print(random.random()) # 0.6394267984578837 random.seed(42) print(random.random()) # 0.6394267984578837同一个种子,同一个位置,输出完全一致。这个特性在工程上有两个相反方向的用法:测试时需要复现,所以固定种子;生产环境中希望每次不同,一般不手动固定种子。我见过不少同事在写单元测试时忘了固定种子,结果测试用例时而过、时而挂,查了半天才发现是随机数在捣乱。到这里,你已经理解了python返回随机数最核心的概念,后面的API和场景都建立在种子机制之上。
2. 环境准备:python安装与random模块的前置知识
2.1 random是标准库,不需要额外安装,但要先确认环境能用
很多新人看到"python返回随机数"就以为需要下载什么扩展包,其实不是。random是Python标准库的一部分,你安装完Python就已经自带。比较常见的坑在于:电脑上装了多个Python版本,或者命令行里运行的python和代码编辑器里用的不是同一个解释器。我建议先验证环境:
import random print(random.__file__) print(random.random())如果能正常打印,说明当前解释器内置random。如果提示ModuleNotFoundError,几乎可以断定是环境问题。检查一下命令行执行的是不是真正的python,Windows下尤其常见:很多人装了Anaconda,又在官网装了Python,两个解释器混着用,pip install装到A环境,代码却在B环境里跑,于是怎么import都报错。
2.2 numpy.random需要单独安装,环境变量要处理好
随机数在很多数据场景里要成批生成,这时候用random模块的for循环效率不高,我一般直接用numpy.random。numpy不是标准库,需要额外安装。分几步走:
- 确认pip可用:
python -m pip --version - 安装:
python -m pip install numpy - 验证:
python -c "import numpy as np; print(np.random.rand(3))"
如果安装慢,或者经常超时,可以临时指定镜像源。我用清华源举例:
python -m pip install numpy -i https://pypi.tuna.tsinghua.edu.cn/simple环境变量这个问题经常被忽略。Windows下如果安装时没勾选“Add Python to PATH”,那你在命令行敲python可能提示“不是内部或外部命令”。解决办法是把Python安装目录和Scripts目录加到PATH里,或者重新用安装包修复一次。Mac和Linux下一般自带Python或通过包管理器安装,但也要留意python3和python的差异,很多老系统默认python还指向Python 2。
提示:每次换电脑或者换项目环境,我建议先跑一遍 import random、numpy、pandas,很多随机数相关的奇奇怪怪报错,其实都输在环境变量和解释器版本上。
3. random模块核心API:从入门到精通的八个函数
3.1 最常用的五个函数:random、uniform、randint、randrange、choice
random模块的API说多不多,但真要熟练使用,也就那么几个。我按使用频率排序,给你一个一个拆开讲。
import random # 返回 [0.0, 1.0) 之间的浮点数 print(random.random()) # 返回 [a, b] 之间的浮点数 print(random.uniform(1, 10)) # 返回 [a, b] 之间的整数,两个端点都包含 print(random.randint(1, 6)) # 返回 [start, stop) 之间的整数,step可以指定 print(random.randrange(0, 10, 2)) # 从序列中随机取一个元素 print(random.choice(["a", "b", "c"]))这里最需要记住的边界规则是:random.randint(a, b)左右两边都包含,random.randrange(a, b)不包含b。很多人在写列表随机索引时习惯用randint(0, len(lst)),然后程序时不时就报IndexError。正确做法是random.randrange(len(lst)),或者random.randint(0, len(lst)-1)。我踩过这个坑不止一次,后面在常见问题里还会再提。
random.uniform的边界在文档里写的是[a, b]或[a, b)都有可能,因为它的实现是a + (b-a) * random(),实际落在哪个范围取决于浮点误差。如果你对边界有严格要求,建议自己写公式,或者直接使用均匀分布明确的范围校验。
3.2 列表重排与抽样:shuffle、sample、choices的权重玩法
除了生成单个随机数,更常用的是对一组数据做操作。比如抽奖、随机出题、数据集划分。这里必须分清三个函数:
import random # 原地球打乱列表 lst = [1, 2, 3, 4, 5] random.shuffle(lst) print(lst) # 从列表中无放回抽取 k 个元素 sample = random.sample(range(100), 5) print(sample) # 有放回抽取,支持权重 result = random.choices( population=["red", "green", "blue"], weights=[10, 5, 1], k=1 ) print(result)random.shuffle是原地操作,不会返回新列表,新手最容易在这里写出lst = random.shuffle(lst)这种错误,结果得到的永远是None。random.sample适合抽奖、留出法验证集划分、随机抽检,它保证元素不重复。random.choices则是有放回的,weights参数可以直接传入权重列表,不用自己手动拼一个加权后的序列,代码会简洁很多。
我在做数据集划分时有一个小习惯:先用random.seed固定种子,再用sample划分训练集和测试集。这样每次跑实验都是同一份划分,问题容易复现,报告里也好交代。
3.3 固定随机种子:让结果可复现
随机种子不只是测试需要,在很多业务场景里也非常重要。比如推荐系统里做在线实验的分桶,如果每次运行都换一种分桶方式,用户可能这次在实验组、下次在对照组,实验结论完全不可信。这时候固定种子是基本操作。
import random random.seed(2024) print(random.sample(range(1000), 10))有几个细节值得注意:第一,seed只需要设置一次,后续所有random模块的函数都共享这个种子;第二,如果代码里多次调用random.seed,每次调用都会重置状态,可能导致你精心设计的随机分布失效;第三,seed的值可以是整数、浮点数或字符串,实际使用中整数最直观。还有一个小坑:如果你在导入模块时给random设置了种子,这个种子会在整个进程生命周期内影响后续所有调用,包括其他库内部依赖random的代码,所以在大型项目里不要盲目在模块顶部设置seed,最好封装成一个带参数的函数。
4. 把随机数用到真实业务里:爬虫、量化、数据Mock
4.1 量化交易策略代码里的随机模拟:蒙特卡洛与随机游走
随机数在量化里最常见的用途是蒙特卡洛模拟。我不建议你直接用随机数去猜涨跌,但可以用随机游走来理解价格波动的基本形态。下面这段代码生成一条随机游走路径,用来演示价格序列的模拟过程:
import random import matplotlib.pyplot as plt def random_walk(steps=1000, start=100): prices = [start] for _ in range(steps): # 每天的涨跌幅在 -2% 到 +2% 之间 change = random.uniform(-0.02, 0.02) prices.append(prices[-1] * (1 + change)) return prices # 生成三条路径,方便对比 random.seed(7) for _ in range(3): plt.plot(random_walk()) plt.show()这段代码本身不是策略,但它可以帮你理解波动率、路径依赖和回测的基本思想。更典型的随机数用法是估计圆周率。用蒙特卡洛方法,往正方形里随机撒点,统计落在圆内的比例,就得到π的近似值:
import random random.seed(1) n = 1000000 inside = 0 for _ in range(n): x, y = random.random(), random.random() if x * x + y * y <= 1.0: inside += 1 print(4 * inside / n)我实际跑下来,100万个点通常能得到3.141左右的结果。这类模拟的关键在于样本量要够,两点之间的距离要足够随机,以及不同随机种子带来的方差要心里有数。做量化策略回测时,我一般会固定种子,保证每次回测结果一致,这样调参才不会被随机噪声干扰。
4.2 爬虫场景里的随机延时与随机UA
爬虫也是python返回随机数的高频使用场景。很多入门教程只会教你怎么发起请求,但真正做采集的人都知道,一个稳定可持续的采集流程,必须要有随机性。最简单的两个控制点:请求之间的延时,以及User-Agent。
import random import time import requests user_agents = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...", ] headers = {"User-Agent": random.choice(user_agents)} # 随机延时 1.5 到 4 秒之间,模拟人工操作节奏 time.sleep(random.uniform(1.5, 4.0)) resp = requests.get("https://example.com/data", headers=headers, timeout=10)为什么要用random.uniform(1.5, 4.0)而不是固定sleep(2)?因为固定间隔本身也是一种规律,容易被对方识别。随机间隔虽然做不到完全人类化,但至少能把规律性抹掉大半。除了UA和延时,还可以在请求参数里加入少量随机噪声、随机选择代理入口、随机打乱爬取队列顺序。这些做法的共同目标是减少模式特征,而不是钻空子。对于被采集方来说,这种节奏也更友好,不至于瞬间打爆服务器。
4.3 用numpy.random批量生成模拟数据与邻接矩阵
如果只是生成一两个随机数,random就够了。但数据分析和算法实验里经常需要整批整批的模拟数据,这时候用numpy.random效率高得多,而且API风格和random很接近。构造邻接矩阵就是一个典型例子,图算法的练习数据经常靠它:
import numpy as np np.random.seed(0) n = 5 adj = np.random.randint(0, 2, size=(n, n)) np.fill_diagonal(adj, 0) # 自己到自己的边置为0 print(adj)这里先用randint生成0或1的矩阵,再把对角线上的1改成0,就得到一张无自环的有向图邻接矩阵。如果想做无向图,可以用np.tril或np.triu把矩阵强制做成三角阵,再对称到另外一半。numpy.random还支持设置随机数生成器实例,比如rng = np.random.default_rng(42),这样可以避免全局状态污染,多个模块之间各用各的生成器,推荐指数比全局np.random.seed高不少。
4.4 协程任务中加入随机调度
Python协程在异步编程里用得非常多,随机数的介入往往是为了模拟真实世界的乱序。比如你有10个异步任务,想让它们以随机的顺序和随机的时间间隔完成,就可以在asyncio.sleep参数里套随机数:
import asyncio import random async def worker(i): delay = random.uniform(0.1, 0.8) await asyncio.sleep(delay) print(f"task {i} finished after {delay:.2f}s") async def main(): tasks = [asyncio.create_task(worker(i)) for i in range(5)] await asyncio.gather(*tasks) asyncio.run(main())这段代码每次运行的输出顺序都不一样。类似技巧可以用在异步队列测试、压力模拟、分布式任务调度演示中。实际项目中我还常把随机数用在重试策略里:失败后不是固定等待,而是用指数退避加随机抖动,避免多个客户端在同一时刻一起重试造成雪崩。抖动一般取0到单次退避时间之间,代码写出来就是random.uniform(0, base)。
5. 常见问题与排查实录:random模块最容易被坑的细节
5.1 randint和randrange边界混淆
这是我在答疑里遇到最多的一个问题。看下面的例子:
import random lst = [10, 20, 30] idx = random.randint(0, len(lst)) # 可能取到3 print(lst[idx]) # 有时报错 IndexErrorrandint包含右端点,所以这里可能取到3,而列表最大下标是2。解决办法有两个:一是用random.randrange(len(lst));二是用random.randint(0, len(lst) - 1)。我个人的建议是,凡是索引随机都统一用randrange,从源头杜绝越界。至于randint和randrange的区别,记住一句话:randint是"闭区间",randrange是"左闭右开区间"。
5.2 为什么每次运行结果都一样?种子被全局设置
很多人遇到的现象是:明明代码里没有设置seed,但每次运行结果却一样。这种情况往往出在"间接设置"上。如果你import了某个第三方库,而它内部调用了random.seed,或者你自己在其他模块里设置了种子,全局状态就会被重置。另外,Jupyter Notebook里如果某个单元格执行过random.seed(42),继续往下执行时,后面的随机数全都基于这个种子,看起来就像"卡在一个固定序列"里了。
排查思路很简单:全项目搜索random.seed和np.random.seed,看有没有人设置过。再就是确认当前运行的脚本是不是重新启动的Python进程。如果你在Notebook里跑,需要重启内核才能真正清空状态。
5.3 随机数重复与唯一性冲突
随机数带上"随机"两个字,容易让人误以为它不会重复。实际上random.randint和random.choice是可能重复的,尤其是从一个小范围里抽样。比如用random.randint(100000, 999999)生成6位验证码,在并发场景下重复概率并不低。
解决方向有两类:如果要求唯一,就用random.sample对全量范围做无放回抽样,或者用UUID、自增序列加随机数组合;如果要求安全,直接换成secrets模块,并加入生成记录和过期时间。这里还有一个常见组合:把随机数当主键。我遇到过有人用random.random()做数据库主键,结果两台机器各自生成,碰撞之后数据被覆盖。主键应该用数据库自增或者UUID,而不是随机浮点数。
5.4 numpy安装失败的排查思路
在随机数相关的报错里,"ModuleNotFoundError: No module named 'numpy'"出现频率极高。这类问题绝大多数不是网络问题,而是环境错乱。排查顺序我建议这样:
- 确认当前python解释器路径:
which python或where python - 确认pip和python配对:
python -m pip --version - 用python -m pip install numpy,而不是裸pip install
裸pip install在系统存在多环境时,经常装到别的Python版本里去。这一步虽然基础,但能解决90%以上的安装失败。如果确实网络慢,再用镜像源,做法我前面已经写过。还有一个小技巧:安装后不要只验证import numpy,要验证numpy.random,因为有些精简环境或依赖冲突会导致numpy能import,但子模块缺损。
6. 更深一层:什么时候必须用secrets而不是random
6.1 random能预测,安全场景不能赌
如果你写的是验证码、密码重置链接、临时token、API Key这类东西,请不要用random模块。原因很直接:random的算法是公开的,只要攻击者拿到足够多的连续输出,理论上可以反推出当前状态,进而预测后续输出。这属于可预测的伪随机,安全场景下是不合格的。Python官方也为这个场景准备了secrets模块,它从操作系统提供的熵源中获取随机数据,设计目标就是不可预测。
一个简单的对照:
import random import secrets # 不推荐:验证码用random生成 code = random.randint(100000, 999999) # 推荐:用secrets生成6位数字验证码 code = secrets.randbelow(900000) + 100000 # 或者直接用token_hex生成随机token token = secrets.token_hex(16)secrets.randbelow(n)返回0到n-1的整数,并且分布是均匀的,配合+100000就能拼出6位数字。它的性能比random稍慢,但在安全场景里那点性能开销完全值得。这个模块我不建议过度封装,保持官方用法最简单。
6.2 用secrets做随机抽样与安全码生成
除了randbelow,secrets还有一个choice函数,用法和random.choice一样:
import secrets chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" password = "".join(secrets.choice(chars) for _ in range(12)) print(password)这段代码可以生成一个12位的随机密码。如果你看过Django或Flask的源码,它们处理token时用的也是类似思路,只是字符集和长度会按安全要求调整。再补充一点:secrets.token_urlsafe(n)生成的字符串比纯hex更短但信息量更高,适合放在URL里。遇到需要"随机且安全"的新需求时,我的默认选择是secrets,而不是random。
6.3 跨系统场景:Oracle查询结果随机抽样与数据Mock
随机数在数据工程里也能派上用场。比如有人问过我怎么用python连接oracle查询数据,然后随机抽取一部分去做数据Mock或人工审核。思路不复杂,先把查询结果取回来,再用random做抽样:
import oracledb import random conn = oracledb.connect(user="user", password="pass", dsn="localhost:1521/orcl") cur = conn.cursor() cur.execute("select id, name from emp") rows = cur.fetchall() random.seed(2024) sample = random.sample(rows, min(5, len(rows))) for row in sample: print(row)这里用random.sample而不是先取10000行再自己截断,是因为sample能保证无放回且结果均匀。Oracle侧如果想要更高效的随机抽样,可以直接在SQL里用ORDER BY DBMS_RANDOM.VALUE,但取回Python端后配合random再加工,灵活性更高,比如按业务规则加权抽样、按字段值分层,这是纯SQL不太好实现的。做这类操作时我习惯把抽样种子放进配置参数,这样不同团队跑同一份数据能得到一致结果,问题也更容易对齐。
结尾就不用总结了,最后再分享一条我的真实操作习惯:写随机数相关代码时,我会在函数参数里显式定义random_seed,哪怕是0也没关系,并且在日志里把种子打出来。这样一旦结果异常,可以直接用同一个种子重放现场。这个习惯帮我省掉了无数定位问题的时间,建议你也试试。