24.Python random 模块完全指南

Python random 模块完全指南

本文档面向零基础新手,目标是让你真正理解:

  • random 模块是什么,能做什么
  • 生成随机整数、浮点数的各种方法
  • 对列表进行随机抽取、打乱顺序
  • 随机种子(seed)是什么,为什么要用它
  • 各种概率分布(正态分布、指数分布等)
  • 如何用随机数解决实际问题(抽奖、密码、模拟、游戏)
  • 安全随机数 vs 普通随机数的区别

配有大量可运行示例,全部从最基础讲起。


第一部分:random 模块是什么?

1.1 为什么需要随机数?

生活中随机无处不在:

  • 抽奖时随机抽取获奖者
  • 游戏里随机生成地图、掉落装备
  • 密码学中生成随机密钥
  • 科学模拟中用随机采样来估算结果
  • 机器学习中随机划分训练集和测试集

Python 内置的 random 模块就是为了满足这些需求而生的。

import random   # 只需要这一行就能使用所有功能

# 快速体验
print(random.random())          # 0.0 到 1.0 之间的随机浮点数
print(random.randint(1, 100))   # 1 到 100 之间的随机整数
print(random.choice(["石头", "剪刀", "布"]))  # 随机选一个

1.2 伪随机数:计算机的随机数是”真随机”吗?

重要概念: 计算机产生的随机数其实是”伪随机数(Pseudo-random numbers)“。

真随机:
  来自真实物理现象(如放射性衰变、热噪声)
  完全不可预测,不能重现

伪随机:
  用数学算法产生的"看起来随机"的数字序列
  从同一个"种子"出发,每次产生的序列完全相同
  这正是我们在科学实验和调试中需要的——可重现!
import random

# 设置种子后,每次运行结果完全相同
random.seed(42)
print(random.random())   # 0.6394267984578837(永远是这个)
print(random.random())   # 0.025010755222666936(永远是这个)
print(random.random())   # 0.27502931836911926(永远是这个)

# 不同种子,产生不同序列
random.seed(100)
print(random.random())   # 完全不同的数字

Python random 模块的底层算法是 Mersenne Twister(梅森旋转算法), 周期长达 2^19937-1,对于日常使用完全够用。


第二部分:生成随机数

2.1 random()——最基础:生成 [0.0, 1.0) 的浮点数

import random

# random() 返回 0.0(含)到 1.0(不含)之间的浮点数
for i in range(5):
    print(f"第{i+1}次:{random.random():.6f}")

# 输出示例:
# 第1次:0.374540
# 第2次:0.950714
# 第3次:0.731994
# 第4次:0.598658
# 第5次:0.156019

用 random() 生成任意范围的浮点数:

# 生成 [a, b) 范围内的浮点数,公式:a + (b-a) * random()
a, b = 10, 20
value = a + (b - a) * random.random()
print(f"{a} 到 {b} 之间:{value:.4f}")

# 生成 0 到 100 的百分比
percent = random.random() * 100
print(f"随机百分比:{percent:.2f}%")

2.2 uniform(a, b)——指定范围的浮点数

uniform(a, b)a + (b-a) * random() 的简写版,更直观:

import random

# 生成 5.0 到 10.0 之间的浮点数
price = random.uniform(5.0, 10.0)
print(f"随机价格:¥{price:.2f}")   # 如:¥7.83

# 注意:a 可以大于 b
print(random.uniform(10, 1))    # 也可以,范围是 [1, 10]

# 实际应用:模拟测量误差
true_value = 100.0
measured   = true_value + random.uniform(-0.5, 0.5)
print(f"真实值:{true_value},测量值:{measured:.4f}")

2.3 randint(a, b)——随机整数(含两端)

randint(a, b) 返回 a 到 b(含 a 和 b)之间的随机整数:

import random

# 模拟掷骰子(1到6)
dice = random.randint(1, 6)
print(f"掷出了:{dice} 点")

# 验证两端都会出现
results = [random.randint(1, 6) for _ in range(10000)]
for face in range(1, 7):
    count = results.count(face)
    print(f"  {face}点:{count}次({count/100:.1f}%)")
# 每个面出现概率约 16.7%

容易混淆的地方:

# randint(a, b):包含 b!
print(random.randint(1, 10))   # 可能输出 1 到 10,10 也会出现

# 和 range(1, 10) 不同!range(1, 10) 不包含 10
# 等价于:random.randrange(1, 11)

2.4 randrange(start, stop, step)——更灵活的随机整数

randrangerange() 参数一模一样,但随机返回其中一个值:

import random

# randrange(stop):0 到 stop-1
print(random.randrange(10))        # 0 到 9

# randrange(start, stop):start 到 stop-1(不含 stop)
print(random.randrange(1, 10))     # 1 到 9(不含 10!)

# randrange(start, stop, step):按步长选
# 等价于 random.choice(range(0, 20, 2))
even = random.randrange(0, 20, 2)
print(f"随机偶数(0-18):{even}")   # 0,2,4,...,18 中的一个

odd = random.randrange(1, 20, 2)
print(f"随机奇数(1-19):{odd}")    # 1,3,5,...,19 中的一个

# 实际应用:随机选择特定倍数
multiple_of_5 = random.randrange(5, 101, 5)
print(f"5的倍数(5-100):{multiple_of_5}")

