Python内置数学模块详解:math、random、statistics、decimal、fractions实战指南

发布时间:2026/9/18 4:31:53
Python内置数学模块详解:math、random、statistics、decimal、fractions实战指南 1. 数学类模块到底在解决什么问题先说点实际的。很多人学Python学到一半觉得列表、字典、循环、函数这些都玩明白了结果一碰数学计算就卡壳。不是不会写代码而是不知道用什么工具去写。比如你要算一组数据的平均值、标准差自己手写循环当然能做但代码又臭又长效率还低。再比如你要生成一个随机数、算个排列组合、或者处理浮点数的精度问题每次都得现查现写烦得很。Python内置的数学类模块就是专门来解决这些“数学脏活累活”的。它们不是某个第三方库而是Python自带的标准库装好Python就有不需要额外安装。常用的有四个math、random、statistics、decimal另外还有一个跟数学关系很紧密的fractions。这篇文章我会一个一个拆开讲结合实际场景告诉你什么时候用哪个、怎么用、坑在哪里。适合谁来读刚学完Python基础语法、想做数据分析但还没接触pandas/numpy的初学者以及写脚本时经常被数学计算卡住的朋友。看完之后你写代码会明显感觉“顺手”很多。2. 先搞懂math模块最基础的数学工具箱2.1 math模块的核心功能math模块是Python数学计算的“地基”。它提供了一堆数学函数和常量比如三角函数、对数函数、幂函数、取整函数等等。要理解它的定位你只要记住一句话它专门处理实数域的数学运算不能处理复数、不能处理数组就是老老实实的单值计算。常用的东西我列一下常量math.pi圆周率、math.e自然常数、math.tau2π、math.inf正无穷、math.nan非数值取整相关math.ceil()向上取整、math.floor()向下取整、math.trunc()截断取整幂和对数math.pow()、math.sqrt()、math.exp()、math.log()、math.log10()、math.log2()三角函数math.sin()、math.cos()、math.tan()、以及它们的反函数和双曲函数角度转换math.radians()角度转弧度、math.degrees()弧度转角度其他math.fabs()绝对值、math.fmod()浮点数取余、math.gcd()最大公约数、math.factorial()阶乘这里我想特别提一下math.gcd。你刷LeetCode或者做算法题的时候求最大公约数是个高频操作。过去很多人自己写辗转相除法代码也不算长但直接用math.gcd(a, b)一行搞定而且底层是C实现的跑得飞快。类似这种“原来标准库已经帮我封装好了”的发现是提升效率的关键。2.2 取整函数的区别很多人用错math模块里最容易踩坑的就是取整函数。ceil、floor、trunc、还有内置的round它们的行为完全不同。import math x 3.7 print(math.ceil(x)) # 4向上取整 print(math.floor(x)) # 3向下取整 print(math.trunc(x)) # 3直接丢弃小数部分 print(round(x)) # 4四舍五入 y -3.7 print(math.ceil(y)) # -3注意向上取整是往正无穷方向 print(math.floor(y)) # -4向下取整是往负无穷方向 print(math.trunc(y)) # -3截断是往零的方向 print(round(y)) # -4Python的round是银行家舍入不是简单的四舍五入看到没有负数的时候floor和trunc的结果完全不一样。floor(-3.7)是-4因为-4比-3.7小trunc(-3.7)是-3因为它就是简单砍掉小数位。很多人在这上面翻车导致计算结果差了个1。另外提一句roundPython 3里的round遵循“银行家舍入法”当小数部分刚好是0.5时它会舍入到最近的偶数。比如round(2.5)得到2round(3.5)得到4。这个坑在财务计算里很致命后面讲decimal的时候我会回头再说它。2.3 math模块的精度和性能特点math模块的函数底层都是C语言写的性能非常强。但有一点要注意它返回的都是Python的float类型也就是双精度浮点数。这意味着它天生存在浮点精度问题。举个经典例子import math print(0.1 0.2) # 0.30000000000000004 print(math.sqrt(2)) # 1.41421356237309510.1 0.2这个结果在Python里众所周知根源是二进制无法精确表示0.1这种十进制小数。如果你做的是普通科学计算、图像处理、游戏物理这个误差完全能接受。但如果你在算钱、算税率、算折扣这就是灾难。所以math模块定位很明确适合科学计算和工程计算不适合精确的十进制运算。3. 随机数到底怎么用random模块3.1 基础随机函数一览random模块是Python里生成随机数的标准方案。注意我强调的是“伪随机”——计算机没有真正的随机random模块用的是梅森旋转算法Mersenne Twister它生成的序列看起来随机但只要种子一样序列就完全一样。这在很多场景下其实是好事比如你要复现实验、跑测试固定种子就能得到相同结果。import random print(random.random()) # 返回[0.0, 1.0)之间的随机浮点数 print(random.uniform(1, 10)) # 返回[1, 10]之间的随机浮点数 print(random.randint(1, 100)) # 返回[1, 100]之间的随机整数注意是闭区间 print(random.randrange(0, 101, 2)) # 返回[0, 100]之间的偶数步长为2 print(random.choice([苹果, 香蕉, 橘子])) # 从序列中随机选一个 print(random.choices([苹果, 香蕉, 橘子], weights[5, 3, 2], k10)) # 带权重的随机选择我特别想强调randint和randrange的区别。randint(1, 100)包含100是闭区间randrange(0, 101, 2)不包含101是半开区间。很多人拿randrange当randint用结果发现边界值始终不出来逐个排查半天才发现是区间问题。3.2 洗牌和抽样写测试和仿真的利器random.shuffle()用于原地打乱列表顺序在写抽奖程序、卡牌游戏、随机测试数据生成时非常有用。跟它配套的是random.sample()从总体中不放回地抽取若干个元素返回一个新列表。import random cards [A, 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K] random.shuffle(cards) print(cards) # 原列表被打乱 users [user1, user2, user3, user4, user5] winners random.sample(users, 2) # 从5个用户中随机抽2个不放回 print(winners)这里有一个重要的区别sample是不放回抽样不会重复抽到同一个元素而choices默认是有放回的同一个元素可能被抽中多次。我做数据增强的时候经常用choices做有放回采样用sample做无放回切分两个配合使用覆盖了绝大多数随机抽样场景。3.3 随机数种子的正确打开方式random.seed()是新手最容易忽略、但老手最依赖的函数。设定种子之后随机序列就固定了。这在调试、测试、复现实验里有不可替代的价值。import random random.seed(42) print(random.randint(1, 100)) # 81 print(random.randint(1, 100)) # 14 random.seed(42) print(random.randint(1, 100)) # 81重新设定种子后序列从头开始 print(random.randint(1, 100)) # 14我自己的习惯是在写单元测试时必设种子保证测试结果可重复在做机器学习数据切分时也用固定种子保证训练集和验证集每次划分一致。否则你调了半天模型发现每次数据切分都不一样那整个实验就没法对比了。3.4 random模块的坑与性能边界random模块最大的坑是不要用它做密码学安全相关的随机数生成。梅森旋转算法是可预测的攻击者只要观察到足够多的输出就能推算出整个序列。如果你需要生成安全的随机密码、令牌、密钥用secrets模块那是基于操作系统提供的高强度随机源做的。另外random.shuffle虽然方便但它的随机性也有上限。如果你在做蒙特卡洛仿真需要海量高质量随机数random模块的性能会成为一个瓶颈。这时候要么用numpy的numpy.random要么用专门的随机数生成库。但对于普通脚本和中小规模数据random完全够用。4. statistics模块不用numpy也能做统计分析4.1 均值、中位数、众数一把梭很多人一提到统计就想到numpy、pandas但处理小规模数据时直接上第三方库有点“杀鸡用牛刀”。Python标准库里的statistics模块专门处理基础的统计计算数据量不大时完全够用。import statistics data [12, 15, 18, 22, 22, 25, 28, 30, 35, 42] print(statistics.mean(data)) # 24.9算术平均数 print(statistics.median(data)) # 23.5中位数 print(statistics.mode(data)) # 22众数 print(statistics.stdev(data)) # 9.32样本标准差分母n-1 print(statistics.pstdev(data)) # 8.84总体标准差分母n print(statistics.variance(data)) # 86.77样本方差 print(statistics.pvariance(data)) # 78.21总体方差注意stdev和pstdev的区别前者是样本标准差分母是n-1适合从样本推断总体后者是总体标准差分母是n适合数据就是完整总体的情况。这个区别在统计学里面很基础但很多写代码的人没这个概念导致计算结果跟Excel对不上。4.2 适合用什么、不适合用什么statistics模块适合处理的数据量级我个人的经验是几千个数值以内没有问题。一旦数据量上了几十万、上百万这个模块的计算速度就会明显变慢因为它是纯Python实现底层没有C扩展。什么时候应该换numpy当你需要对整个数组做矢量化运算、数据量巨大、或者需要进行矩阵运算时去用numpy。但如果你只是算个列表的平均值、标准差为了这个专门装一个numpy有点过重。标准库的statistics模块零依赖、开箱即用在很多运维脚本和数据处理脚本里反而更轻便。4.3 分位数、协方差等进阶用法statistics模块还提供了一些进阶功能比如分位数、协方差、相关系数。这些功能平时用到的频率略低但在做数据分布分析和变量相关性初筛时非常好用。import statistics data [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] x [1, 2, 3, 4, 5] y [2, 4, 6, 8, 10] # 分位数计算数据分布的分位点 # n4表示四分位默认情况下返回0%、25%、50%、75%、100%五个值 print(statistics.quantiles(data, n4)) # [3.25, 5.5, 7.75] # 协方差衡量两个变量共同变化的程度 print(statistics.covariance(x, y)) # 7.5完全正相关时协方差为正值 # 相关系数标准化后的协方差范围[-1, 1] print(statistics.correlation(x, y)) # 1.0完全正相关quantiles函数返回的是一个列表默认是n4就是四分位数。你可以改成n10得到十分位数n100得到百分位数。数据分析里经常用分位数来查看数据分布情况这个功能直接用标准库就能做不用非得开pandas。5. decimal模块精确计算财务数据的正确姿势5.1 浮点数精度问题的根源讲decimal之前必须先说清楚为什么float会有精度问题。计算机内部使用二进制表示数字而浮点数的存储空间是有限的。0.1在十进制里很简单但转换成二进制却是一个无限循环小数。就像1/3在十进制里是0.3333...一样无法被精确存储。所以每次你用float做运算都可能产生微小的偏差。print(0.1 0.2) # 0.30000000000000004 print(1.1 * 3) # 3.3000000000000003 print(0.3 - 0.1) # 0.19999999999999998这些误差在科学计算、机器学习里完全无所谓因为模型本身就有噪声。但如果你在写电商系统、财务系统、订单系统一分钱都不能差这时候就必须用decimal。5.2 decimal模块的基本用法decimal模块的核心是Decimal类。它使用十进制浮点表示法可以精确地表示十进制小数。你创建Decimal对象时要注意一个细节不要直接传float要传字符串。from decimal import Decimal # 正确姿势传字符串 a Decimal(0.1) b Decimal(0.2) print(a b) # 0.3精确结果 # 错误姿势传float a Decimal(0.1) b Decimal(0.2) print(a b) # 0.3000000000000000166533453694带着float的误差进来这个坑特别关键。你想想Decimal(0.1)先把0.1变成float而float的0.1已经不是精确的0.1了然后再转成Decimal就把之前的误差一起带进来了。所以记住创建Decimal对象从字符串创建不要从float创建。5.3 设置精度和舍入模式decimal模块允许你控制运算精度和舍入规则。这是它相比float最大的优势之一。默认精度是28位有效数字你可以通过getcontext()来调整。from decimal import Decimal, getcontext, ROUND_HALF_UP # 设置全局精度为50位小数 getcontext().prec 50 a Decimal(1) / Decimal(3) print(a) # 0.33333333333333333333333333333333333333333333333333 # 设置舍入模式为“四舍五入” getcontext().rounding ROUND_HALF_UP # 保留2位小数 price Decimal(10.005) print(price.quantize(Decimal(0.01))) # 10.01四舍五入这里有个特别值得关注的点quantize方法用于控制小数位数而舍入模式由上下文决定。默认的舍入模式是ROUND_HALF_EVEN银行家舍入跟内置round的行为一致。但在财务系统里中国习惯“四舍五入”所以你要手动改成ROUND_HALF_UP。我在做订单金额分摊时经常用decimal。比如一个订单总金额100元要分给三个商品每个商品33.33元剩余1分钱需要精确处理。用float算最后很可能出现99.99或100.01这种对不上的情况。用Decimal配合quantize每一步都精确可控最后能把每一分钱都对上账。5.4 float转Decimal的隐藏风险刚才说过不要从float创建Decimal但在实际项目中前端传过来的数据就可能是float。这时候你有个两步转化的办法from decimal import Decimal # 先将float转成字符串再转Decimal value 0.1 d Decimal(str(value)) print(d) # 0.1精确值通过str()中转一下可以消除float表示中多余的误差位。当然如果数据来源是字符串或者整数直接用字符串创建就行了完全绕开float。6. 顺带把fractions模块也讲清楚6.1 什么时候用分数计算fractions模块处理有理数运算。它跟decimal的区别在于decimal是十进制表示法虽然精确但仍然有精度上限而fractions用分子分母表示一个有理数任意精度的分数乘法、加法、比较都不丢失任何信息。from fractions import Fraction a Fraction(1, 3) # 1/3 b Fraction(2, 5) # 2/5 print(a b) # 11/15 print(a * b) # 2/15 print(a / b) # 5/6 print(float(a)) # 0.3333333333333333转成float会损失精度如果交给decimal1/3只能取一个近似值因为十进制下1/3是无限循环小数。但用fractions1/3就是精确的Fraction(1, 3)。什么时候用分数比如小朋友算数学题要求精确的分数结果或者你写算法涉及比率、比例中间过程用分数可以避免累积误差。6.2 从字符串和浮点数创建分数Fraction还可以从字符串和浮点数创建这个特性在解析配置数据时特别方便。from fractions import Fraction print(Fraction(3/7)) # 3/7从字符串解析 print(Fraction(0.25)) # 1/4从字符串解析 print(Fraction(0.25)) # 1/4从float解析注意它能正确约分 print(Fraction(1.5)) # 3/2注意从float创建Fraction时由于float本身是近似的结果可能是你意想不到的分数。比如Fraction(0.1)会得到一个巨大的分子分母打印出来是3602879701896397/36028797018963968。所以在实际项目里我几乎只用字符串或整数来创建分数从float创建太容易出幺蛾子。7. 四大模块组合使用的真实场景7.1 场景一商品价格统计与异常检测一个电商运营脚本需要计算一批商品价格的平均值、标准差再找出超过平均价2个标准差的“异常高价”商品。还要处理优惠券计算中的金额精度问题。这个场景里statistics负责统计decimal负责金额运算。from decimal import Decimal, getcontext, ROUND_HALF_UP import statistics prices [Decimal(19.90), Decimal(29.90), Decimal(49.90), Decimal(99.90), Decimal(199.00), Decimal(999.00)] # 统计指标计算 price_floats [float(p) for p in prices] mean_price statistics.mean(price_floats) stdev_price statistics.stdev(price_floats) # 异常检测均值±2倍标准差 threshold_high mean_price 2 * stdev_price threshold_low mean_price - 2 * stdev_price for p in prices: f float(p) if f threshold_high or f threshold_low: print(f异常价格: {p}) # 订单金额精确计算 getcontext().prec 10 getcontext().rounding ROUND_HALF_UP total sum(prices) discount Decimal(0.85) final_total (total * discount).quantize(Decimal(0.01)) print(f85折后总价: {final_total})这个脚本里有个关键点我先用float(p)把Decimal转成float给statistics用因为这个模块不支持Decimal直接计算。但最终金额运算又重新用回Decimal。混合使用的原则是统计计算可以允许微小精度误差金额计算必须精确。7.2 场景二模拟摇号系统一个小程序的抽奖模块需要从用户列表中抽取一等奖1名、二等奖3名、三等奖10名且不能重复。用random.sample实现无放回抽取用random.seed保证测试可复现。import random random.seed(20250101) # 固定种子便于测试 users [fuser_{i} for i in range(1, 101)] # 100个用户 first_prize random.sample(users, 1) remaining [u for u in users if u not in first_prize] second_prize random.sample(remaining, 3) remaining [u for u in remaining if u not in second_prize] third_prize random.sample(remaining, 10) print(f一等奖: {first_prize}) print(f二等奖: {second_prize}) print(f三等奖: {third_prize})模板写清楚了之后你可以把种子去掉就变成真正的随机抽奖。注意抽完一等奖后要从剩余用户里抽二等奖所以要用列表推导式把已经中奖的人排除掉。这是抽奖系统最容易踩坑的地方。7.3 场景三分数与概率计算一个增益计算系统玩家的暴击率是3/17装备加成是1/4两者叠加后暴击率怎么算用Fraction做精确计算最后再转成百分比字符串展示。from fractions import Fraction base_crit_rate Fraction(3, 17) equipment_bonus Fraction(1, 4) total_crit_rate base_crit_rate equipment_bonus print(total_crit_rate) # 29/68精确分数 print(float(total_crit_rate)) # 0.4264705882352941 print(f暴击率: {total_crit_rate.numerator / total_crit_rate.denominator * 100:.2f}%) # 42.65%如果你直接用Fraction(3, 17) 0.25Python会尝试把float转成Fraction误差很可能导致结果不是29/68而是个很诡异的分数。所以混合运算时建议先把float转成Fraction或者Decimal。实际操作中我一般全程用Fraction最后显示时才转成百分比字符串。8. 常见问题与排查技巧实录8.1 为什么math.pow和**运算符结果不一样math.pow(2, 3)返回的是8.0是浮点数而**运算符2 ** 3返回的是8是整数。如果你的代码后面要跟整数运算最好用**如果要做浮点运算用math.pow。另外math.pow会把两个参数都转成float所以math.pow(2, -1)结果是0.5而2 ** -1也是0.5这一步两者行为一致。真正需要注意的是大数运算时math.pow(10, 100)返回的是1e100是个float精度会丢失。而10 ** 100直接返回一个100位的整数精度完整。所以要做超大整数运算时一定别用math.pow。8.2 Quantize精度与舍入不一致的问题使用Decimal.quantize时如果你没有显式设置舍入模式它使用的是上下文getcontext().rounding的默认值也就是ROUND_HALF_EVEN。这会导致Decimal(2.5).quantize(Decimal(1))的结果是2而不是3。from decimal import Decimal, getcontext, ROUND_HALF_UP print(Decimal(2.5).quantize(Decimal(1))) # 2默认银行家舍入 getcontext().rounding ROUND_HALF_UP print(Decimal(2.5).quantize(Decimal(1))) # 3四舍五入这个问题的隐蔽性在于你本地测试时可能没注意舍入模式一上线到财务系统发现对不上账。我的建议是任何跟钱打交道的项目第一行就设置好rounding为ROUND_HALF_UP不要依赖默认值。8.3 random模块跟多线程的恩怨random模块不是线程安全的。多线程环境下多个线程同时调用random.randint或random.random可能导致生成随机数的行为异常虽然不至于崩溃但随机序列可能不符合预期。更可靠的做法是使用threading.local为每个线程维护独立的random实例或者使用底层不依赖全局状态的随机数生成器。import random import threading local threading.local() def get_random(): if not hasattr(local, rng): local.rng random.Random() # 每个线程独立的随机数生成器 return local.rng # 每个线程里使用 get_random().randint(1, 100)如果只是简单脚本不用太纠结多线程问题。真正的高并发随机数场景建议直接上numpy或者专门的随机数库。8.4 statistics模块遇到空列表statistics.mean([])会直接抛StatisticsError。有时候你的数据是动态采集的可能为空这时候要提前判断别让异常把整个脚本搞崩。import statistics data [] if data: print(statistics.mean(data)) else: print(数据为空无法计算均值)8.5 float和Decimal混用直接报错Decimal和float做运算Python会直接抛出TypeError。你写代码时要注意要么全转Decimal要么全转float不能混着来。from decimal import Decimal price Decimal(10.5) print(price 0.5) # TypeError: unsupported operand type(s) print(price Decimal(0.5)) # 11.0正确这个坑新手经常踩。记住Decimal是一个“霸道”类型它不允许跟float直接计算这是有意的设计——为了强制你显式处理精度问题。9. 我的个人经验总结说实话这几个模块我平时用得最多的是math和random这两个几乎是每天写脚本都会碰到的。statistics在分析日志数据、监控数据时经常用来快速算个平均值和波动范围。decimal和fractions用得少一些但只要涉及金额和精确保育场景我是一律不用float的。有一个经验想特别分享不要迷信numpy。网上很多教程一上来就让你装numpy、pandas但对于简单的数学运算标准库模块完全够用省去了环境配置和依赖管理的心智负担。我用statistics处理几十万条数据确实会慢一点但绝大多数场景根本到不了那个量级。另外一个习惯是每个项目一开始就明确哪些计算需要精确、哪些可以容忍误差然后选择对应的模块。比如写电商脚本金额相关的字段从输入到输出全程走Decimal写数据分析脚本全部用float反而更省事。这样代码结构清晰也不会出现因为数据精度问题导致的诡异bug。最后再说一个小技巧math模块很多函数都有对应的“近似版本”或者“优化版本”比如math.hypot计算直角三角形的斜边它比math.sqrt(x*x y*y)更稳定、更防溢出。类似的细节建议你没事翻一翻官方文档每次都能发现惊喜。这些标准库函数都是经过了大量优化的比自己手写要可靠得多。