今天不聊那些花里胡哨的第三方库,就盯住Python标准库里最容易被低估的两个模块——time和random。很多人写脚本的时候天天用它们,但真问起来,往往是一知半解:time.sleep(1)让程序停一秒会写,random.randint(1,100)猜数字会写,再往深了问,perf_counter和time有什么区别?random.seed()该什么时候用?strptime和strftime谁转谁来着?一下就卡壳了。
这篇文章就把这两个库彻底讲透。从核心函数的时间语义、格式化映射,到随机数的种子机制、抽样逻辑,再到两者联手解决真实业务问题(超时重试、日志打点、测试数据构造),一次说清楚。文章里有参数说明、有对照表、有实测结论,也有我这些年踩过的坑,适合刚起步的Python初学者建立正确概念,也适合写了不少脚本但没系统梳理过这两个库的开发者查漏补缺。读完你不需要再翻第二篇资料。
1. 先把time库的四个核心函数吃透
1.1 时间在Python里的两种形态:时间戳与时间元组
很多初学者刚开始接触time库时,最困惑的就是“Python里时间到底长什么样”。这问题问得很好,因为如果你不理解时间有两种形态,后面所有格式化操作都会一头雾水。
在time库里,时间一共就两种形态。
第一种叫时间戳(timestamp),就是一个浮点数,表示从1970年1月1日0时0分0秒(UTC)到当前时刻经过的秒数。time.time()返回的就是这个数。它的特点是:精确、方便比较大小、方便做加减法运算。比如你记录一个任务开始和结束的时间戳,一减就能得出耗时,这在性能测试和耗时统计里非常常用。
第二种叫时间元组(struct_time),是一个类元组的对象,包含年、月、日、时、分、秒、星期几、一年中的第几天、是否夏令时这九个字段。time.localtime()和time.gmtime()返回的都是它。它的特点是:字段清晰、人类可读,适合用来提取“今天是星期几”“现在是几月”这类信息。
理解这两种形态是掌握time库的钥匙。时间戳是“机器的语言”,时间元组是“人的语言”。所谓的格式化输出,本质就是把人看的时间元组渲染成字符串;所谓的解析时间字符串,本质就是把字符串拆解回时间元组。
import time # 当前时间戳(float秒) now_ts = time.time() print(now_ts) # 1720000000.123456 # 转为本地时间元组(struct_time) now_tm = time.localtime(now_ts) print(now_tm) # time.struct_time(tm_year=2024, tm_mon=7, tm_mday=3, ...)上面这段代码里,time.localtime()默认可以接收一个时间戳参数,如果不传参数就用当前时间。它会把UTC时间戳换算成你所在的本地时区时间。而time.gmtime()不做时区换算,直接生成UTC时间元组。绝大多数业务场景里你想要的是localtime的本地时间结果,用gmtime的话会比北京时间少8个小时。
1.2 strftime与strptime:格式化的双向通道
解决了“时间长什么样”的问题,接下来就是“怎么把时间给别人看”和“怎么把别人给的时间读进来”。这就是strftime(string format time)和strptime(string parse time)的职责。
strftime的用法:
import time now = time.localtime() # 标准日志时间格式 log_time = time.strftime("%Y-%m-%d %H:%M:%S", now) print(log_time) # 2024-07-03 15:08:32这里的%Y、%m、%d、%H、%M、%S都是格式化指令。%Y是四位数年份,%y是两位数年份;%m是两位数字月份;%d是两位数字日期;%H是24小时制小时(%I是12小时制,配合%p可以显示AM/PM);%M是分钟;%S是秒钟。还有几个不太常见但特别好用的:%A输出完整星期名(如Monday),%j输出一年中的第几天(001到366),%U输出一年中的第几周。按周做数据报表的时候,%U和%j简直救命。
strptime是反方向操作,把字符串解析成时间元组:
import time time_str = "2024/07/03 15:08" parsed = time.strptime(time_str, "%Y/%m/%d %H:%M") print(parsed.tm_year, parsed.tm_mon, parsed.tm_mday) # 2024 7 3注意,strptime的格式化字符串必须和待解析的字符串严格匹配,包括里面的分隔符。你字符串里用斜杠,格式化串里也得用斜杠;你字符串里没有秒,格式化串里就不能写%S。这个函数返回的时间元组里,秒钟字段默认为0,这个细节很多人不知道。
实战里最常见的报错就是格式匹配不上。比如用户传进来的是"2024-7-3"(月和日没补零),你用"%Y-%m-%d"解析就报错。这一点在写接口处理外部传入的时间参数时尤其要小心,要么用严格校验,要么在解析前统一做数据清洗。
我把常见的格式化指令整理成了一张速查表,建议直接存下来:
| 指令 | 含义 | 示例输出 |
|---|---|---|
%Y | 四位数年份 | 2024 |
%y | 两位数年份 | 24 |
%m | 两位数月份 | 07 |
%d | 两位数日期 | 03 |
%H | 24小时制小时 | 15 |
%I | 12小时制小时 | 03 |
%M | 分钟 | 08 |
%S | 秒钟 | 32 |
%p | AM/PM标识 | PM |
%A | 完整星期名 | Wednesday |
%a | 缩写星期名 | Wed |
%B | 完整月份名 | July |
%b | 缩写月份名 | Jul |
%j | 一年中的第几天 | 184 |
%U | 一年中的第几周(周日为一周起点) | 26 |
注意:
strptime和strftime的参数顺序容易记混,前者是“先给字符串再给格式”,后者是“先给格式再给时间元组”。我记了这么多年,口诀就一句话:有时间元组才能“格式化成字符串”(time -> string,format),所以strftime(格式, 时间)格式在前;有字符串才能“解析成时间元组”(string -> time,parse),所以strptime(字符串, 格式)字符串在前。
1.3 sleep与perf_counter:两个经常被误解的函数
time.sleep(seconds)没人不会用,但很少有人想过:它和直接用time.time()前后相减的精度差异有多大?以及time.perf_counter()为什么更适合做程序耗时测量?
先说sleep。它接收一个浮点数参数,让当前线程挂起指定秒数。注意我说的是“当前线程”,不是整个程序。多线程场景下,time.sleep(0.01)只会让当前这个线程让出CPU,其他线程该跑还跑。另外sleep(0)是一个合法用法,它不会真正让程序睡过去,而是主动让出CPU时间片,让其他线程有机会执行,在IO密集型的多线程程序里偶尔会用来缓解线程饥饿。这个用法知道的人不多,但遇见问题的时候特别好使。
再说perf_counter。做代码性能测试的时候,很多人习惯用time.time()前后一减:
import time t0 = time.time() # 要测量的代码... t1 = time.time() print(f"耗时: {t1 - t0} 秒")这种做法在Windows上问题不大,但在某些系统上,time.time()的精度和稳定性并不可靠。因为time.time()返回的是墙上时钟时间(wall clock time),它会受系统校时影响——比如你的电脑自动同步了网络时间,时间往后拨了,那你测出来的t1 - t0可能变成负数。
time.perf_counter()返回的是“性能计数器”,它的基准点不确定(可能是开机时间,也可能是某个任意的固定时刻),但它专为测量短时间间隔设计,精度远高于time.time(),并且不受系统时间调整的影响。这才是做性能测试的正确工具:
import time t0 = time.perf_counter() # 要测量的代码... t1 = time.perf_counter() print(f"耗时: {(t1 - t0) * 1000:.2f} 毫秒")经验之谈:测量超过几秒钟的任务,
time.time()也够用;但测量毫秒级别的函数耗时,务必用perf_counter。另外还有个time.process_time(),它只统计当前进程消耗的CPU时间,而且不包含sleep期间的时间。如果你想精确知道“这段代码占用了多少CPU”,用它比perf_counter更贴切。不过日常性能排查里perf_counter是最顺手的一个。
2. time库的高阶操作与常见踩坑盘点
2.1 计算时间差与日期偏移的通用手法
时间戳形态最大的优势就是可计算性。所谓时间差,说白了就是两个时间戳相减;所谓日期偏移,说白了就是时间戳加上一个固定的秒数。这个思路一旦建立,很多需求都会变得异常简单。
比如“昨天的这个时刻”“三天前的中午12点”“下周一的0点整”,这类时间偏移需求,用datetime库当然也能做,但在只需要time库的场景下,直接用秒数推算反而更直接。一天是86400秒,这一条就够用了。
import time now = time.time() # 24小时前的时刻 yesterday = time.localtime(now - 86400) print(time.strftime("%Y-%m-%d %H:%M:%S", yesterday)) # 30天前的时刻 thirty_days_ago = time.localtime(now - 30 * 86400) print(time.strftime("%Y-%m-%d %H:%M:%S", thirty_days_ago))这里最大的坑在于“天”这个概念。直接减去86400秒,算的是“24小时前”,不是“昨天的此刻”。如果昨天的这个时刻是02:30,而今天凌晨恰好经历了夏令时切换,那“昨天的此刻”和“24小时前”就不是一回事。国内没有夏令时问题,但如果你处理的是海外业务或者跟UTC时间打交道,就要格外小心。
还有一个实用技巧:求某个月的第一天或最后一天。虽然time库不直接提供“这个月有几天”的接口,但你可以利用下个月第一天的前1秒来反推:
import time # 当前月份的首日 first_day = time.strptime(time.strftime("%Y-%m-01", time.localtime()), "%Y-%m-%d") first_ts = time.mktime(first_day) # 下月首日 - 1秒 = 本月最后一天 import calendar # 其实这里有更轻量的做法:先把月份加1,构造下月首日,再减1秒不过说句实在话,真到了复杂的日期推算场景(跨月、跨年、自然月边界),time库的体验就比较涩了。这种情况我强烈建议直接用datetime库的timedelta和replace,它专门处理这类日历运算。工具选型要分场景:简单的时间戳运算用time,复杂的日历运算用datetime,两者配合才是完整的时间处理方案。
2.2 时间字符串解析的边界问题
strptime在解析自由格式的字符串时,经常因为格式不匹配而抛ValueError。这个异常信息对新手很不友好,往往只报一个“time data does not match format”,并不告诉你具体哪个位置对不上。
实际排查的时候,我一般用二分法缩小范围:先解析前半段(比如只解析日期部分%Y-%m-%d),成功后再加时间部分。或者在解析前先打印待解析字符串的repr,排除掉肉眼不可见的空格、换行符、以及全角字符问题。这类排查经验比背异常文档管用得多。
还有一个细节是%y和%Y的解析差异。strptime("24-07-03", "%y-%m-%d")会解析成2024年,%y自动帮你把两位年份映射到世纪:69及以下映射到2000年代,70及以上映射到1900年代(POSIX标准)。这个历史和Unix时间戳的基准有关,知道有这么回事就行,但手写解析逻辑的时候最好统一用四位数年份,别让这种隐式逻辑给你挖坑。
再补充一个我干活时必用的骚操作:解析ISO格式的UTC时间字符串时,如果字符串末尾带Z(表示UTC),strptime是不认识%z处理不了Z的(%z能处理+0800这种带时区偏移的,但不认Z)。处理办法是先把字符串里的Z替换成+0000再解析。这个坑在对接外部接口、云厂商日志时经常遇到,记下来能省半小时。
2.3 time.struct_time的索引访问特性
struct_time这个类型很多人只知道按字段名访问(tm_year、tm_mon),但忽略了一个特性:它支持类似元组的索引访问。time.localtime()[0]就是年份,[1]是月份,[2]是日。这在某些需要快速遍历字段的场景里很有用。
但需要注意:struct_time虽然长得像元组,但它的字段值有严格的取值范围。比如tm_mon必须是1到12,tm_hour是0到23。如果你试图构造一个非法的时间元组再传给time.mktime(),它会抛异常或者给出一个匪夷所思的结果。前几年我遇到过一个问题:用time.mktime((2024, 13, 1, ...))想构造“明年1月”的时间戳,结果报错。后来我养成了习惯,凡是涉及手动构造时间元组的逻辑,都先在本地跑一遍边界值。
3. random库:从均匀随机到随机抽样
3.1 核心随机数函数的语义差异
random库里最基础的是“生成一个随机小数”和“生成一个随机整数”这两类函数。很多初学者分不清random.random()、random.uniform(a, b)、random.randint(a, b)、random.randrange(start, stop, step)四者的关系,其实把它们按照区间和类型来划分就非常清晰了。
random.random()返回[0.0, 1.0)区间的随机浮点数,左闭右开。它是整个模块最底层的构建块,很多其他函数都是在它基础上变形得到的。如果你要一个0到100之间的浮点数,用random.random() * 100即可。
random.uniform(a, b)返回[a, b]或[b, a]区间内的随机浮点数(不保证a小于b,自动处理)。它和random.random()的区别在于,它内部处理了端点范围,你只需要关心业务语义上的上下界。
random.randint(a, b)返回[a, b]区间内的随机整数,注意这里是闭区间,b是包含在内的。也就是说random.randint(1, 3)可能返回1、2、3三个数。这是和randrange最容易混淆的点。
random.randrange(start, stop, step)等价于random.choice(range(start, stop, step)),它是半开区间,stop不包含。random.randrange(1, 3)只返回1或2。如果你习惯写循环里的range(0, n)这种语法,那么randrange的边界语义和它完全一致,不容易出界。
import random print(random.random()) # 0.374... [0,1) print(random.uniform(10, 20)) # 15.342... [10,20] print(random.randint(1, 6)) # 5 闭区间 [1,6],骰子模拟首选 print(random.randrange(0, 10, 2)) # 6 半开区间 [0,10) 步长2,只会返回偶数四者的差异我用一张表总结了一下:
| 函数 | 返回类型 | 区间特性 | 典型用途 |
|---|---|---|---|
random.random() | float | [0.0, 1.0) | 底层概率计算 |
random.uniform(a, b) | float | [a, b](自动处理a>b) | 模拟连续量 |
random.randint(a, b) | int | [a, b]闭区间 | 骰子、抽奖整数 |
random.randrange(start, stop, step) | int | [start, stop)半开区间 | 索引随机、步长遍历 |
3.2 choice、choices、sample:随机抽样的三兄弟
如果说前面的函数是“按数值随机”,那么choice、choices、sample就是“按元素随机”。它们都是从序列中抽取元素,但抽取方式大不相同,用错了业务逻辑就出问题。
random.choice(seq)从一个非空序列中随机返回一个元素。列表、元组、字符串都可以,哪怕字符串传进去,返回的也是单个字符。它对应“一个样本”的需求。
random.choices(population, weights=None, k=1)是从总体中有放回地抽取k个样本,返回一个列表。重点是有放回——同一样本可能被多次抽到。它还支持weights权重参数,这在地球场景里非常有用:比如按比例做流量分配、抽奖概率调整。
import random # 模拟掷10次硬币 results = random.choices(["正面", "反面"], weights=[0.5, 0.5], k=10) print(results) # 按权重抽取:80%概率选A,20%概率选B choice = random.choices(["A", "B"], weights=[0.8, 0.2], k=1)[0]random.sample(population, k)是无放回地抽取k个样本,返回列表。它保证抽取的结果不重复,适合做“从100个人里随机选10个”“随机划分训练集和验证集”这类场景。要注意的是 k 不能超过总体长度,否则抛ValueError。
三兄弟的差异性总结:
| 函数 | 有无放回 | 返回形式 | 是否支持权重 | 典型场景 |
|---|---|---|---|---|
choice | 无(单次) | 单个元素 | 否 | 随机选一个 |
choices | 有 | 列表(k个) | 是 | 概率抽样、大量采样 |
sample | 无 | 列表(k个) | 否 | 不重复随机抽样、数据划分 |
sample还有一个非常实用的特例:当 k 等于总体长度时,它等价于原地打乱后返回一个全新列表。所以如果你既需要随机打乱列表又不希望影响原列表数据,可以random.sample(lst, len(lst))。这个技巧比先shuffle再复制更简洁,只是要注意它会额外占用一份内存。
3.3 shuffle与seed:随机性背后的控制手段
random.shuffle(lst)是原地打乱列表元素顺序,注意它不返回新列表,返回的是None。这个设计导致了一个很常见的新手错误:
import random lst = [1, 2, 3, 4, 5] new_lst = random.shuffle(lst) print(new_lst) # None print(lst) # [3, 1, 4, 5, 2] 原列表已经被修改如果你需要保留原列表,应该用上一节说的random.sample(lst, len(lst))。另外shuffle对字符串这样的不可变类型是无效的,它只支持可变序列(列表)。需要打乱字符串的时候,先list(str)打乱再''.join()回来。
然后就是random.seed()。这可能是整个random库里概念最重要、却最容易被忽略的函数。
random.seed(a)的作用是让随机数生成器进入一个确定性的状态。只要种子相同,后续产生的随机数序列就完全相同。这个特性有两大核心价值:第一是实验可复现——你做机器学习实验、模拟分析时需要别人用完全相同的数据重现你的结果,这时设置种子是不可或缺的一环;第二是调试可复现——程序里某个随机错误出现了,你设置了种子后重新运行,就能稳定复现同一个错误,不用碰运气。
import random random.seed(42) print([random.randint(1, 100) for _ in range(5)]) # [82, 15, 4, 95, 36] random.seed(42) # 重置种子 print([random.randint(1, 100) for _ in range(5)]) # [82, 15, 4, 95, 36] 完全一致但这里有一个新手极易踩的坑:把seed放在循环内部。比如你每隔一段时间要生成一组随机数,习惯性地在每次循环开头都调一次random.seed(当前时间戳),结果你会发现生成的“随机”数据高度雷同甚至完全一样。原因很简单:只要两个循环迭代之间的时间间隔极短,时间戳的整数部分完全一样,种子一样,随机序列自然一样。正确做法是在程序入口设置一次种子,而不是在每个使用点重复设置。
还有一点要记住:seed()一旦设置,它影响的是全局随机数生成器的状态。如果你在某个模块里调用了random.seed(),会影响到程序其他部分的随机性。如果只是想局部复现随机序列,正确的做法是创建独立的实例:
import random # 两个独立的随机数生成器,互不干扰 rng1 = random.Random(42) rng2 = random.Random(42) print(rng1.random(), rng2.random()) # 相同 print(rng1.random(), rng2.random()) # 相同,且互不影响random.Random(seed)创建一个独立的生成器实例,它有自己的内部状态。这在多模块、多线程程序中是推荐做法:在A模块中使用rng1,在B模块中使用rng2,彼此之间完全隔离,既保证了局部可复现,又不破坏全局随机性。
4. 两个库的真实联动作战与问题排查
4.1 打日志带时间戳,最日常却最容易出错的细节
日志打点是time库最经典的日常应用。很多人的第一版日志代码长这样:
import time print(f"[{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}] 用户登录成功")功能没问题,但有几个细节打磨之后就显专业了。
第一,把时间戳放在日志中间是一个很不好的习惯。因为一旦日志多起来,你要按时间排序、过滤时间段,时间戳不固定在开头,处理起来就非常痛苦。正确的格式是[2024-07-03 15:08:32] [INFO] 用户登录成功,把时间缀在最前。
第二,如果日志要求毫秒精度,%Y-%m-%d %H:%M:%S不够用。处理方式是让strftime负责秒级别,再用时间戳的小数部分手动补毫秒:
import time def current_time_ms(): ts = time.time() ms = int((ts - int(ts)) * 1000) return f"{time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(ts))}.{ms:03d}" print(f"[{current_time_ms()}] [INFO] 用户登录成功")第三,直接用print打日志的时候,控制台的时间来自系统终端,文件输出时间和终端时间有时会有肉眼可见的错位。这是因为 print 的缓冲机制和 flush 时机不同。如果你是为了调试而打日志,建议把日志字符串写完整之后一次性输出,或者干脆用标准库logging配置格式。真到了生产环境,手工拼时间串的表现力远不如logging的Formatter方便。
4.2 模拟接口延迟与指数退避重试
讲完了time和random各自的核心能力,就到了联动的重头戏——模拟网络抖动、构造接口负载、实现重试机制。这两个库在一起能解决一类非常典型的工程问题。
先说模拟接口延迟。你在写单元测试或者压测脚本时,需要让请求处理耗时看起来像真实的网络交互,这就要用到random.uniform(0.1, 0.5)加time.sleep()配合。
import random import time def simulate_network_request(): # 模拟 100ms 到 500ms 的网络波动 time.sleep(random.uniform(0.1, 0.5)) return "ok"这里用uniform而不是randint,就是为了让延迟分布更平滑自然,更接近真实网络中的连续分布。
再进阶一点,就是“指数退避重试”(exponential backoff)。这是客户端遇到请求失败时普遍采用的重试策略:第一次失败后等1秒再试,第二次失败后等2秒,第三次等4秒,等比增长,同时加入随机抖动(jitter),防止多个客户端在同一个时间点集体重试,导致服务端瞬间被请求淹没。这就是time和random的经典配合场景:
import random import time base_delay = 1 # 基础等待1秒 max_retries = 5 for attempt in range(max_retries): success = call_server() # 假设这是你的请求函数 if success: break # 指数退避 + 随机抖动 delay = min(30, base_delay * (2 ** attempt)) + random.uniform(0, 0.5) print(f"第 {attempt + 1} 次重试,等待 {delay:.2f}s") time.sleep(delay)为什么必须加随机抖动?因为如果没有 jitter,所有客户端都会在同一时刻发送重试请求,形成“惊群效应”,本来服务端只是偶发错误,结果被重试流量硬生生打挂了。加抖动之后的退避策略是业界标准做法,值得记下来。
另外,time.sleep在Windows上的最小精度大约是15.6毫秒。也就是说time.sleep(0.001)实际上可能睡15毫秒以上。如果你在Windows上需要高精度的短时等待,建议退而求其次用忙等待(busy wait)配合perf_counter,但那需要额外处理CPU占用问题。做跨平台脚本时,这个精度差异很容易让人产生“代码在Windows上速度不对”的困惑。
4.3 构造随机测试数据的标准姿势
做测试时经常需要伪造一批用户数据:手机号、订单号、随机分数。这类需求用random库的各个函数组合起来就非常快。
import random import string def fake_user_id(): # 8位字母数字组合,不含容易混淆的字符 chars = string.ascii_uppercase + string.digits return ''.join(random.choices(chars, k=8)) def fake_score(): # 生成符合正态分布的分数,均值75,标准差10 score = int(random.gauss(75, 10)) return max(0, min(100, score))这个例子里有几点值得说明:第一,random.choices优于random.choice的循环调用,因为一次调用就生成全部字符,性能更高。第二,string.ascii_uppercase + string.digits是常用的字符池,比手写一大串字符更可靠、更易读。第三,random.gauss(mu, sigma)可以生成正态分布随机数,模拟真实场景里的成绩分布比均匀分布合理得多——真实数据大多围绕均值波动,而不是均匀撒开。
从“简单随机”到“真实随机”,这个认知差异正是初级和进阶脚本的分水岭。均匀分布在扑克牌、抽奖场景中是对的,但模拟身高等物理量、模拟成绩、模拟收入这类场景,正态分布才符合现实规律。random库虽然不提供复杂分布(比如泊松分布),但配合纯Python也能构造大部分基础分布逻辑。
4.4 常见问题快查表
最后把我在实际使用中遇到的、以及帮别人排查过的高频问题汇总成了一张表。每个问题都附上了原因和解决方向,按图索骥能省很多时间。
| 问题现象 | 根本原因 | 解决方向 |
|---|---|---|
strftime结果带乱码或奇怪字符 | 传入的不是struct_time,而是直接传了时间戳 | 先localtime再格式化 |
strptime报“time data does not match format” | 字符串与格式串分隔符/字段不一致 | 打印字符串的repr检查不可见字符 |
用time.time()测量短耗时结果偏差大 | 墙钟受系统校时影响 | 改用perf_counter |
random.randint(0, 1)总觉得结果不够随机 | 没设置种子,或每种子的使用方式不对 | 业务需要确定性时显式seed |
random.shuffle(lst)返回了None | 它是原地修改,不是返回新列表 | 需要新列表时用sample(lst, len(lst)) |
random.sample(lst, k)报错 k 大于总体 | 无放回抽取不能超过总长度 | 先检查长度,或改用choices |
sleep(0.001)在Windows上实际睡更久 | Windows 系统定时器精度限制 | 接受该精度,或换 busy wait |
| 多线程下全局随机数相互干扰 | seed影响全局状态 | 用random.Random()独立实例 |
这张表我每次带新人的时候都会打印出来贴墙上。别看都是小问题,生产环境里线上故障排查,往往就卡在这些不起眼的细节上。
开头说这个库简单,是相对于那些动辄几百个API的重量级框架而言的。但真正把它用好的关键,永远是理解边界语义和操作系统限制。time让你对“时间流逝”有了精确的控制权,random让你对“不确定性”有了可复现的驾驭力,这两样东西组合起来,已经能覆盖工作中相当大比例的脚本需求了。最后分享一个我个人的使用习惯:任何用到random的脚本,我都会在入口预留一个--seed命令行参数。平时随机跑,复现问题或做对比实验时固定种子。这个习惯帮我避开了无数次“上次跑得好好的,这次怎么结果不一样”的排查困境,强烈推荐你也试一下。