数值、随机、统计、精确计算与日期时间
本节目标
查询 Python math、Decimal、Fraction、随机、统计和日期时间语义。
选择模块前先说明结果要近似还是精确、随机值用于模拟还是安全,以及时间值代表墙上读数还是绝对时刻。本章依次以 math、decimal、fractions、random、secrets、statistics、datetime、zoneinfo 与 time 的 Python 3.14 文档为准。迭代、归约和排序工具见集合与函数工具,向外部进程传递数值和时间文本时再检查操作系统、命令行参数与子进程的接口边界。
math 与数值边界
math 提供实数数学函数、浮点判断以及组合数、最大公约数等整数函数。API 应先按输入域和结果类型选择,再定义如何处理定义域错误、溢出与近似比较。math 的同名实数函数不接受复数;期望复数结果时应改用 cmath,而不是吞掉 ValueError。
from math import isclose, isfinite, isqrt
root = isqrt(81)
close = isclose(0.1 + 0.2, 0.3, rel_tol=1e-12, abs_tol=0.0)
valid = isfinite(measurement)
isclose() 的相对和绝对容差是业务合同的一部分;尤其与零比较时通常需要非零 abs_tol。不要用 == 判断一般浮点计算,也不要把 math.pow() 当精确整数幂:它会把两个参数转换为 float,精确整数幂应使用 ** 或内置 pow()。
Decimal、context 与舍入
Decimal 表示十进制定点和浮点数,context 管理运算精度、舍入模式、信号标志与 trap。十进制意图应从十进制文本构造 Decimal,并在计算边界显式选择 context 与 quantize() 规则;从 float 构造只会忠实带入该二进制浮点值,无法恢复用户原先书写的十进制文本。
from decimal import Decimal, ROUND_HALF_UP, localcontext
with localcontext() as context:
context.prec = 12
price = Decimal("19.995").quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
context 精度影响算术运算,不会截短构造器已保存的有效数字;localcontext() 用于限制临时策略的作用域。不要把 context 当全局且不变的隐式配置,也不要忽略 Inexact、DivisionByZero 等信号是否应被检查或设为 trap。
from datetime import datetime, timedelta, timezone
from decimal import Decimal
from fractions import Fraction
from random import Random
from statistics import mean
total = Decimal("9.99") + Decimal("10.00")
ratio = Fraction(3, 4)
average = mean([1, 2, 3, 4])
first_random = Random(314159)
second_random = Random(314159)
reproducible = [first_random.randrange(100) for _ in range(4)] == [
second_random.randrange(100) for _ in range(4)
]
fixed_zone = timezone(timedelta(hours=8))
moment = datetime(2026, 8, 17, 12, tzinfo=timezone.utc).astimezone(fixed_zone)
print(f"total={total}")
print(f"ratio={ratio}")
print(f"mean={average}")
print(f"random-reproducible={reproducible}")
print(f"local={moment.isoformat()}")
total=19.99
ratio=3/4
mean=2.5
random-reproducible=True
local=2026-08-17T20:00:00+08:00
代表脚本只使用固定输入:十进制金额来自文本,有理数保持精确,随机部分只比较两个独立生成器,日期时间从 UTC 转为固定偏移。代表脚本使用固定偏移 timezone,不读取宿主机的 IANA 时区数据、当前时间、locale 或系统本地时区。
Fraction 与精确有理数
Fraction 把值规范为互质的整数分子与正分母。比例本来就是整数之比或十进制文本时,应直接从这些来源构造;需要近似时再明确限制分母。由 float 构造得到的是该二进制浮点值的精确比值,所以 Fraction(0.1) 并不等于 Fraction(1, 10)。
from fractions import Fraction
ratio = Fraction(3, 4)
approximation = Fraction("3.1415926").limit_denominator(1_000)
limit_denominator(max_denominator) 返回分母不超过上限的最接近 Fraction,这是有损近似而不是修改原值。连续加、乘精确有理数可能让分子分母迅速增长;不要因为结果“精确”就忽略整数大小、内存与后续序列化边界。
random、种子与可复现性
random.Random 是确定性的伪随机生成器,模拟和可重复测试应使用显式注入种子的局部实例,避免共享模块级隐式状态。两个同版本、同算法调用路径且同种子的局部生成器可以重现结果,但固定种子所得序列不是应用可依赖的跨 Python 版本合同;除 random() 与兼容 seeder 的有限保证外,模块算法可能演进。
from random import Random
generator = Random(314159)
sample = generator.sample(range(100), k=5)
改变一次调用、分支或消费顺序都会推进状态并改变后续结果;并发共享状态还会让调用顺序成为隐含输入。random 不适合安全用途,也不要在快照中固化教学脚本无关的具体伪随机数字。
secrets 与安全随机
secrets 使用操作系统提供的安全随机源。密码重置令牌、不可猜 URL、认证秘密和安全选择需要 token_bytes()、token_hex()、token_urlsafe() 或 choice();固定种子的 PRNG 不具备这一安全属性。
from secrets import compare_digest, token_urlsafe
token = token_urlsafe(32)
matches = compare_digest(candidate, expected)
安全令牌不可复现,因此正文和测试只验证类型、长度范围或协议属性,不能把真实令牌写进固定输出和日志。省略 token 长度时采用的默认熵值可能在维护版本变化;协议要求固定强度时显式给出字节数,并把存储、轮换和泄漏响应也纳入设计。
statistics 与描述统计
statistics 面向描述统计和常见分布。选择 mean()、median()、variance() 或 pvariance() 前,先规定数据代表样本还是总体,以及空输入、缺失值和统一数值类型的处理方式。多数函数支持 int、float、Decimal 与 Fraction,但混合类型集合的行为未定义且依赖实现。
from statistics import StatisticsError, mean, variance
average = mean([1, 2, 3, 4])
try:
spread = variance([1])
except StatisticsError:
spread = None
variance() 是样本方差并至少需要两个数据点,pvariance() 则描述总体;空数据或数据量不足应成为显式失败合同。NaN 会给排序型统计带来意外结果,通常应先剔除;不要把描述统计函数误当成数据清洗、缺失值推断或完整科学计算库。
naive 与 aware datetime
datetime 同时表达日期字段、持续时间和带时区语义的时间点。系统边界保存绝对时刻时应使用 aware datetime,通常规范化为 UTC,只在展示边界转换。tzinfo 非 None 且 utcoffset() 非 None 的 datetime 才是 aware;naive datetime 不能标识无歧义的绝对时刻。
from datetime import datetime, timezone
instant = datetime(2026, 8, 17, 12, tzinfo=timezone.utc)
wire_value = instant.isoformat()
固定偏移 timezone 只表达恒定 UTC 偏移,不包含地区的夏令时和历史规则。不要用 replace(tzinfo=...) 冒充一般的时区转换,也不要混合排序 naive 与 aware datetime:它们的顺序比较会抛出 TypeError。
zoneinfo、时区规则与 fold
ZoneInfo 把 IANA 时区规则接到 datetime。用户选择地区时区时应保存时区键,并从已知绝对时刻转换;永久保存一次算出的偏移会丢失后续规则。zoneinfo 本身不携带时区数据库,会查找系统数据或已安装的 tzdata,缺少键时构造会失败。
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
local = datetime(2026, 1, 1, tzinfo=timezone.utc).astimezone(
ZoneInfo("Asia/Shanghai")
)
时钟回拨会让某些本地字段出现两次;fold 用 0 或 1 区分回拨区间里两次同名的本地时间,分别选择转换前与转换后的偏移。不要假定每台主机都有同版 IANA 数据,也不要把不存在的本地时间、模糊本地时间或 ZoneInfo 键拼写错误静默解释为 UTC。
日期时间解析与格式化
解析与格式化首先是交换协议。明确的 ISO 8601 形状可配合 fromisoformat()/isoformat();选择其他格式时,则要给出完整的 strptime() 合同。偏移是否存在、字段能否缺失、精度与错误处理都必须在接口处定义。
from datetime import datetime
parsed = datetime.fromisoformat("2026-08-17T20:00:00+08:00")
encoded = parsed.isoformat(timespec="seconds")
strptime() 未提供的字段会取默认值,格式和输入不匹配会抛出 ValueError;包含月份/日期却省略年份的格式还容易遇到闰年问题。%a、%A、%b、%B、%c 和 %p 等指令依赖 locale,不应进入跨环境固定输出。
单调时钟、deadline 与测试
墙钟可能因校时或管理员操作跳变,而 monotonic() 适合测持续时间和超时。进入操作时先计算 deadline,此后每次等待都用 deadline - monotonic() 得到剩余额度,避免多阶段各自重置完整 timeout。monotonic() 不会倒退,但参考点未定义,只有同一时钟两次读数之差有意义。
from time import monotonic
deadline = monotonic() + timeout
remaining = max(0.0, deadline - monotonic())
perf_counter() 适合高分辨率短时测量,process_time() 则不计 sleep,三者不可随意互换。测试 deadline 逻辑应注入可控时钟或直接测试纯剩余量计算;不要断言真实运行耗时恰好等于某值,也不要把单调时钟读数保存成可跨进程、跨重启解释的时间戳。