Python 的 random,用不好真会把测试数据搞歪
脚本里要随机挑 20 个用户做回访,结果每次跑出来都像“假随机”。
我第一眼一般不怀疑 Python,先看代码。十有八九是把 random.choice()、random.sample()、random.shuffle() 混着用,甚至还拿 random 生成登录验证码。这个地方得先掰正:random 适合测试、抽样、打乱顺序、模拟数据,不适合密码、Token、验证码这种安全场景。
先看最常用的随机数。
import random
# 生成 1 到 100 之间的整数,包含 1 和 100
order_no_tail = random.randint(1, 100)
# 从 0、5、10、15、20 里随机拿一个
retry_gap = random.randrange(0, 21, 5)
# 生成浮点数,适合做折扣、比例、模拟耗时
mock_cost = random.uniform(0.3, 2.8)
print(order_no_tail, retry_gap, round(mock_cost, 2))
这里有个细节,randint(1, 100) 两头都包含,randrange(0, 21, 5) 更像 range(),右边不包含。这个坑不大,但线上脚本里经常见。比如你想随机 0 到 20,写成 randrange(0, 20),那 20 永远出不来。
我平时写批量脚本,更多用它做“抽几条看看”。
比如一批用户 ID,随机抽 5 个做接口回归:
import random
user_ids = [
"u_1001", "u_1002", "u_1003", "u_1004",
"u_1005", "u_1006", "u_1007", "u_1008"
]
picked = random.sample(user_ids, 5)
for uid in picked:
print("check user:", uid)
sample() 有个好处:不重复。
这点很重要。你要抽 5 个用户,就真的是 5 个不同用户。不要用循环套 choice(),那玩意儿可能重复。
这种代码我见过:
import random
bad_picked = []
for _ in range(5):
bad_picked.append(random.choice(user_ids))
print(bad_picked)
看着没毛病,实际可能抽出:
['u_1003', 'u_1003', 'u_1008', 'u_1001', 'u_1003']
如果只是模拟点击,那还行。如果是抽样验数据,这结果就有点糊弄自己了。
choice() 更适合“从几个策略里挑一个”。
import random
backup_nodes = ["node-a", "node-b", "node-c"]
target_node = random.choice(backup_nodes)
print("send request to:", target_node)
这类用法很干净。不要让它承担抽样的活。
再看乱序。
随机乱序我一般用在两类地方:一是批量任务别老按同一个顺序打到同一批资源上;二是测试时把数据顺序打散,看看代码有没有偷偷依赖顺序。
import random
jobs = [
{"job_id": "j_01", "type": "sync_order"},
{"job_id": "j_02", "type": "sync_user"},
{"job_id": "j_03", "type": "fix_coupon"},
{"job_id": "j_04", "type": "check_refund"},
]
random.shuffle(jobs)
for job in jobs:
print(job["job_id"], job["type"])
注意,shuffle() 是原地打乱。
也就是说,原来的 jobs 列表被改了。如果后面代码还要用原始顺序,别直接 shuffle。
我一般会这么写:
import random
raw_jobs = ["sync_order", "sync_user", "fix_coupon", "check_refund"]
run_jobs = raw_jobs[:]
random.shuffle(run_jobs)
print("origin:", raw_jobs)
print("run:", run_jobs)
这个小切片救过不少低级 bug。尤其是脚本越写越长,前面把列表打乱了,后面还以为它是原来的顺序,查起来很烦。
还有一种场景,带权重随机。
比如灰度策略里,90% 走老逻辑,10% 走新逻辑。别自己用一堆 if random.randint() 硬凑,Python 已经给了 choices()。
import random
routes = ["old_parser", "new_parser"]
for _ in range(10):
route = random.choices(routes, weights=[90, 10], k=1)[0]
print("use:", route)
这里 choices() 和 choice() 不是一回事。
choices() 可以重复,可以带权重,返回的是列表。哪怕 k=1,它返回的也是列表,所以后面要 [0]。这个地方我第一次也嫌它啰嗦,但习惯了还好。
如果想让随机结果可复现,就用 seed()。
排查测试失败时很有用。随机数据跑崩了,第二次跑又不崩,这种问题最烦。加个种子,至少能把现场固定住。
import random
random.seed(202406)
prices = []
for _ in range(5):
price = round(random.uniform(10, 99), 2)
prices.append(price)
print(prices)
同一个种子,同一段代码,生成结果会保持一致。
但别在正式逻辑里乱加固定 seed。否则你以为自己在随机,其实每次都走同一套结果。测试脚本里可以,业务代码里要小心。
最后补一个我常用的小脚本,随机抽日志行,不用把整个文件都塞进内存。大文件这么干舒服一点。
import random
defpick_lines(log_path, limit=20):
box = []
with open(log_path, "r", encoding="utf-8") as f:
for line_no, line in enumerate(f, 1):
line = line.rstrip()
ifnot line:
continue
if len(box) < limit:
box.append(line)
continue
hit = random.randint(1, line_no)
if hit <= limit:
box[hit - 1] = line
return box
for item in pick_lines("app.log", 10):
print(item)
这段不是为了炫技,就是一个现场脚本的写法。
日志很大时,readlines() 一把梭容易把内存顶上去。随机抽几行看错误分布,用这种 reservoir sampling 的思路更稳。
random 这模块不复杂,真正容易出问题的是选错函数。
要唯一抽样,用 sample()。
要抽一个,用 choice()。
要打乱原列表,用 shuffle()。
要带权重,用 choices()。
要复现问题,用 seed()。
要生成安全验证码、密码、Token,别用它,换 secrets。这个边界记住,基本就不会写出太离谱的随机代码。