randint vs randrange 对比:

# randint(1, 10):包含 10
# randrange(1, 10):不包含 10,即 1-9
# randrange(1, 11):等价于 randint(1, 10)

# 记忆技巧:randrange 和 range 一样,不含右端点

2.5 getrandbits(k)——生成 k 位随机二进制整数

import random

# 生成指定位数的随机整数
bit4  = random.getrandbits(4)    # 0 到 15(4位二进制)
bit8  = random.getrandbits(8)    # 0 到 255(8位)
bit32 = random.getrandbits(32)   # 0 到 2^32-1

print(f"4位:{bit4}(二进制:{bin(bit4)})")
print(f"8位:{bit8}(二进制:{bin(bit8)})")
print(f"32位:{bit32}")

# 实际应用:生成随机的 ID
token = random.getrandbits(128)
print(f"128位随机ID:{token}")
print(f"十六进制:{hex(token)}")

第三部分:序列相关操作

3.1 choice(seq)——从序列中随机选一个

choice 从非空序列(列表、元组、字符串)中随机选一个元素:

import random

# 从列表选
fruits = ["苹果", "香蕉", "橙子", "葡萄", "西瓜"]
print(f"今天吃:{random.choice(fruits)}")

# 从元组选
directions = ("上", "下", "左", "右")
move = random.choice(directions)
print(f"向{move}走")

# 从字符串选(选一个字符)
chars = "ABCDEFG"
char  = random.choice(chars)
print(f"随机字母:{char}")

# 实用:模拟石头剪刀布
def computer_move():
    return random.choice(["石头", "剪刀", "布"])

for round_ in range(3):
    print(f"第{round_+1}局:电脑出了{computer_move()}")

注意:序列不能为空,否则报错:

try:
    random.choice([])   # IndexError: Cannot choose from an empty sequence
except IndexError as e:
    print(f"错误:{e}")

3.2 choices(population, weights, k)——有权重的随机抽取(可重复)

choices 支持权重,且允许重复抽取(放回抽样):

import random

# 基础用法:从列表中抽 k 个(可重复)
colors = ["红", "绿", "蓝"]
result = random.choices(colors, k=5)
print(f"抽到:{result}")   # ['红', '蓝', '红', '红', '绿']

# 设置权重(红色概率更高)
# weights 和列表一一对应,不需要相加等于1
weighted = random.choices(
    population=["红", "绿", "蓝"],
    weights=[5, 3, 2],     # 红:绿:蓝 = 5:3:2
    k=100
)
print(f"红色出现:{weighted.count('红')}次(期望约50次)")
print(f"绿色出现:{weighted.count('绿')}次(期望约30次)")
print(f"蓝色出现:{weighted.count('蓝')}次(期望约20次)")

# cum_weights:累积权重(效果相同,只是另一种写法)
# weights=[5,3,2] 等价于 cum_weights=[5,8,10]
result2 = random.choices(
    ["一等奖", "二等奖", "三等奖", "谢谢参与"],
    cum_weights=[1, 5, 15, 100],   # 累积:1%, 5%, 15%, 100%
    k=1
)[0]
print(f"抽奖结果:{result2}")

模拟一个加权骰子:

import random
from collections import Counter

