Python技术迷

如何使用 Python 的 random 模块生成随机数、实现随机乱序和随机抽样?

抽检脚本跑了三次,三次结果一模一样。

第一眼我就不太信“随机数坏了”。Python 的 random 没那么容易坏,更多时候是代码写得太随手:把 seed 固定了、抽样后又改原列表、或者把“乱序”和“抽样”混成一件事。

random 这个模块,平时写脚本够用了。比如生成测试订单号、随机挑几条数据回放、把任务列表打散、从一批用户里抽样做灰度。别拿它做密码、验证码、密钥,这个后面单独说。

先看最常见的随机数。

import random

# 0 到 1 之间的小数,不包含 1
score = random.random()

# 生成 1000 到 9999 的整数,左右都包含
ticket_no = random.randint(1000, 9999)

# 从 0, 5, 10, 15 ... 95 里挑一个
bucket = random.randrange(0, 100, 5)

# 生成一个浮点金额,模拟压测数据
amount = round(random.uniform(8.8, 299.9), 2)

print(score, ticket_no, bucket, amount)

这里有个小坑,randint(1, 10) 是包含 10 的,randrange(1, 10) 不包含 10。

这个差异不大,但写抽奖、分桶、分页随机偏移的时候,边界错一次就挺恶心。尤其是 randrange(len(items)) 这种写法,刚好适合拿来取列表下标。

users = ["u_101", "u_102", "u_103", "u_104"]

idx = random.randrange(len(users))
picked_user = users[idx]

print(picked_user)

如果只是从列表里拿一个,别绕这么远,直接用 choice。

levels = ["debug", "info", "warn", "error"]

log_level = random.choice(levels)
print(log_level)

我平时写一些造数据脚本,比较喜欢把随机逻辑收在一个小函数里。后面要调权重、加字段,不至于满文件都是 random.xxx。

import random
from datetime import datetime

defbuild_mock_event(user_id: str) -> dict:
    actions = ["login", "search", "pay", "logout"]
    areas = ["beijing", "shanghai", "hangzhou", "chengdu"]

return {
"user_id": user_id,
"action": random.choice(actions),
"area": random.choice(areas),
"cost_ms": random.randint(20, 1800),
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    }

for uid in ["u_1001", "u_1002", "u_1003"]:
    print(build_mock_event(uid))

这段不复杂,但够用。真到线上排障,很多时候缺的不是一套漂亮框架,而是能马上造出几条像样数据的脚本。

再看随机乱序。

shuffle 会直接改原列表,这点要记住。它不是返回一个新列表。

import random

task_ids = ["job_01", "job_02", "job_03", "job_04", "job_05"]

random.shuffle(task_ids)

print(task_ids)

如果原列表后面还要用,就别直接 shuffle。这个坑我见过,前面为了“随机执行任务”打乱了一下,后面生成对账文件也用了同一个列表,结果顺序全乱了,还以为是导出逻辑有问题。

稳一点这么写:

origin_tasks = ["job_01", "job_02", "job_03", "job_04", "job_05"]

run_tasks = origin_tasks[:]
random.shuffle(run_tasks)

print("原始顺序:", origin_tasks)
print("执行顺序:", run_tasks)

还有一个更干净的写法,用 sample 抽取全部元素,它会返回新列表,也能达到乱序效果。

origin_tasks = ["job_01", "job_02", "job_03", "job_04", "job_05"]

run_tasks = random.sample(origin_tasks, k=len(origin_tasks))

print(run_tasks)

shuffle 适合你明确就是要改原列表。sample 适合你想要一个新的随机结果。

随机抽样这块,最容易写错的是“是否允许重复”。

sample 是不放回抽样,抽过的不会再出现。

import random

order_ids = [
"od_10001", "od_10002", "od_10003",
"od_10004", "od_10005", "od_10006"
]

check_orders = random.sample(order_ids, k=3)

print(check_orders)

这个场景很常见,比如从当天订单里抽 100 条做人工核对。抽出来的订单不应该重复,那就用 sample。

如果允许重复,用 choices。

colors = ["red", "blue", "green"]

balls = random.choices(colors, k=10)

print(balls)

choices 还有个挺实用的参数:权重。

比如做接口压测,不同接口流量比例不一样。别每个接口平均随机,那样测出来的数据会骗人。

import random

apis = ["/api/login", "/api/search", "/api/order/create", "/api/pay"]
weights = [30, 50, 15, 5]

for _ in range(10):
    api = random.choices(apis, weights=weights, k=1)[0]
    print(api)

这段意思是搜索接口流量更高,支付接口流量最低。虽然只是脚本,但比瞎随机靠谱一点。

还有一个现场经常用到的点:固定随机结果。

比如你排查一个批处理 bug,随机抽样抽到了某几条脏数据。你第二次想复现,就需要 seed。

import random

random.seed(20240618)

user_ids = [f"u_{i}"for i in range(1000, 1020)]
picked = random.sample(user_ids, k=5)

print(picked)

只要 seed 一样,随机序列就一样。

这东西适合测试复现,不适合放在正式随机逻辑里。尤其别在循环里反复 random.seed(),那样随机性会被你自己干掉。

我见过这种写法:

for item in items:
    random.seed(100)
    print(random.randint(1, 10))

看着每次都在随机,其实结果基本被锁死了。seed 一般放在脚本入口,真需要复现时才打开。

最后补一刀安全问题。

random 是伪随机,适合模拟、抽样、乱序,不适合生成密码、短信验证码、重置 token。安全相关的东西,用 secrets。

import secrets

code = "".join(str(secrets.randbelow(10)) for _ in range(6))
token = secrets.token_urlsafe(24)

print(code)
print(token)

所以平时写 Python 脚本,我大概这么分:

生成普通随机数,用 random()、randint()、randrange()。

从列表里挑一个,用 choice()。

打乱列表,用 shuffle(),但注意它会改原列表。

不重复抽样,用 sample()。

允许重复、还要按权重抽,用 choices()。

要复现问题,用 seed()。

涉及安全,别碰 random,直接换 secrets。

这几个函数掌握住,日常写脚本基本够了。别小看这些小工具,很多排障脚本、数据清洗脚本、压测造数脚本,最后拼的就是这些不起眼的细节。