资讯动态

Python标准库time与random模块核心用法详解

发布时间:2026/10/14 10:56:00 来源:尧图企业网站定制
今天不聊那些花里胡哨的第三方库就盯住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_year2024, tm_mon7, tm_mday3, ...)上面这段代码里time.localtime()默认可以接收一个时间戳参数如果不传参数就用当前时间。它会把UTC时间戳换算成你所在的本地时区时间。而time.gmtime()不做时区换算直接生成UTC时间元组。绝大多数业务场景里你想要的是localtime的本地时间结果用gmtime的话会比北京时间少8个小时。1.2 strftime与strptime格式化的双向通道解决了“时间长什么样”的问题接下来就是“怎么把时间给别人看”和“怎么把别人给的时间读进来”。这就是strftimestring format time和strptimestring 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%H24小时制小时15%I12小时制小时03%M分钟08%S秒钟32%pAM/PM标识PM%A完整星期名Wednesday%a缩写星期名Wed%B完整月份名July%b缩写月份名Jul%j一年中的第几天184%U一年中的第几周周日为一周起点26注意strptime和strftime的参数顺序容易记混前者是“先给字符串再给格式”后者是“先给格式再给时间元组”。我记了这么多年口诀就一句话有时间元组才能“格式化成字符串”time - stringformat所以strftime(格式, 时间)格式在前有字符串才能“解析成时间元组”string - timeparse所以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表示UTCstrptime是不认识%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到12tm_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]自动处理ab模拟连续量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, weightsNone, k1)是从总体中有放回地抽取k个样本返回一个列表。重点是有放回——同一样本可能被多次抽到。它还支持weights权重参数这在地球场景里非常有用比如按比例做流量分配、抽奖概率调整。import random # 模拟掷10次硬币 results random.choices([正面, 反面], weights[0.5, 0.5], k10) print(results) # 按权重抽取80%概率选A20%概率选B choice random.choices([A, B], weights[0.8, 0.2], k1)[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, k8)) 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_counterrandom.randint(0, 1)总觉得结果不够随机没设置种子或每种子的使用方式不对业务需要确定性时显式seedrandom.shuffle(lst)返回了None它是原地修改不是返回新列表需要新列表时用sample(lst, len(lst))random.sample(lst, k)报错 k 大于总体无放回抽取不能超过总长度先检查长度或改用choicessleep(0.001)在Windows上实际睡更久Windows 系统定时器精度限制接受该精度或换 busy wait多线程下全局随机数相互干扰seed影响全局状态用random.Random()独立实例这张表我每次带新人的时候都会打印出来贴墙上。别看都是小问题生产环境里线上故障排查往往就卡在这些不起眼的细节上。开头说这个库简单是相对于那些动辄几百个API的重量级框架而言的。但真正把它用好的关键永远是理解边界语义和操作系统限制。time让你对“时间流逝”有了精确的控制权random让你对“不确定性”有了可复现的驾驭力这两样东西组合起来已经能覆盖工作中相当大比例的脚本需求了。最后分享一个我个人的使用习惯任何用到random的脚本我都会在入口预留一个--seed命令行参数。平时随机跑复现问题或做对比实验时固定种子。这个习惯帮我避开了无数次“上次跑得好好的这次怎么结果不一样”的排查困境强烈推荐你也试一下。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