# 作弊骰子:6点概率是其他面的3倍
biased_dice = random.choices(
    [1, 2, 3, 4, 5, 6],
    weights=[1, 1, 1, 1, 1, 3],   # 6的权重是3,其他是1
    k=10000
)
counts = Counter(biased_dice)
print("作弊骰子统计:")
for face in range(1, 7):
    pct = counts[face] / 100
    bar = "█" * (counts[face] // 100)
    print(f"  {face}点:{counts[face]:5d}次({pct:.1f}%){bar}")

3.3 sample(population, k)——不重复抽取(无放回抽样)

sample 从序列中随机选 k 个不重复的元素:

import random

# 从 1-100 中不重复地选 6 个数(模拟彩票)
lottery = random.sample(range(1, 101), 6)
lottery.sort()
print(f"本期开奖号码:{lottery}")

# 和 choices 的区别:sample 不重复!
pool = [1, 2, 3, 4, 5]
print(f"sample(不重复):{random.sample(pool, 3)}")
print(f"choices(可重复):{random.choices(pool, k=3)}")

# 从字符串中取样(不重复字符)
word    = "PYTHON"
letters = random.sample(word, 4)
print(f"从{word}随机选4个字母:{''.join(letters)}")

# k 不能超过序列长度!
try:
    random.sample([1, 2, 3], 5)   # ValueError
except ValueError as e:
    print(f"错误:{e}")

实际应用——随机分组:

import random

students = ["张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十"]

# 随机打乱顺序后分组
shuffled = random.sample(students, len(students))   # 等同于洗牌

group_size = 2
groups = [shuffled[i:i+group_size] for i in range(0, len(shuffled), group_size)]

print("随机分组结果:")
for i, group in enumerate(groups, 1):
    print(f"  第{i}组:{'、'.join(group)}")

3.4 shuffle(seq)——原地打乱序列

shuffle 直接修改原列表,没有返回值

import random

# 基础用法
cards = list(range(1, 11))   # [1, 2, 3, ..., 10]
print(f"洗牌前:{cards}")
random.shuffle(cards)
print(f"洗牌后:{cards}")

# 注意:shuffle 修改原列表,返回 None!
deck = ["A", "2", "3", "4", "5"]
result = random.shuffle(deck)
print(f"deck:{deck}")       # 已被打乱
print(f"result:{result}")   # None!!!

# 如果需要保留原列表,先复制
original = [1, 2, 3, 4, 5]
shuffled = original.copy()
random.shuffle(shuffled)
print(f"原列表:{original}")  # 不变
print(f"打乱后:{shuffled}")  # 打乱

# 实际应用:发牌
def create_deck():
    suits  = ["♠", "♥", "♦", "♣"]
    values = ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
    return [f"{v}{s}" for s in suits for v in values]

deck = create_deck()
random.shuffle(deck)

players = 4
hands   = 5   # 每人5张

print("发牌结果:")
for i in range(players):
    hand = deck[i*hands : (i+1)*hands]
    print(f"  玩家{i+1}:{' '.join(hand)}")

3.5 choice vs choices vs sample vs shuffle 对比总结

┌──────────┬──────────────────────────────┬──────────┬──────────────────────────┐
│  方法    │  用途                        │  重复    │  返回值                  │
├──────────┼──────────────────────────────┼──────────┼──────────────────────────┤
│ choice   │ 选 1 个                      │ -        │ 单个元素                 │
│ choices  │ 选 k 个(可加权)            │ 允许重复 │ 新列表                   │
│ sample   │ 选 k 个(不重复)            │ 不重复   │ 新列表                   │
│ shuffle  │ 打乱整个列表                 │ -        │ None(原地修改!)       │
└──────────┴──────────────────────────────┴──────────┴──────────────────────────┘

第四部分:随机种子(seed)

4.1 什么是种子?

随机数生成器就像一台机器:
  - 种子(seed)= 机器的初始状态
  - 相同的种子 → 相同的初始状态 → 产生完全相同的随机序列
  - 不设种子 → Python 用当前时间戳作为种子 → 每次运行结果不同
import random

# ===== 不设种子:每次运行结果不同 =====
print("不设种子:")
print(random.randint(1, 100))   # 每次运行都不同
print(random.randint(1, 100))

print()

# ===== 设置种子:每次运行结果完全相同 =====
print("种子=42:")
random.seed(42)
print(random.randint(1, 100))   # 永远是 52
print(random.randint(1, 100))   # 永远是 93
print(random.randint(1, 100))   # 永远是 15

print()

# 再次设置相同种子,序列重置
random.seed(42)
print("重置种子=42:")
print(random.randint(1, 100))   # 还是 52(和前面一样)
print(random.randint(1, 100))   # 还是 93

4.2 种子的实际用途

import random

# 用途1:让实验结果可重现(科学研究、机器学习调试)
def train_model(seed=None):
    if seed is not None:
        random.seed(seed)
    data = list(range(100))
    random.shuffle(data)
    train = data[:80]
    test  = data[80:]
    print(f"训练集前5个:{train[:5]}")
    print(f"测试集前5个:{test[:5]}")

print("第1次运行(seed=2024):")
train_model(seed=2024)
print()
print("第2次运行(seed=2024):")
train_model(seed=2024)   # 完全相同!
print()
print("第3次运行(不设种子):")
train_model()            # 每次不同

# 用途2:游戏地图生成——相同种子生成相同地图
def generate_map(seed, size=5):
    random.seed(seed)
    terrain = []
    types   = ["🌲", "🏔", "🌊", "🌾", "🏜"]
    for _ in range(size):
        row = [random.choice(types) for _ in range(size)]
        terrain.append(row)
    return terrain

print("n地图(种子=100):")
for row in generate_map(100):
    print(" ".join(row))

print("n地图(种子=100,再次生成):")
for row in generate_map(100):
    print(" ".join(row))   # 完全相同的地图!

4.3 保存和恢复随机状态

import random

random.seed(0)

# 产生几个随机数
for _ in range(3):
    random.random()

# 保存当前状态
state = random.getstate()

# 继续产生随机数
r1 = random.random()
r2 = random.random()
r3 = random.random()
print(f"原始序列:{r1:.4f}  {r2:.4f}  {r3:.4f}")

# 恢复状态
random.setstate(state)

# 再次产生,完全相同
r1b = random.random()
r2b = random.random()
r3b = random.random()
print(f"恢复后:  {r1b:.4f}  {r2b:.4f}  {r3b:.4f}")
print(f"完全相同:{r1==r1b and r2==r2b and r3==r3b}")

第五部分:概率分布

5.1 为什么需要不同的概率分布?

uniform(均匀分布):每个值等概率出现
  → 掷骰子、彩票、简单随机选择

normalvariate(正态分布):中间多、两端少的"钟形"分布
  → 人的身高体重、测量误差、成绩分布、股价波动

expovariate(指数分布):等待时间分布
  → 顾客到店的间隔时间、设备故障间隔

其他分布用于特定科学/工程计算场景

5.2 gauss / normalvariate——正态分布

正态分布有两个参数:

  • mu(μ):均值(分布的中心)
  • sigma(σ):标准差(分布的宽度,越大越分散)
import random

# gauss(mu, sigma):正态分布(速度稍快)
# normalvariate(mu, sigma):正态分布(线程安全)
# 对普通用途,二者效果相同

# 模拟一班学生成绩(均值70,标准差15)
scores = [random.gauss(70, 15) for _ in range(1000)]

# 统计分布
buckets = {"不及格(<60)": 0, "及格(60-69)": 0,
           "良好(70-79)": 0, "优秀(80-89)": 0, "满分(90+)": 0}

for s in scores:
    if   s < 60:  buckets["不及格(<60)"] += 1
    elif s < 70:  buckets["及格(60-69)"] += 1
    elif s < 80:  buckets["良好(70-79)"] += 1
    elif s < 90:  buckets["优秀(80-89)"] += 1
    else:         buckets["满分(90+)"]   += 1

print("成绩分布(1000人):")
for label, count in buckets.items():
    bar = "█" * (count // 10)
    print(f"  {label:15s}:{count:4d}人  {bar}")

输出(示例):

成绩分布(1000人):
  不及格(<60)   : 154人  ███████████████
  及格(60-69)  : 237人  ███████████████████████
  良好(70-79)  : 251人  █████████████████████████
  优秀(80-89)  : 228人  ██████████████████████
  满分(90+)    : 130人  █████████████

5.3 expovariate(lambd)——指数分布

指数分布描述”独立随机事件之间的等待时间”:

  • lambd(λ):单位时间内平均发生次数(速率)
  • 平均等待时间 = 1 / λ
import random

# 模拟咖啡店:平均每分钟来3个顾客
# lambd = 3 → 平均间隔 = 1/3 分钟 ≈ 20秒

arrivals = [random.expovariate(3) for _ in range(100)]

avg_interval = sum(arrivals) / len(arrivals)
print(f"100次到店间隔:平均 {avg_interval*60:.1f} 秒(期望约20秒)")

# 模拟一小时内到店时间轴
time   = 0.0
count  = 0
while time < 60:   # 60分钟
    interval = random.expovariate(3)
    time    += interval
    if time < 60:
        count += 1

print(f"1小时内到店顾客:{count}人(期望约180人)")

5.4 triangular(low, high, mode)——三角分布

三角分布指定最小值、最大值和最可能的值(众数)

import random

# 项目完工时间估算(最乐观3天,最悲观10天,最可能5天)
estimates = [random.triangular(3, 10, 5) for _ in range(10000)]

avg    = sum(estimates) / len(estimates)
over_7 = sum(1 for x in estimates if x > 7) / len(estimates)

print(f"平均完工时间:{avg:.2f} 天")
print(f"超过7天的概率:{over_7:.1%}")

5.5 betavariate(alpha, beta)——Beta 分布

Beta 分布生成 (0, 1) 之间的值,常用于模拟概率、比例:

import random

# alpha=beta=1:退化为均匀分布
# alpha>beta:偏向高值(商品好评率高)
# alpha<beta:偏向低值(商品好评率低)

# 模拟两款商品的好评率
product_a = [random.betavariate(8, 2) for _ in range(1000)]   # 通常是高好评
product_b = [random.betavariate(2, 5) for _ in range(1000)]   # 通常是低好评

avg_a = sum(product_a) / len(product_a)
avg_b = sum(product_b) / len(product_b)

print(f"商品A 平均好评率:{avg_a:.1%}(期望:{8/(8+2):.1%})")
print(f"商品B 平均好评率:{avg_b:.1%}(期望:{2/(2+5):.1%})")

5.6 其他分布(一览)

import random

# vonmisesvariate(mu, kappa):圆形分布,用于方向/角度
angle = random.vonmisesvariate(0, 2)   # 弧度值
print(f"随机角度:{angle:.4f} 弧度(≈{angle*180/3.14:.1f}°)")

# paretovariate(alpha):帕累托分布(80/20法则)
# 用于财富分配、城市规模等幂律分布模拟
wealth = random.paretovariate(1.5)
print(f"帕累托分布:{wealth:.4f}")

# weibullvariate(alpha, beta):威布尔分布
# 用于设备寿命、可靠性工程
lifetime = random.weibullvariate(10, 1.5)   # 平均寿命约10年
print(f"设备寿命估计:{lifetime:.2f} 年")

# gammavariate(alpha, beta):伽马分布
g = random.gammavariate(2, 1)
print(f"伽马分布:{g:.4f}")

# lognormvariate(mu, sigma):对数正态分布(用于股价等)
stock_return = random.lognormvariate(0.01, 0.1)
print(f"股票收益乘数:{stock_return:.4f}")

第六部分:实战案例

6.1 案例一:密码生成器

import random
import string

def generate_password(length=12, use_upper=True, use_digits=True, use_symbols=True):
    """
    生成指定规则的随机密码

    参数:
        length      - 密码长度(默认12位)
        use_upper   - 是否包含大写字母
        use_digits  - 是否包含数字
        use_symbols - 是否包含特殊符号
    """
    # 构建字符池
    chars = string.ascii_lowercase   # 小写字母 a-z

    if use_upper:
        chars += string.ascii_uppercase   # 大写字母 A-Z
    if use_digits:
        chars += string.digits            # 数字 0-9
    if use_symbols:
        chars += "!@#$%^&*"              # 常用特殊符号

    # 确保密码至少包含每种类型的一个字符
    must_have = [random.choice(string.ascii_lowercase)]
    if use_upper:
        must_have.append(random.choice(string.ascii_uppercase))
    if use_digits:
        must_have.append(random.choice(string.digits))
    if use_symbols:
        must_have.append(random.choice("!@#$%^&*"))

    # 用 sample 填充剩余位置(不重复,但这里 choices 更合适)
    remaining = random.choices(chars, k=length - len(must_have))

    # 合并并打乱顺序
    password_list = must_have + remaining
    random.shuffle(password_list)

    return "".join(password_list)

# 生成5个密码
print("生成的密码:")
for i in range(5):
    pwd = generate_password(16)
    print(f"  {i+1}. {pwd}")

# 不同配置
print("n纯数字PIN码(6位):")
print("  " + "".join(random.choices(string.digits, k=6)))

print("n只含字母(8位):")
print("  " + generate_password(8, use_digits=False, use_symbols=False))

6.2 案例二:模拟抽奖系统

import random

class LotterySystem:
    """可配置的抽奖系统"""

    def __init__(self, prizes):
        """
        prizes: 字典,格式为 {"奖品名称": 权重, ...}
        权重越大,中奖概率越高
        """
        self.prizes  = list(prizes.keys())
        self.weights = list(prizes.values())

    def draw_once(self):
        """抽取一次"""
        return random.choices(self.prizes, weights=self.weights, k=1)[0]

    def draw_multiple(self, n):
        """抽取 n 次(可重复)"""
        return random.choices(self.prizes, weights=self.weights, k=n)

    def simulate(self, n_draws=10000):
        """模拟 n 次抽奖,统计概率"""
        results = self.draw_multiple(n_draws)
        total   = self.weights
        total_w = sum(total)

        print(f"模拟{n_draws}次抽奖结果:")
        for prize in self.prizes:
            count    = results.count(prize)
            actual   = count / n_draws
            expected = self.weights[self.prizes.index(prize)] / total_w
            print(f"  {prize:10s}:实际{actual:.2%}  期望{expected:.2%}  "
                  f"(差距:{abs(actual-expected)*100:.2f}%)")

# 创建抽奖系统
lottery = LotterySystem({
    "一等奖(iPhone)": 1,
    "二等奖(AirPods)": 5,
    "三等奖(购物券)": 14,
    "参与奖(贴纸)":  80,
})

print("=== 单次抽奖 ===")
print(f"恭喜您获得:{lottery.draw_once()}")

print("n=== 十连抽 ===")
results = lottery.draw_multiple(10)
for r in results:
    print(f"  {r}")

print("n=== 概率模拟 ===")
lottery.simulate(100000)

6.3 案例三:蒙特卡洛估算 π

蒙特卡洛方法:用大量随机数来估算数学结果。

原理:
  在边长为2的正方形内(面积4),有一个半径为1的圆(面积π)
  随机撒点,落在圆内的概率 = π/4
  因此:π ≈ 4 × (落在圆内的点数 / 总点数)
import random

def estimate_pi(n_samples=1_000_000):
    """用蒙特卡洛方法估算 π"""
    inside = 0

    for _ in range(n_samples):
        x = random.uniform(-1, 1)
        y = random.uniform(-1, 1)
        # 判断点是否在半径为1的圆内
        if x**2 + y**2 <= 1:
            inside += 1

    return 4 * inside / n_samples

# 测试不同样本量
print("蒙特卡洛估算 π:")
for n in [100, 1_000, 10_000, 100_000, 1_000_000]:
    pi_est = estimate_pi(n)
    error  = abs(pi_est - 3.14159265) / 3.14159265 * 100
    print(f"  {n:>10,} 个样本:π ≈ {pi_est:.6f}  误差:{error:.4f}%")

输出(示例):

蒙特卡洛估算 π:
         100 个样本:π ≈ 3.120000  误差:0.6915%
       1,000 个样本:π ≈ 3.148000  误差:0.2025%
      10,000 个样本:π ≈ 3.143200  误差:0.0466%
     100,000 个样本:π ≈ 3.141880  误差:0.0196%
   1,000,000 个样本:π ≈ 3.141652  误差:0.0123%

6.4 案例四:随机游走(股价模拟)

import random

def simulate_stock(initial_price=100.0, days=252, mu=0.0003, sigma=0.02, seed=None):
    """
    模拟股票价格的随机游走

    参数:
        initial_price - 初始价格
        days          - 交易天数(252 = 一年)
        mu            - 每日平均收益率
        sigma         - 每日收益率标准差(波动率)
        seed          - 随机种子
    """
    if seed is not None:
        random.seed(seed)

    prices = [initial_price]
    for _ in range(days - 1):
        daily_return = random.gauss(mu, sigma)
        new_price    = prices[-1] * (1 + daily_return)
        prices.append(max(0.01, new_price))   # 价格不能为负

    final     = prices[-1]
    total_ret = (final - initial_price) / initial_price * 100
    max_p     = max(prices)
    min_p     = min(prices)

    return prices, final, total_ret, max_p, min_p

# 模拟一只股票一年的走势
prices, final, total_ret, high, low = simulate_stock(seed=2024)

print(f"初始价格:¥100.00")
print(f"最终价格:¥{final:.2f}")
print(f"全年收益:{total_ret:+.2f}%")
print(f"最高价格:¥{high:.2f}")
print(f"最低价格:¥{low:.2f}")

# 简单绘制走势(用字符画)
print("n价格走势(每周一个数据点):")
weekly = prices[::5]   # 每5天取一个点
max_w  = max(weekly)
min_w  = min(weekly)
for i, p in enumerate(weekly):
    bar_len  = int((p - min_w) / (max_w - min_w) * 30)
    bar      = "█" * bar_len
    print(f"  第{i*5+1:3d}天:¥{p:7.2f}  {bar}")

6.5 案例五:简易文字游戏——猜数字

import random

def guessing_game():
    """猜数字游戏"""
    secret  = random.randint(1, 100)
    max_try = 7
    attempt = 0

    print("=" * 40)
    print("    欢迎来到猜数字游戏!")
    print(f"   我想了一个 1~100 的数字")
    print(f"   你有 {max_try} 次机会,加油!")
    print("=" * 40)

    while attempt < max_try:
        attempt += 1
        remaining = max_try - attempt

        try:
            guess = int(input(f"n第{attempt}次猜测(还剩{remaining}次):"))
        except ValueError:
            print("  请输入整数!")
            attempt -= 1
            continue

        if guess < 1 or guess > 100:
            print("  超出范围!请输入 1~100 的数字")
            attempt -= 1
            continue

        if guess == secret:
            print(f"n🎉 恭喜!你猜对了!答案就是 {secret}!")
            print(f"   用了 {attempt} 次猜中!")
            return
        elif guess < secret:
            hint = "太小了!"
            if secret - guess > 20:
                hint += "(差很远呢)"
            print(f"  ↑ {hint}")
        else:
            hint = "太大了!"
            if guess - secret > 20:
                hint += "(差很远呢)"
            print(f"  ↓ {hint}")

    print(f"n💔 游戏结束!答案是 {secret},下次加油!")

# 运行游戏
# guessing_game()   # 取消注释即可运行

6.6 案例六:随机名字/词语生成器

import random

# 中文随机姓名生成
SURNAMES = ["赵", "钱", "孙", "李", "周", "吴", "郑", "王", "冯", "陈",
            "张", "刘", "杨", "黄", "林", "朱", "何", "罗", "宋", "唐"]

NAMES_MALE   = ["伟", "芳", "娜", "秀英", "敏", "静", "丽", "强", "磊",
                "军", "洋", "勇", "艳", "杰", "娟", "涛", "明", "超", "秀兰"]
NAMES_FEMALE = ["雪", "梅", "霞", "云", "燕", "莉", "琴", "慧", "颖", "雯",
                "婷", "美", "珍", "兰", "凤", "英", "文", "春", "华", "玲"]

def random_chinese_name(gender="random"):
    if gender == "random":
        gender = random.choice(["male", "female"])
    surname = random.choice(SURNAMES)
    pool    = NAMES_MALE if gender == "male" else NAMES_FEMALE
    # 随机1或2个字的名
    if random.random() < 0.4:
        given = random.choice(pool)
    else:
        given = random.choice(pool) + random.choice(pool)
    return surname + given

# 生成一份虚假的名单
print("随机生成10个名字:")
for i in range(10):
    name = random_chinese_name()
    age  = random.randint(18, 60)
    city = random.choice(["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉"])
    print(f"  {i+1:2d}. {name},{age}岁,来自{city}")

第七部分:安全随机数

7.1 普通 random 的安全性问题

random 模块使用 Mersenne Twister 算法:
  ✅ 速度快,统计特性好
  ❌ 不适合密码学用途!

原因:
  Mersenne Twister 不是"密码学安全"的随机数生成器
  攻击者只要观察到足够多的输出值(624个32位数),
  就可以完整推算出内部状态,预测未来的所有随机数!

  因此,如果你用 random 生成密码、令牌、验证码等,
  存在被攻击者预测的安全风险。

7.2 secrets 模块——密码学安全随机数

Python 3.6+ 内置了 secrets 模块,专门用于安全场景:

import secrets
import string

# secrets.randbelow(n):安全的 0 到 n-1 随机整数
print(secrets.randbelow(100))

# secrets.randbits(k):安全的 k 位随机整数
print(secrets.randbits(32))

# secrets.choice(seq):安全版 choice
print(secrets.choice(["A", "B", "C", "D"]))

# secrets.token_bytes(n):n 字节随机二进制数据
token = secrets.token_bytes(16)
print(f"随机字节:{token.hex()}")

# secrets.token_hex(n):n 字节的十六进制字符串(常用于 API Key)
api_key = secrets.token_hex(32)
print(f"API Key:{api_key}")

# secrets.token_urlsafe(n):URL 安全的随机字符串
session_id = secrets.token_urlsafe(32)
print(f"Session ID:{session_id}")

def secure_password(length=16):
    """使用 secrets 生成真正安全的密码"""
    alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
    while True:
        password = "".join(secrets.choice(alphabet) for _ in range(length))
        # 确保包含各类字符
        has_upper  = any(c.isupper() for c in password)
        has_lower  = any(c.islower() for c in password)
        has_digit  = any(c.isdigit() for c in password)
        has_symbol = any(c in "!@#$%^&*" for c in password)
        if all([has_upper, has_lower, has_digit, has_symbol]):
            return password

print("n安全密码(用 secrets 生成):")
for i in range(3):
    print(f"  {secure_password()}")

7.3 random vs secrets 场景对比

使用 random 的场景(速度优先,无安全要求):
  ✅ 游戏开发(地图生成、战斗结算)
  ✅ 模拟实验(蒙特卡洛、数值模拟)
  ✅ 机器学习(数据打乱、随机采样)
  ✅ 测试数据生成
  ✅ 抽奖、游戏概率

使用 secrets 的场景(安全性优先):
  ✅ 用户密码生成
  ✅ API 密钥生成
  ✅ Session Token(登录令牌)
  ✅ 邮件验证码、短信验证码
  ✅ CSRF Token
  ✅ 任何"如果被预测会造成损失"的场景

第八部分:常见陷阱与注意事项

8.1 陷阱1:shuffle 不返回新列表

import random

data = [1, 2, 3, 4, 5]

# ❌ 错误写法(很多新手犯的错)
shuffled = random.shuffle(data)
print(shuffled)   # None!!!原因:shuffle 原地修改,返回 None

# ✅ 正确写法1:先复制,再 shuffle
data2    = data.copy()
random.shuffle(data2)
print(data2)      # 打乱后的列表

# ✅ 正确写法2:用 sample 代替 shuffle,它返回新列表
data3 = random.sample(data, len(data))
print(data3)      # 打乱后的新列表,原 data 不变

# ✅ 正确写法3:sorted + key=lambda: random.random()
data4 = sorted(data, key=lambda x: random.random())
print(data4)

8.2 陷阱2:randint 和 randrange 的端点问题

import random

# randint(a, b):含 b(闭区间 [a, b])
values_int = set(random.randint(1, 5) for _ in range(10000))
print(f"randint(1,5) 可能出现的值:{sorted(values_int)}")   # {1, 2, 3, 4, 5}

# randrange(a, b):不含 b(半开区间 [a, b))
values_range = set(random.randrange(1, 5) for _ in range(10000))
print(f"randrange(1,5) 可能出现的值:{sorted(values_range)}")  # {1, 2, 3, 4}

# 容易犯错的场景
# 想从 0-9 中取一个数:
print(random.randrange(10))       # ✅ 0 到 9
print(random.randint(0, 9))       # ✅ 0 到 9
print(random.randrange(0, 10))    # ✅ 0 到 9
# print(random.randint(0, 10))    # ⚠️ 0 到 10!含 10!

8.3 陷阱3:多线程下的随机数不安全

import random
import threading

# ❌ 问题:多个线程共享同一个随机数生成器,可能导致结果不一致
# 解决方案1:每个线程创建自己的 Random 实例

def worker(thread_id):
    # ✅ 每个线程用独立的 Random 对象
    rng = random.Random()   # 独立实例,自动获取不同种子
    for _ in range(3):
        print(f"线程{thread_id}:{rng.randint(1, 100)}")

threads = [threading.Thread(target=worker, args=(i,)) for i in range(3)]
for t in threads:
    t.start()
for t in threads:
    t.join()

8.4 陷阱4:种子相同但调用顺序不同

import random

# 种子固定后,结果依赖于调用顺序
random.seed(42)
a = random.randint(1, 10)
b = random.random()
c = random.choice(["x", "y", "z"])
print(f"顺序1:a={a}, b={b:.4f}, c={c}")

random.seed(42)
# 改变调用顺序!
c2 = random.choice(["x", "y", "z"])  # 先调用这个
a2 = random.randint(1, 10)
b2 = random.random()
print(f"顺序2:a={a2}, b={b2:.4f}, c={c2}")
# 结果完全不同!因为消耗随机数的顺序变了

8.5 陷阱5:用 random 生成安全令牌(危险!)

import random
import secrets

# ❌ 不安全:用 random 生成验证码
def bad_verification_code():
    return "".join([str(random.randint(0, 9)) for _ in range(6)])

# ✅ 安全:用 secrets 生成验证码
def good_verification_code():
    return "".join([str(secrets.randbelow(10)) for _ in range(6)])

print(f"不安全的验证码:{bad_verification_code()}")
print(f"安全的验证码:  {good_verification_code()}")

第九部分:Random 类——创建独立的随机生成器

9.1 为什么要用 Random 类?

默认情况下,import random 后使用的是一个"全局实例"。
如果你需要:
  - 多个独立的随机流(互不影响)
  - 不同模块使用不同的种子
  - 不影响全局随机状态

就应该使用 random.Random 类创建独立实例。
import random

# 创建独立的随机生成器实例
rng1 = random.Random(seed=100)   # 种子100
rng2 = random.Random(seed=200)   # 种子200

# 两个生成器完全独立
print("生成器1:", [rng1.randint(1, 10) for _ in range(5)])
print("生成器2:", [rng2.randint(1, 10) for _ in range(5)])

# 重置生成器1,生成器2不受影响
rng1.seed(100)
print("生成器1重置后:", [rng1.randint(1, 10) for _ in range(5)])
print("生成器2继续:", [rng2.randint(1, 10) for _ in range(5)])

# 全局随机状态没有被影响
print("全局 random:", random.randint(1, 10))   # 不受 rng1/rng2 影响

9.2 实际应用:游戏中多个独立随机流

import random

class GameEngine:
    """使用独立随机流的游戏引擎"""

    def __init__(self, seed=None):
        base_seed = seed or random.randint(0, 2**32)

        # 不同系统使用不同种子,互相不干扰
        self.map_rng    = random.Random(base_seed ^ 0x1234)   # 地图生成
        self.loot_rng   = random.Random(base_seed ^ 0x5678)   # 掉落计算
        self.enemy_rng  = random.Random(base_seed ^ 0x9ABC)   # 敌人 AI
        self.effect_rng = random.Random(base_seed ^ 0xDEF0)   # 视觉效果

    def generate_map_tile(self):
        tiles = ["草地", "森林", "山地", "河流", "沙漠"]
        return self.map_rng.choice(tiles)

    def roll_loot(self):
        if self.loot_rng.random() < 0.1:   # 10% 掉落稀有物品
            return self.loot_rng.choice(["传奇武器", "稀有盔甲", "神秘药水"])
        return self.loot_rng.choice(["金币", "草药", "普通装备"])

    def enemy_action(self):
        return self.enemy_rng.choice(["攻击", "防御", "逃跑", "使用技能"])

# 使用固定种子,确保游戏可重现
game = GameEngine(seed=42)

print("游戏地图(前6格):")
for _ in range(6):
    print(f"  {game.generate_map_tile()}", end="  ")
print()

print("n战斗掉落(5场):")
for i in range(5):
    print(f"  第{i+1}场:{game.roll_loot()}")

print("n敌人行动(5次):")
for i in range(5):
    print(f"  回合{i+1}:{game.enemy_action()}")

第十部分:完整速查表

10.1 函数分类速查

📌 生成随机数
  random.random()             → [0.0, 1.0) 的浮点数
  random.uniform(a, b)        → [a, b] 的浮点数
  random.randint(a, b)        → [a, b] 的整数(含两端)
  random.randrange(stop)      → [0, stop) 的整数
  random.randrange(a, b)      → [a, b) 的整数(不含 b)
  random.randrange(a, b, s)   → range(a,b,s) 中的整数
  random.getrandbits(k)       → k 位随机整数

📌 序列操作
  random.choice(seq)          → 随机选 1 个
  random.choices(seq, k=n)    → 随机选 n 个(可重复,可加权)
  random.sample(seq, k)       → 随机选 k 个(不重复)
  random.shuffle(lst)         → 原地打乱(返回 None!)

📌 随机种子
  random.seed(n)              → 设置种子
  random.getstate()           → 获取当前状态
  random.setstate(state)      → 恢复状态

📌 概率分布
  random.gauss(mu, sigma)         → 正态分布
  random.normalvariate(mu, sigma) → 正态分布(线程安全版)
  random.uniform(a, b)            → 均匀分布
  random.expovariate(lambd)       → 指数分布
  random.triangular(low,high,mode)→ 三角分布
  random.betavariate(alpha, beta) → Beta 分布
  random.lognormvariate(mu,sigma) → 对数正态分布

📌 安全随机(secrets 模块)
  secrets.randbelow(n)        → 安全随机整数 [0, n)
  secrets.randbits(k)         → 安全随机 k 位整数
  secrets.choice(seq)         → 安全随机选一个
  secrets.token_hex(n)        → n 字节的十六进制字符串
  secrets.token_urlsafe(n)    → URL 安全的随机字符串
  secrets.token_bytes(n)      → n 字节随机二进制数据

10.2 选择哪个函数?决策树

你想要的是...

生成随机数?
  ├─ 浮点数 0~1      → random()
  ├─ 浮点数 a~b      → uniform(a, b)
  ├─ 整数(含两端) → randint(a, b)
  └─ 整数(类似range)→ randrange(a, b, step)

从序列中选?
  ├─ 只选1个          → choice(seq)
  ├─ 选多个可重复     → choices(seq, k=n)
  │   └─ 有概率权重   → choices(seq, weights=[...], k=n)
  ├─ 选多个不重复     → sample(seq, k)
  └─ 打乱整个序列     → shuffle(seq)(原地!)

需要可重现结果?  → seed(n) 设置种子
需要安全性?      → 用 secrets 模块
需要独立随机流?  → Random() 创建实例
需要概率分布?    → gauss / expovariate / triangular 等

总结

random 模块是 Python 中最常用的内置模块之一。学完本章,你应该掌握:

  1. 基础函数random()uniform()randint()randrange() 的区别和使用场景
  2. 序列操作choice(选1个)、choices(有放回)、sample(无放回)、shuffle(原地打乱)
  3. 随机种子:为什么要用 seed(),什么时候用
  4. 概率分布:正态分布、指数分布、三角分布等应用场景
  5. 安全随机:普通 random 不能用于密码学,应使用 secrets 模块
  6. 独立实例:用 random.Random() 创建独立的生成器,避免干扰全局状态
  7. 常见陷阱shuffle 返回 Nonerandint 含端点、多线程安全等

发表评论