专题:Python标准库精讲系统学习
关键词:Python, 标准库, fractions, 分数, 有理数, Fraction, 分数计算, 约分
一、fractions模块概述
fractions 模块是 Python 标准库中用于有理数算术运算的核心模块。它提供了一种精确表示分数的方式,完全避免了浮点数(float)固有的精度损失问题。在日常生活中,我们经常使用分数概念(如 1/3 杯水、3/4 英里),但浮点数无法精确表示这些值——0.1 在二进制中实际上是无限循环小数,而 Fraction 对象可以精确地表示 1/10。
fractions 模块的核心类是 Fraction,它基于有理数的数学定义:一个有理数可以表示为两个整数的比值 p/q(其中 q ≠ 0)。这意味着任何可以用分数形式表达的数字都可以被精确存储和计算。在金融计算、科学测量、教育资源、食谱比例等需要精确分数的场景中,fractions 模块有着不可替代的价值。
核心优势:Fraction 对象始终保持精确的有理数表示,不会出现浮点数的舍入误差。例如 1/3 + 1/6 在浮点数中会得到近似值 0.5(实际约等于 0.49999999999999994),而 Fraction 能精确得到 1/2。
适用场景一览
- 金融计算:利率按比例分摊、汇率换算,避免浮点舍入产生财务误差
- 教育工具:数学教育软件中分数的四则运算演示,确保结果精确
- 食谱与配比:按比例调整食材用量,保持配比精确
- 概率统计:概率值以分数形式(如 1/6)表示,更符合数学直觉
- 单位换算:英寸与英尺转换(12英寸=1英尺),天然适合分数表示
与 decimal 模块的关系
Python 标准库还提供了 decimal 模块用于十进制精确计算。两者的侧重点不同:decimal 适合十进制货币计算,精度由上下文设置;Fraction 则专注于有理数的精确比值运算,结果永远是约分后的最简分数。在实际开发中,可以根据需求选择——需要控制舍入策略时用 decimal,需要纯粹的有理数算术时用 Fraction。
二、Fraction对象的创建
Fraction 提供了多种构造方式,可以根据输入数据的形态选择最合适的创建方法。
2.1 从整数对创建
最常见的构造方式:传入分子(numerator)和分母(denominator),Fraction 会自动约分并处理符号。分母不能为 0,否则会抛出 ZeroDivisionError。
# 基本分数创建
from fractions import Fraction
f1 = Fraction(1, 3) # 1/3
f2 = Fraction(5, 10) # 自动约分为 1/2
f3 = Fraction(-4, 6) # 自动约分为 -2/3
f4 = Fraction(0, 7) # 分子为 0,结果为 0
f5 = Fraction(3, -5) # 符号归一化:-3/5
print(f1) # 输出: 1/3
print(f2) # 输出: 1/2
print(float(f2)) # 输出: 0.5
2.2 从字符串创建
字符串形式的分数:格式为 '分子/分母' 或纯整数字符串。这种方式特别适合解析用户输入或读取文本数据。
f1 = Fraction('3/7') # 字符串分数:3/7
f2 = Fraction('5') # 分子是 5,分母默认为 1
f3 = Fraction('-7/8') # 负分数:-7/8
f4 = Fraction(' 3 / 4 ') # 自动去除首尾空格,结果:3/4
# 错误写法(不会自动拆分字符串表达式)
# Fraction('3/7 + 1/7') # ValueError
2.3 从浮点数创建(含有限分母逼近)
直接从 float 创建 Fraction 可能会产生非常大的分母,因为 float 本质上是一个二进制近似值。这时可以使用 limit_denominator() 方法来找到最接近的"人可读"分数。
# 从浮点数直接创建
f1 = Fraction(0.75) # 精确表示为 3/4
print(f1) # 输出: 3/4
f2 = Fraction(0.1) # 输出: 3602879701896397/36028797018963968
print(f2) # 这是 0.1 在二进制下的精确表示,并非真正的 1/10
# limit_denominator() 方法:找到分母不超过指定值的最接近分数
f3 = Fraction(0.1).limit_denominator(10)
print(f3) # 输出: 1/10(分母最大为 10 时最接近 0.1 的分数)
# 常用分母限制值
Fraction(0.333).limit_denominator(1000) # 333/1000
Fraction(0.333).limit_denominator(10) # 1/3
最佳实践:优先使用整数对或字符串创建 Fraction,避免从 float 创建再转 Fraction 这种"先引入误差再还原"的曲折路径。实在需要从近似值出发时,配合 limit_denominator 使用。
2.4 从 Decimal 创建
Fraction 还可以和 decimal.Decimal 对象互转,这在金融计算链路中非常实用。
from decimal import Decimal
# Decimal 转 Fraction
d = Decimal('0.1')
f = Fraction(d)
print(f) # 输出: 1/10
# 对比:float 转 Fraction
print(Fraction(0.1)) # 输出: 3602879701896397/36028797018963968
三、算术运算与比较
Fraction 全面支持 Python 的数字运算符,所有运算结果都会自动约分为最简形式。这意味着你可以像操作普通数字一样对待 Fraction 对象。
3.1 基本算术运算
a = Fraction(1, 3)
b = Fraction(1, 6)
# 加法
print(a + b) # 输出: 1/2
print(a + 1) # 输出: 4/3(与整数混合运算)
# 减法
print(a - b) # 输出: 1/6
print(1 - a) # 输出: 2/3
# 乘法
print(a * b) # 输出: 1/18
print(a * 3) # 输出: 1(3 * 1/3 = 1)
# 除法
print(a / b) # 输出: 2(1/3 ÷ 1/6 = 2)
print(a / 2) # 输出: 1/6
# 整数除法
print(a // b) # 输出: 2
print(a % b) # 输出: 0(1/3 ÷ 1/6 余 0)
# 乘方
print(a ** 2) # 输出: 1/9
print(a ** -1) # 输出: 3(倒数)
print(a ** Fraction(1, 2)) # 输出近似值(分数指数可能产生无理数)
3.2 比较运算
Fraction 支持所有标准的比较运算符,并且可以与 int、float 等类型直接比较。比较操作是精确的,基于分数的数学值而不是近似值。
a = Fraction(1, 3)
b = Fraction(2, 6)
print(a == b) # 输出: True(自动约分后相等)
print(a == Fraction(1, 3)) # True
print(a < Fraction(1, 2)) # True
print(a > 0.3) # True(与 float 比较)
print(a != Fraction(1, 4)) # True
3.3 数学函数支持
Fraction 可以无缝配合 math 模块使用,但需要注意 math 函数通常会返回 float 类型(因为结果可能不是有理数)。
import math
f = Fraction(9, 4)
print(float(f)) # 2.25
print(math.sqrt(float(f))) # 1.5
print(abs(Fraction(-3, 5))) # 3/5
# math.gcd 可用于手动约分(虽然 Fraction 已自动完成)
print(math.gcd(12, 18)) # 6
四、分数化简与类型转换
Fraction 在创建时自动执行约分操作,确保分数始终处于最简形式。开发者也可以按需获取或转换成其他数值类型。
4.1 自动约分机制
Fraction 的内部实现使用最大公约数(GCD)算法,在初始化时自动约分。分子和分母始终保持互质(coprime),符号由分子承载。
f = Fraction(42, 56)
print(f) # 输出: 3/4(自动约分)
print(f.numerator) # 输出: 3(分子属性)
print(f.denominator) # 输出: 4(分母属性)
# 手动访问约分前后的值
from math import gcd
num, den = 42, 56
g = gcd(num, den)
print(f'{num//g}/{den//g}') # 输出: 3/4
4.2 as_integer_ratio() 方法
返回 (分子, 分母) 元组,与 float 的 as_integer_ratio 方法对应。这个方法在序列化和跨系统传输分数值时非常方便。
f = Fraction(7, 8)
ratio = f.as_integer_ratio()
print(ratio) # 输出: (7, 8)
# 可用于序列化后重建
num, den = ratio
reconstructed = Fraction(num, den)
print(reconstructed) # 输出: 7/8
4.3 转换为浮点数
使用 float() 将 Fraction 转换为浮点数。要注意的是,如果分数的分母不是 2 的幂,转换后可能会丢失精度。
f1 = Fraction(1, 4)
print(float(f1)) # 0.25(精确,因为 1/4 是 2 的幂)
f2 = Fraction(1, 3)
print(float(f2)) # 0.3333333333333333(近似值)
f3 = Fraction(355, 113)
print(float(f3)) # 3.1415929203539825(圆周率的分数近似)
4.4 与 Decimal 互转
在需要精确十进制结果的场景中,结合 Decimal 使用是最佳方案。
from decimal import Decimal, getcontext
# Fraction → Decimal
f = Fraction(1, 7)
d = Decimal(f.numerator) / Decimal(f.denominator)
print(d) # 输出: 0.1428571428571428571428571429(默认精度28位)
# 控制精度
getcontext().prec = 50
d_high = Decimal(f.numerator) / Decimal(f.denominator)
print(d_high) # 0.14285714285714285714285714285714285714285714285714
4.5 四舍五入与取整
Fraction 同样支持 round() 函数和 int() 类型转换。
f = Fraction(7, 3) # 7/3 ≈ 2.333...
print(round(f)) # 输出: 2(四舍五入取整)
print(round(f, 2)) # 输出: 2.33(保留两位小数,返回 float)
print(int(f)) # 输出: 2(截断取整)
print(f.numerator // f.denominator) # 输出: 2
五、实战应用
以下通过三个典型场景展示 Fraction 在实际开发中的应用价值。
5.1 场景一:比例缩放与配方调整
食谱需要按人数调整用量。假设一份蛋糕配方需要 2/3 杯糖和 1/4 杯牛奶,现在需要调整为 3 人份(原配方为 8 人份)。
from fractions import Fraction
# 原始配方(8人份)
original_sugar = Fraction(2, 3) # 2/3 杯糖
original_milk = Fraction(1, 4) # 1/4 杯牛奶
original_butter = Fraction(1, 2) # 1/2 杯黄油
# 缩放比例:3人份 / 8人份 = 3/8
scale = Fraction(3, 8)
# 调整后的用量
new_sugar = original_sugar * scale
new_milk = original_milk * scale
new_butter = original_butter * scale
print(f"糖:{new_sugar} 杯") # 1/4 杯
print(f"牛奶:{new_milk} 杯") # 3/32 杯
print(f"黄油:{new_butter} 杯") # 3/16 杯
# 汇总用量
total = new_sugar + new_milk + new_butter
print(f"总干湿料:{total} 杯 ≈ {float(total):.2f} 杯")
5.2 场景二:概率计算
计算抽牌概率:从一副标准扑克牌(52 张)中随机抽取一张,计算抽到特定牌的概率,并以分数形式精确呈现。
from fractions import Fraction
# 总牌数
total_cards = 52
# 抽到红桃的概率
hearts = 13
p_heart = Fraction(hearts, total_cards)
print(f"抽到红桃的概率:{p_heart} = {float(p_heart):.4f}")
# 抽到 A 的概率
aces = 4
p_ace = Fraction(aces, total_cards)
print(f"抽到 A 的概率:{p_ace} = {float(p_ace):.4f}")
# 抽到红桃 A 的概率
p_heart_ace = Fraction(1, total_cards)
print(f"抽到红桃 A 的概率:{p_heart_ace} = {float(p_heart_ace):.4f}")
# 抽到红桃或 A 的概率(容斥原理)
p_heart_or_ace = p_heart + p_ace - p_heart_ace
print(f"抽到红桃或 A 的概率:{p_heart_or_ace} = {float(p_heart_or_ace):.4f}")
# 连续两次抽到 A 的概率(无放回)
p_two_aces = Fraction(aces, total_cards) * Fraction(aces - 1, total_cards - 1)
print(f"连续两次抽到 A 的概率:{p_two_aces} ≈ {float(p_two_aces):.6f}")
5.3 场景三:单位转换与精确计算
长度单位之间的精确转换。例如,将英尺(foot)和英寸(inch)之间的值用分数表示,避免浮点数舍入。
# 英寸与英尺转换:1 英尺 = 12 英寸
inch_per_foot = Fraction(1, 12) # 1 英寸 = 1/12 英尺
# 5 英寸 7 英寸 = ? 英尺
height_inch = Fraction(67, 1) # 67 英寸
height_foot = height_inch * inch_per_foot
print(f"67 英寸 = {height_foot} 英尺 = {float(height_foot):.4f} 英尺")
# 输出: 67 英寸 = 67/12 英尺 = 5.5833 英尺
# 3 又 1/2 英寸 + 2 又 3/4 英寸
length1 = Fraction(7, 2) # 3.5 = 7/2
length2 = Fraction(11, 4) # 2.75 = 11/4
total_length = length1 + length2
print(f"总长度 = {total_length} 英寸 = {float(total_length)} 英寸")
# 输出: 总长度 = 25/4 英寸 = 6.25 英寸
六、核心总结
关键要点
- 精确有理数:Fraction 是 Python 中唯一能精确表示有理数的内置类型,适用于需要精确分数计算的场景
- 自动约分:创建 Fraction 时自动通过 GCD 约分至最简形式,分子分母始终保持互质
- 多种构造方式:支持从整数对、字符串、浮点数(配合 limit_denominator)、Decimal 等多种途径创建
- 完整运算符支持:加减乘除、幂运算、比较运算全部开箱即用,支持与 int 和 float 混合运算
- 避免 float 陷阱:在创建时优先使用整数对或字符串,避免从 float 创建带来二进制近似误差
常见误区
- 过度使用:Fraction 的计算速度慢于 float,在性能敏感的数值计算中请权衡精度与效率
- 误解自动约分:Fraction 会约分,但不会自动将假分数(如 7/3)转换为带分数(如 2 1/3)——需要手动实现
- 忽略序列化:Fraction 对象不能直接 JSON 序列化,需要借助 as_integer_ratio() 或自定义编码器
一句话记住:需要精确的有理数算术运算时用 Fraction——它给出了数学意义上的精确答案,而不是计算机意义上的近似答案。
速查表
| 操作/方法 |
说明 |
示例 |
| Fraction(a, b) |
创建分数 a/b,自动约分 |
Fraction(2, 4) → 1/2 |
| Fraction('a/b') |
从字符串创建分数 |
Fraction('3/8') → 3/8 |
| limit_denominator(n) |
找到分母不超过 n 的最接近分数 |
F(0.333).limit(10) → 1/3 |
| as_integer_ratio() |
返回 (分子, 分母) 元组 |
F(1,3).ratio() → (1,3) |
| numerator / denominator |
访问分子和分母属性 |
f.numerator, f.denominator |
| float(f) / int(f) |
转换为浮点数或整数 |
float(F(7,3)) → 2.333... |