用 Python 分数表示有理数
目录
fractions
Python 中的模块可以说是标准库中最未被充分利用的元素之一。尽管它可能并不广为人知,但它是一个有用的工具,因为它可以帮助解决二进制浮点运算的缺点。如果您计划处理财务数据或者您的计算需要无限精度,这非常重要。
在本教程即将结束时,您将看到一些动手示例,其中分数是最合适和最优雅的选择。您还将了解他们的弱点以及如何在此过程中充分利用它们。
在本教程中,您将学习如何:
- 在十进制和小数表示法之间转换
- 执行有理数算术
- 近似无理数
- 以无限精度精确表示分数
- 知道什么时候选择
Fraction
了Decimal
或float
本教程的大部分内容都介绍了该fractions
模块,除了了解其数字类型外,该模块本身不需要深入的 Python 知识。但是,如果您熟悉更高级的概念(例如 Python 的内置collections
模块、itertools
模块和生成器),那么您将处于一个很好的位置来完成后面的所有代码示例。如果您想充分利用本教程,您应该已经熟悉这些主题。
fractions
Python 中的模块可以说是标准库中最未被充分利用的元素之一。尽管它可能并不广为人知,但它是一个有用的工具,因为它可以帮助解决二进制浮点运算的缺点。如果您计划处理财务数据或者您的计算需要无限精度,这非常重要。
在本教程即将结束时,您将看到一些动手示例,其中分数是最合适和最优雅的选择。您还将了解他们的弱点以及如何在此过程中充分利用它们。
在本教程中,您将学习如何:
- 在十进制和小数表示法之间转换
- 执行有理数算术
- 近似无理数
- 以无限精度精确表示分数
- 知道什么时候选择
Fraction
了Decimal
或float
本教程的大部分内容都介绍了该fractions
模块,除了了解其数字类型外,该模块本身不需要深入的 Python 知识。但是,如果您熟悉更高级的概念(例如 Python 的内置collections
模块、itertools
模块和生成器),那么您将处于一个很好的位置来完成后面的所有代码示例。如果您想充分利用本教程,您应该已经熟悉这些主题。
十进制与分数表示法
让我们沿着记忆之路走一走,带回您对数字的学校知识并避免可能的混淆。这里有四个概念在起作用:
- 数学中的数字类型
- 数制
- 数字符号
- Python 中的数字数据类型
您现在将快速了解其中的每一个,以更好地理解Fraction
Python 中数据类型的用途。
数字分类
如果您不记得数字的分类,这里有一个快速复习:
数字类型
数学中有更多类型的数字,但这些是日常生活中最相关的数字。在最高层,你会发现复数包括虚和实数。反过来,实数又由有理数和无理数组成。最后,有理数包含整数和自然数。
数字系统和符号
几个世纪以来,出现了各种视觉表达数字的系统。今天,大多数人使用基于印度-阿拉伯符号的位置数字系统。您可以为此类系统选择任何基数或基数。然而,虽然人们更喜欢十进制系统(基数为 10),但计算机在二进制系统(基数为 2)中工作得最好。
在十进制系统本身中,您可以使用替代符号表示一些数字:
- 十进制: 0.75
- 分数: ¾
这些都不比另一个更好或更精确。用十进制表示数字可能更直观,因为它类似于百分比。比较小数也更直接,因为它们已经有一个共同的分母——系统的基数。最后,十进制数可以通过保留尾随零和前导零来传达精度。
另一方面,分数在手动执行符号代数时更方便,这就是它们主要用于学校的原因。但是您还记得上次使用分数是什么时候吗?如果你不能,那是因为十进制表示法是当今计算器和计算机的核心。
分数符号通常仅与有理数相关联。毕竟,有理数的定义表明,只要分母不为零,您就可以将其表示为两个整数的商或分数。但是,当您考虑可以近似无理数的无限连分数时,这并不是全部:
无理数总是有一个非终止和非重复的十进制扩展。例如,pi (π) 的十进制展开式永远不会用完似乎具有随机分布的数字。如果您要绘制它们的直方图,那么每个数字的频率将大致相似。
另一方面,大多数有理数都有终止的十进制扩展。但是,有些可以无限循环十进制扩展,在一段时间内重复一位或多位数字。重复的数字通常用十进制表示法中的省略号 (0.33333...) 表示。不管它们的十进制扩展如何,有理数(例如代表三分之一的数字)在分数表示法中总是看起来优雅而紧凑。
Python 中的数字数据类型
具有无限十进制扩展的数字在作为浮点数据类型存储在计算机内存中时会导致舍入错误,而计算机内存本身是有限的。更糟糕的是,通常不可能用二进制终止十进制扩展来精确表示数字!
这被称为浮点表示错误,它会影响所有编程语言,包括 Python。每个程序员迟早都会面临这个问题。例如,您不能float
在银行等应用程序中使用,在这些应用程序中,必须在不损失任何精度的情况下存储和处理数字。
PythonFraction
是解决这些障碍的方法之一。虽然它代表一个有理数,但这个名字Rational
已经代表了numbers
模块中的一个抽象基类。该numbers
模块定义了抽象数字数据类型的层次结构,以对数学中的数字分类进行建模:
Python 中数字的类型层次结构
Fraction
是 的直接且具体的子类Rational
,它提供了 Python 中对有理数的完整实现。积分类型像int
和bool
也派生自Rational
,但那些更具体。
有了这个理论背景,是时候创建你的第一个分数了!
从不同的数据类型创建 Python 分数
与int
or不同float
,分数不是 Python 中的内置数据类型,这意味着您必须从标准库中导入相应的模块才能使用它们。然而,一旦你通过了这额外的步骤,你会发现分数只是代表另一种数字类型,你可以在算术表达式中自由地与其他数字和数学运算符混合。
注:分数是在纯Python实现的并且是多比,可以直接在硬件上运行浮点数慢。在大多数需要大量计算的情况下,性能比精度更重要。另一方面,如果您同时需要性能和精度,那么可以考虑直接替换称为quicktions 的分数,这可能会在一定程度上提高您的执行速度。
在 Python 中有几种创建分数的方法,它们都涉及使用Fraction
类。这是您需要从fractions
模块导入的唯一内容。类构造函数接受零、一个或两个不同类型的参数:
>>> from fractions import Fraction
>>> print(Fraction())
0
>>> print(Fraction(0.75))
3/4
>>> print(Fraction(3, 4))
3/4
当您调用不带参数的类构造函数时,它会创建一个表示数字零的新分数。单参数风格尝试将另一种数据类型的值转换为分数。传入第二个参数使构造函数需要一个分子和一个分母,它们必须是Rational
类或其后代的实例。
请注意,您必须print()
用/
分子和分母之间的斜杠字符 ( ) 来表示分数以显示其人性化的文本表示。如果不这样做,它将退回到由一段 Python 代码组成的稍微更明确的字符串表示形式。在本教程后面,您将学习如何将分数转换为字符串。
有理数
当您Fraction()
使用两个参数调用构造函数时,它们都必须是有理数,例如整数或其他分数。如果分子或分母不是有理数,则您将无法创建新分数:
>>> Fraction(3, 4.0)
Traceback (most recent call last):
...
raise TypeError("both arguments should be "
TypeError: both arguments should be Rational instances
你得到一个TypeError
。尽管4.0
在数学中是有理数,但 Python 并不认为它是有理数。这是因为该值存储为浮点数据类型,该类型过于宽泛,可用于表示任何实数。
注意:浮点数据类型不能精确地将无理数存储在计算机的内存中,因为它们是非终止和非重复的十进制扩展。但实际上,这没什么大不了的,因为它们的近似值通常已经足够好了。唯一真正可靠的方法是需要对像 π 这样的传统符号使用符号计算。
同样,您不能创建分母为零的分数,因为这会导致被零除,这是未定义的,在数学中没有意义:
>>> Fraction(3, 0)
Traceback (most recent call last):
...
raise ZeroDivisionError('Fraction(%s, 0)' % numerator)
ZeroDivisionError: Fraction(3, 0)
Python 将ZeroDivisionError
. 但是,当您指定有效的分子和有效的分母时,只要它们具有公约数,它们就会自动为您标准化:
>>> Fraction(9, 12) # GCD(9, 12) = 3
Fraction(3, 4)
>>> Fraction(0, 12) # GCD(0, 12) = 12
Fraction(0, 1)
两个量级都被它们的最大公约数 (GCD)简化,GCD恰好分别为 3 和 12。当您定义负分数时,归一化也会考虑减号:
>>> -Fraction(9, 12)
Fraction(-3, 4)
>>> Fraction(-9, 12)
Fraction(-3, 4)
>>> Fraction(9, -12)
Fraction(-3, 4)
无论是在构造函数之前还是在任何一个参数之前放置减号,为了保持一致性,Python 总是将分数的符号与其分子相关联。目前有一种禁用此行为的方法,但它没有记录,将来可能会被删除。
您通常将分数定义为两个整数的商。每当你只提供一个整数,Python会变成这个数字为假分数假设分母为1
:
>>> Fraction(3)
Fraction(3, 1)
相反,如果您跳过两个参数,则分子将为0
:
>>> Fraction()
Fraction(0, 1)
不过,您不必总是为分子和分母提供整数。文档说明它们可以是任何有理数,包括其他分数:
>>> one_third = Fraction(1, 3)
>>> Fraction(one_third, 3)
Fraction(1, 9)
>>> Fraction(3, one_third)
Fraction(9, 1)
>>> Fraction(one_third, one_third)
Fraction(1, 1)
在每种情况下,您都会得到一个分数作为结果,即使它们有时表示像 9 和 1 这样的整数值。稍后,您将看到如何将分数转换为其他数据类型。
如果你给Fraction
构造函数一个恰好也是一个分数的参数会发生什么?试试这个代码找出:
>>> Fraction(one_third) == one_third
True
>>> Fraction(one_third) is one_third
False
您得到相同的值,但它是输入分数的不同副本。那是因为调用构造函数总是会产生一个新实例,这与分数是不可变的这一事实相吻合,就像 Python 中的其他数字类型一样。
浮点数和十进制数
到目前为止,您只使用了有理数来创建分数。毕竟,Fraction
构造函数的双参数版本要求两个数字都是Rational
实例。但是,单参数构造函数不是这种情况,它很乐意接受任何实数,甚至是非数字值,例如字符串。
Python 中实数数据类型的两个主要示例是float
和decimal.Decimal
。虽然只有后者才能准确表示有理数,但两者都可以很好地近似无理数。与此相关,如果您想知道,在这方面Fraction
与Decimal
此类似,因为它是Real
.
是Decimal
实数吗?显示隐藏
在 Python 3.2 之前,您只能使用 the.from_float()
和.from_decimal()
class 方法从实数创建分数。虽然没有被弃用,但它们今天是多余的,因为Fraction
构造函数可以直接将两种数据类型作为参数:
>>> from decimal import Decimal
>>> Fraction(0.75) == Fraction(Decimal("0.75"))
True
无论是Fraction
从对象float
还是Decimal
对象创建对象,它们的值都是相同的。您之前已经看到过从浮点值创建的分数:
>>> print(Fraction(0.75))
3/4
结果是用小数表示法表示的相同数字。但是,此代码仅出于巧合而按预期工作。在大多数情况下,由于影响float
数字的表示错误,您将无法获得预期值,无论它们是否有理:
>>> print(Fraction(0.1))
3602879701896397/36028797018963968
哇!这里发生了什么?
让我们用慢动作分解它。前面的数字可以表示为 0.75 或 3/4,也可以表示为 1/2 和 1/4 的和,它们是 2 的负幂,具有精确的二进制表示。另一方面,数字 ⅒ 只能用二进制数字的非终止重复扩展来近似:
由于有限的内存,二进制字符串最终必须结束,所以它的尾部被四舍五入。默认情况下,Python 只显示 中定义的最重要的数字sys.float_info.dig
,但如果你想,你可以用任意数量的数字格式化一个浮点数:
>>> str(0.1)
'0.1'
>>> format(0.1, ".17f")
'0.10000000000000001'
>>> format(0.1, ".18f")
'0.100000000000000006'
>>> format(0.1, ".19f")
'0.1000000000000000056'
>>> format(0.1, ".55f")
'0.1000000000000000055511151231257827021181583404541015625'
当您将 afloat
或Decimal
数字传递给Fraction
构造函数时,它会调用它们的.as_integer_ratio()
方法来获取两个不可约整数的元组,其比率给出与输入参数完全相同的十进制展开。然后将这两个数字分配给新分数的分子和分母。
注意:从 Python 3.8 开始,Fraction
还实现了.as_integer_ratio()
对其他数字类型的补充。例如,它可以帮助您将分数转换为元组。
现在,您可以将这两个大数字的来源拼凑起来:
>>> Fraction(0.1)
Fraction(3602879701896397, 36028797018963968)
>>> (0.1).as_integer_ratio()
(3602879701896397, 36028797018963968)
如果你拿出你的袖珍计算器并输入这些数字,那么除法的结果就是 0.1。但是,如果您要手动分割它们或使用WolframAlpha 之类的工具,那么您最终会得到之前看到的小数点后 55 位。
有一种方法可以找到具有更多实际值的分数的近似值。.limit_denominator()
例如,您可以使用,您将在本教程后面了解更多信息:
>>> one_tenth = Fraction(0.1)
>>> one_tenth
Fraction(3602879701896397, 36028797018963968)
>>> one_tenth.limit_denominator()
Fraction(1, 10)
>>> one_tenth.limit_denominator(max_denominator=int(1e16))
Fraction(1000000000000000, 9999999999999999)
但是,这可能并不总是为您提供最佳近似值。最重要的是,您永远不应该尝试直接从实数创建分数,例如float
如果您想避免可能出现的舍入错误。Decimal
如果你不够小心,即使是班级也可能会受到影响。
不管怎样,分数让你用它们的构造函数中的字符串最准确地传达十进制符号。
字符串
Fraction
构造函数接受两种字符串格式,分别对应于十进制和小数表示法:
>>> Fraction("0.1")
Fraction(1, 10)
>>> Fraction("1/10")
Fraction(1, 10)
两种记数法都可以选择有加号 ( +
) 或减号 ( -
),而小数点还可以包括指数,以防您想使用科学记数法:
>>> Fraction("-2e-3")
Fraction(-1, 500)
>>> Fraction("+2/1000")
Fraction(1, 500)
两种结果的唯一区别是一种是负的,一种是正的。
使用小数表示法时,不能在斜杠字符 ( /
)周围使用空格字符:
>>> Fraction("1 / 10")
Traceback (most recent call last):
...
raise ValueError('Invalid literal for Fraction: %r' %
ValueError: Invalid literal for Fraction: '1 / 10'
要找出到底哪些字符串是有效还是无效,你可以探索的正则表达式的模块的源代码。请记住从字符串或正确实例化的Decimal
对象而不是float
值创建分数,以便您可以保持最大精度。
既然您已经创建了几个分数,您可能想知道除了将两个数字组合在一起之外,它们还能为您做什么。这是一个很好的问题!
检查 Python 分数
所述Rational
抽象基类定义了两个只读属性,用于访问一个分数的分子和分母:
>>> from fractions import Fraction
>>> half = Fraction(1, 2)
>>> half.numerator
1
>>> half.denominator
2
由于分数是不可变的,你不能改变它们的内部状态:
>>> half.numerator = 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
如果您尝试为分数的一个属性分配一个新值,则会出现错误。事实上,只要您想修改一个分数,就必须创建一个新分数。例如,要反转您的分数,您可以调用.as_integer_ratio()
获取一个元组,然后使用切片语法反转其元素:
>>> Fraction(*half.as_integer_ratio()[::-1])
Fraction(2, 1)
一元星号运算符 ( *
)解包您的反向元组并将其元素传递给Fraction
构造函数。
每个分数附带的另一种有用方法可让您找到与十进制表示法中给出的数字最接近的有理数近似值。这就是.limit_denominator()
您在本教程前面已经提到的方法。您可以选择为您的近似值请求最大分母:
>>> pi = Fraction("3.141592653589793")
>>> pi
Fraction(3141592653589793, 1000000000000000)
>>> pi.limit_denominator(20_000)
Fraction(62813, 19994)
>>> pi.limit_denominator(100)
Fraction(311, 99)
>>> pi.limit_denominator(10)
Fraction(22, 7)
初始近似值可能不是最方便使用的,但它是最忠实的。此方法还可以帮助您恢复存储为浮点数据类型的有理数。请记住float
,即使它们具有终止的十进制扩展,也可能无法准确表示所有有理数:
>>> pi = Fraction(3.141592653589793)
>>> pi
Fraction(884279719003555, 281474976710656)
>>> pi.limit_denominator()
Fraction(3126535, 995207)
>>> pi.limit_denominator(10)
Fraction(22, 7)
与前面的代码块相比,您会注意到突出显示的行上的结果不同,即使该float
实例看起来与您之前传递给构造函数的字符串文字相同!稍后,您将探索一个.limit_denominator()
用于查找无理数近似值的示例。
将 Python 分数转换为其他数据类型
您已经学习了如何从以下数据类型创建分数:
str
int
float
decimal.Decimal
fractions.Fraction
相反的呢?如何将Fraction
实例转换回这些类型?您将在本节中找到。
浮点数和整数
Python 中本机数据类型之间的转换通常涉及调用内置函数之一,例如int()
或float()
在对象上。只要对象实现了相应的特殊方法,例如.__int__()
或 ,这些转换就会起作用.__float__()
。分数恰好从Rational
抽象基类继承了后者:
>>> from fractions import Fraction
>>> three_quarters = Fraction(3, 4)
>>> float(three_quarters)
0.75
>>> three_quarters.__float__() # Don't call special methods directly
0.75
>>> three_quarters.__int__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Fraction' object has no attribute '__int__'
您不应该直接在对象上调用特殊方法,但它有助于演示目的。在这里,您会注意到分数仅实现.__float__()
而不是.__int__()
.
当您研究源代码时,您会注意到该.__float__()
方法方便地将分数的分子除以其分母得到一个浮点数:
>>> three_quarters.numerator / three_quarters.denominator
0.75
请记住,将Fraction
实例转换为float
实例可能会导致有损转换,这意味着您最终可能会得到一个稍微偏离的数字:
>>> float(Fraction(3, 4)) == Fraction(3, 4)
True
>>> float(Fraction(1, 3)) == Fraction(1, 3)
False
>>> float(Fraction(1, 10)) == Fraction(1, 10)
False
尽管分数不提供整数转换的实现,但所有实数都可以被截断,这是int()
函数的后备:
>>> fraction = Fraction(14, 5)
>>> int(fraction)
2
>>> import math
>>> math.trunc(fraction)
2
>>> fraction.__trunc__() # Don't call special methods directly
2
稍后您将在有关舍入分数的部分中发现其他一些相关方法。
十进制数
如果您尝试Decimal
从Fraction
实例创建数字,那么您很快就会发现这种直接转换是不可能的:
>>> from decimal import Decimal
>>> Decimal(Fraction(3, 4))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: conversion from Fraction to Decimal is not supported
当你尝试时,你会得到一个TypeError
. 但是,由于分数代表除法,您可以通过仅用其中一个数字包裹Decimal
并手动除法来绕过该限制:
>>> fraction = Fraction(3, 4)
>>> fraction.numerator / Decimal(fraction.denominator)
Decimal('0.75')
与 不同float
但相似Fraction
,Decimal
数据类型没有浮点表示错误。所以,当你转换一个不能用二进制浮点精确表示的有理数时,你将保留数字的精度:
>>> fraction = Fraction(1, 10)
>>> decimal = fraction.numerator / Decimal(fraction.denominator)
>>> fraction == decimal
True
>>> fraction == 0.1
False
>>> decimal == 0.1
False
同时,不终止重复十进制扩展的有理数在从分数转换为十进制表示法时会导致精度损失:
>>> fraction = Fraction(1, 3)
>>> decimal = fraction.numerator / Decimal(fraction.denominator)
>>> fraction == decimal
False
>>> decimal
Decimal('0.3333333333333333333333333333')
那是因为在三分之一或 的十进制展开中有无数个三Fraction(1, 3)
,而该Decimal
类型具有固定精度。默认情况下,它只存储二十八位小数。如果你愿意,你可以调整它,但它仍然是有限的。
字符串
分数的字符串表示使用熟悉的分数表示法显示它们的值,而它们的规范表示输出一段 Python 代码,其中包含对Fraction
构造函数的调用:
>>> one_third = Fraction(1, 3)
>>> str(one_third)
'1/3'
>>> repr(one_third)
'Fraction(1, 3)'
无论使用str()
或repr()
,结果都是一个字符串,但它们的内容不同。
与其他数字类型不同,分数在 Python 中不支持字符串格式:
>>> from decimal import Decimal
>>> format(Decimal("0.3333333333333333333333333333"), ".2f")
'0.33'
>>> format(Fraction(1, 3), ".2f")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported format string passed to Fraction.__format__
如果你尝试,那么你会得到一个TypeError
. Fraction
例如,如果您想引用字符串模板中的实例来填充占位符,这可能是一个问题。另一方面,您可以通过将分数转换为浮点数来快速解决这个问题,尤其是在这种情况下您不需要关心精度。
如果您在Jupyter Notebook 中工作,那么您可能希望根据您的分数而不是它们的常规文本表示来呈现LaTeX公式。要做到这一点,必须猴补丁的Fraction
加入了新的方法,数据类型._repr_pretty_()
,其中Jupyter笔记本承认:
from fractions import Fraction
from IPython.display import display, Math
Fraction._repr_pretty_ = lambda self, *args: \
display(Math(rf"$$\frac{{{self.numerator}}}{{{self.denominator}}}"))
它将一段 LaTeX 标记包装在一个Math
对象中,并将其发送到笔记本的富显示器,它可以使用MathJax库呈现标记:
下次评估包含Fraction
实例的笔记本单元格时,它将绘制漂亮的数学公式,而不是打印文本。
对分数执行有理数算术
如前所述,您可以在由其他数字类型组成的算术表达式中使用分数。分数将与大多数数字类型互操作,除了decimal.Decimal
,它有自己的一套规则。此外,另一个操作数的数据类型,无论它位于分数的左侧还是右侧,都将决定算术运算结果的类型。
添加
您可以添加两个或多个分数,而无需考虑将它们减少到它们的公分母:
>>> from fractions import Fraction
>>> Fraction(1, 2) + Fraction(2, 3) + Fraction(3, 4)
Fraction(23, 12)
结果是一个新的分数,它是所有输入分数的总和。当您将整数和分数相加时,也会发生同样的情况:
>>> Fraction(1, 2) + 3
Fraction(7, 2)
但是,一旦您开始将分数与非有理数(即不是其子类的数字)混合,那么numbers.Rational
您的分数将在添加之前首先转换为该类型:
>>> Fraction(3, 10) + 0.1
0.4
>>> float(Fraction(3, 10)) + 0.1
0.4
无论您是否明确使用float()
. 该转换可能会导致精度损失,因为您的分数以及结果现在都以浮点表示形式存储。尽管数字 0.4 看起来是正确的,但它并不完全等于分数 4/10。
减法
减去分数与添加分数没有什么不同。Python 会为你找到共同点:
>>> Fraction(3, 4) - Fraction(2, 3) - Fraction(1, 2)
Fraction(-5, 12)
>>> Fraction(4, 10) - 0.1
0.30000000000000004
这一次,精度损失如此之大,一目了然。请注意在4
十进制扩展末尾的一长串零后跟一个数字。这是四舍五入一个值的结果,否则将需要无限数量的二进制数字。
乘法
当您将两个分数相乘时,它们的分子和分母会按元素相乘,如果需要,所得分数会自动减少:
>>> Fraction(1, 4) * Fraction(3, 2)
Fraction(3, 8)
>>> Fraction(1, 4) * Fraction(4, 5) # The result is 4/20
Fraction(1, 5)
>>> Fraction(1, 4) * 3
Fraction(3, 4)
>>> Fraction(1, 4) * 3.0
0.75
同样,根据另一个操作数的类型,您最终会在结果中得到不同的数据类型。
分配
Python 中有两种除法运算符,分数支持这两种运算符:
- 真分裂:
/
- 楼层划分:
//
真正的除法产生另一个分数,而地板除法总是返回一个整数,小数部分被截断:
>>> Fraction(7, 2) / Fraction(2, 3)
Fraction(21, 4)
>>> Fraction(7, 2) // Fraction(2, 3)
5
>>> Fraction(7, 2) / 2
Fraction(7, 4)
>>> Fraction(7, 2) // 2
1
>>> Fraction(7, 2) / 2.0
1.75
>>> Fraction(7, 2) // 2.0
1.0
请注意,地板除法的结果并不总是整数!结果可能最终float
取决于您与分数一起使用的数据类型。分数还支持模运算符 ( %
)以及divmod()
函数,这可能有助于从不正确的分数创建混合分数:
>>> def mixed(fraction):
... floor, rest = divmod(fraction.numerator, fraction.denominator)
... return f"{floor} and {Fraction(rest, fraction.denominator)}"
...
>>> mixed(Fraction(22, 7))
'3 and 1/7'
您可以更新函数以返回由整个部分和小数余数组成的元组,而不是像上面的输出那样生成字符串。继续尝试修改函数的返回值以查看差异。
求幂
您可以使用二元求幂运算符 ( **
)或内置pow()
函数将分数求幂。您也可以使用分数本身作为指数。现在回到你的Python 解释器,开始探索如何将分数提升为幂:
>>> Fraction(3, 4) ** 2
Fraction(9, 16)
>>> Fraction(3, 4) ** (-2)
Fraction(16, 9)
>>> Fraction(3, 4) ** 2.0
0.5625
您会注意到,您可以同时使用正指数值和负指数值。当指数不是Rational
数字时,您的分数会float
在继续之前自动转换为。
当指数是一个Fraction
实例时,事情会变得更加复杂。由于分数幂通常会产生无理数,float
除非底数和指数都是整数,否则两个操作数都将转换为:
>>> 2 ** Fraction(2, 1)
4
>>> 2.0 ** Fraction(2, 1)
4.0
>>> Fraction(3, 4) ** Fraction(1, 2)
0.8660254037844386
>>> Fraction(3, 4) ** Fraction(2, 1)
Fraction(9, 16)
只有当指数的分母等于 1 并且您正在提高一个Fraction
实例时,才会得到分数。
舍入 Python 分数
Python 中有许多四舍五入的策略,数学中还有更多。您可以对分数和十进制数使用相同的一组内置全局和模块级函数。他们会让你为一个分数分配一个整数,或者创建一个与更少小数位相对应的新分数。
当您将分数转换为 a 时int
,您已经了解了一种粗略的舍入方法,它会截断小数部分,只留下整个部分(如果有):
>>> from fractions import Fraction
>>> int(Fraction(22, 7))
3
>>> import math
>>> math.trunc(Fraction(22, 7))
3
>>> math.trunc(-Fraction(22, 7))
-3
在这种情况下,调用int()
等效于调用math.trunc()
,它将正分数向下舍入,负分数向上舍入。这两个操作分别称为floor和天花板。如果你想,你可以直接使用两者:
>>> math.floor(-Fraction(22, 7))
-4
>>> math.floor(Fraction(22, 7))
3
>>> math.ceil(-Fraction(22, 7))
-3
>>> math.ceil(Fraction(22, 7))
4
比较的结果math.floor()
,并math.ceil()
与同期的来电math.trunc()
。每个函数都有不同的舍入偏差,这可能会影响舍入数据集的统计属性。幸运的是,有一种称为四舍五入的策略,它比截断、下限或上限更不偏向。
本质上,它会将您的分数四舍五入到最接近的整数,同时为等距的一半选择最接近的偶数。您可以致电round()
以利用此策略:
>>> round(Fraction(3, 2)) # 1.5
2
>>> round(Fraction(5, 2)) # 2.5
2
>>> round(Fraction(7, 2)) # 3.5
4
请注意这些分数是如何根据最接近的偶数的位置向上或向下舍入的?自然,这条规则仅适用于到左边最近整数的距离与右边相同的情况。否则,舍入方向基于到整数的最短距离,无论它是否为偶数。
您可以选择为该round()
函数提供第二个参数,该参数指示您要保留的小数位数。当你这样做时,你总是会得到一个Fraction
而不是一个整数,即使你请求零位:
>>> fraction = Fraction(22, 7) # 3.142857142857143
>>> round(fraction, 0)
Fraction(3, 1)
>>> round(fraction, 1) # 3.1
Fraction(31, 10)
>>> round(fraction, 2) # 3.14
Fraction(157, 50)
>>> round(fraction, 3) # 3.143
Fraction(3143, 1000)
但是,请注意调用round(fraction)
和之间的区别round(fraction, 0)
,它产生相同的值但使用不同的数据类型:
>>> round(fraction)
3
>>> round(fraction, 0)
Fraction(3, 1)
当您省略第二个参数时,round()
将返回最接近的整数。否则,您将得到一个减少的分数,其分母最初是与您要求的十进制数相对应的十的幂。
在 Python 中比较分数
在现实生活中,比较以分数表示的数字可能比比较以十进制表示的数字更困难,因为分数表示法由两个值组成,而不仅仅是一个。为了理解这些数字,您通常将它们简化为一个公分母并仅比较它们的分子。例如,尝试根据它们的值按升序排列以下分数:
- 2/3
- 5/8
- 8/13
它不如十进制表示法方便。混合符号会使情况变得更糟。然而,当你用一个公分母重写这些分数时,对它们进行排序就变得很简单了:
- 208/312
- 195/312
- 192/312
3、8 和 13 的最大公约数是 1。这意味着所有三个分数的最小公分母是它们的乘积 312。一旦您将所有分数转换为使用它们的最小公分母,您就可以忽略分母并专注于比较分子。
在 Python 中,当您比较和排序Fraction
对象时,这在幕后工作:
>>> from fractions import Fraction
>>> Fraction(8, 13) < Fraction(5, 8)
True
>>> sorted([Fraction(2, 3), Fraction(5, 8), Fraction(8, 13)])
[Fraction(8, 13), Fraction(5, 8), Fraction(2, 3)]
Python 可以Fraction
使用内置sorted()
函数快速对对象进行排序。有用的是,所有比较运算符都按预期工作。您甚至可以将它们用于其他数字类型,复数除外:
>>> Fraction(2, 3) < 0.625
False
>>> from decimal import Decimal
>>> Fraction(2, 3) < Decimal("0.625")
False
>>> Fraction(2, 3) < 3 + 2j
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'Fraction' and 'complex'
比较运算符处理浮点数和小数,但是当您尝试使用复数时会出错3 + 2j
。这与复数不定义自然排序关系的事实有关,因此您无法将它们与任何东西进行比较——包括分数。
选择之间Fraction
,Decimal
和Float
如果你需要选择的只有一件事,从阅读本教程记住,那么就应该是当选择Fraction
了Decimal
和float
。所有这些数字类型都有其用例,因此最好了解它们的优点和缺点。在本节中,您将简要了解如何在这三种数据类型中的每一种中表示数字。
二进制浮点: float
float
在大多数情况下,数据类型应该是表示实数的默认选择。例如,它适用于执行速度比精度更重要的科学、工程和计算机图形学。几乎没有任何程序需要比浮点数更高的精度。
注意:如果您只需要使用整数,那么使用int
速度和内存效率更高的数据类型。
浮点运算的无与伦比的速度源于它在硬件而不是软件中的实现。几乎所有数学协处理器都符合IEEE 754标准,该标准描述了如何以二进制浮点数表示数字。正如您所猜测的那样,使用二进制系统的缺点是臭名昭著的表示错误。
但是,除非您有特定的理由使用不同的数字类型,否则您应该坚持使用float
或int
在可能的情况下使用。
十进制浮点和定点: Decimal
有时使用二进制系统不能为实数提供足够的精度。一个值得注意的例子是财务计算,它涉及同时处理非常大和非常小的数字。他们还倾向于一遍又一遍地重复相同的算术运算,这可能会累积显着的舍入误差。
您可以使用十进制浮点算法存储实数来缓解这些问题并消除二进制表示错误。它类似于float
移动小数点以适应更大或更小的幅度。但是,它以十进制而不是二进制运行。
另一种提高数值精度的策略是定点算法,它为十进制扩展分配特定数量的数字。例如,最多四位小数的精度需要将分数存储为按 10,000 倍放大的整数。为了恢复原始馏分,它们将相应地按比例缩小。
Python 的decimal.Decimal
数据类型在底层是十进制浮点和定点表示的混合。它还遵循以下两个标准:
- 通用十进制算术规范 ( IBM )
- 基数无关的浮点运算 ( IEEE 854-1987 )
它们是在软件而不是硬件中模拟的,这使得这种数据类型在时间和空间方面的效率远低于float
. 另一方面,它可以表示具有任意但有限精度的数字,您可以自由调整。请注意,如果算术运算超过最大小数位数,您仍可能面临舍入错误。
但是,今天固定精度提供的安全缓冲区明天可能会变得不足。考虑恶性通货膨胀或处理汇率差异很大的多种货币,例如比特币 (0.000029 BTC) 和伊朗里亚尔 (42,105.00 IRR)。如果您想要无限精度,请使用Fraction
.
无限精度有理数: Fraction
无论是Fraction
和Decimal
类型共享一些相似之处。它们解决了二进制表示错误,它们是在软件中实现的,您可以将它们用于货币应用程序。尽管如此,分数的主要用途是表示有理数,因此与小数相比,它们在存储货币方面可能不太方便。
注意:虽然Fraction
数据类型是在纯 Python 中实现的,但大多数 Python 发行版都为该Decimal
类型提供了一个编译的动态链接库。如果它不适用于您的平台,那么 Python 也会回退到纯 Python 实现。但是,即使是编译版本也不会像以前那样充分利用硬件float
。
使用Fraction
over有两个优点Decimal
。第一个是无限精度,仅受可用内存的限制。这使您可以使用非终止和重复的十进制扩展来表示有理数,而不会丢失任何信息:
>>> from fractions import Fraction
>>> one_third = Fraction(1, 3)
>>> print(3 * one_third)
1
>>> from decimal import Decimal
>>> one_third = 1 / Decimal(3)
>>> print(3 * one_third)
0.9999999999999999999999999999
将 1/3 乘以 3 可以得到小数形式中的 1,但结果以十进制形式四舍五入。它有二十八位小数,这是该Decimal
类型的默认精度。
再看看分数的另一个好处,你之前已经开始学习了。与 不同Decimal
,分数可以与二进制浮点数互操作:
>>> Fraction("0.75") - 0.25
0.5
>>> Decimal("0.75") - 0.25
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'decimal.Decimal' and 'float'
当您将分数与浮点数混合时,您会得到一个浮点数。另一方面,如果您尝试将分数与Decimal
数据类型混合使用,则会遇到TypeError
.
在行动中研究 Python 分数
在本节中,您将通过一些有趣且实用的示例Fraction
在 Python中使用数据类型。您可能会惊讶于分数有多么方便,同时它们又被低估了多少。准备好潜水吧!
逼近无理数
无理数在数学中扮演着重要的角色,这就是它们与许多子领域(如算术、微积分和几何)相切的原因。您之前可能听说过的一些最著名的有:
- 二的平方根 (√2)
- 阿基米德常数 (π)
- 黄金比例(φ)
- 欧拉数 ( e )
在数学史上,pi (π) 一直特别有趣,这导致了许多尝试为其找到准确的近似值。
虽然古代哲学家必须竭尽全力,但今天您可以使用 Python 使用蒙特卡罗方法(例如布冯针法或类似方法)找到非常好的 pi 估计值。然而,在大多数日常问题中,只有一个方便分数形式的粗略近似就足够了。以下是如何确定两个整数的商,从而逐渐更好地逼近无理数:
from fractions import Fraction
from itertools import count
def approximate(number):
history = set()
for max_denominator in count(1):
fraction = Fraction(number).limit_denominator(max_denominator)
if fraction not in history:
history.add(fraction)
yield fraction
该函数接受一个无理数,将其转换为分数,然后找到一个小数位数较少的不同分数。在Python的设置防止通过保持历史数据生成重复值,以及itertools
模块的count()
迭代器计数到无穷远。
您现在可以使用此函数来查找 pi 的前十个分数近似值:
>>> from itertools import islice
>>> import math
>>> for fraction in islice(approximate(math.pi), 10):
... print(f"{str(fraction):>7}", "→", float(fraction))
...
3 → 3.0
13/4 → 3.25
16/5 → 3.2
19/6 → 3.1666666666666665
22/7 → 3.142857142857143
179/57 → 3.1403508771929824
201/64 → 3.140625
223/71 → 3.140845070422535
245/78 → 3.141025641025641
267/85 → 3.1411764705882352
好的!有理数 22/7 已经很接近了,这表明 pi 可以在早期近似,毕竟不是特别无理。该islice()
迭代接收请求的10个值之前停止无限迭代。继续并通过增加结果数量或寻找其他无理数的近似值来玩这个例子。
获取显示器的纵横比
图像或显示器的纵横比是其宽度与高度的商,可以方便地表示比例。它通常用于电影和数字媒体,而电影导演喜欢利用纵横比作为一种艺术衡量标准。如果您一直在寻找新的智能手机,那么规格可能会提到屏幕比例,例如 16:9。
您可以通过使用官方 Python 发行版附带的Tkinter测量其宽度和高度来找出计算机显示器的纵横比:
>>> import tkinter as tk
>>> window = tk.Tk()
>>> window.winfo_screenwidth()
2560
>>> window.winfo_screenheight()
1440
请注意,如果您连接了多个显示器,则此代码可能无法按预期工作。
计算纵横比是创建一个会减少自身的分数的问题:
>>> from fractions import Fraction
>>> Fraction(2560, 1440)
Fraction(16, 9)
你去吧。显示器的分辨率为 16:9。但是,如果您使用的是屏幕尺寸较小的笔记本电脑,那么您的分数一开始可能无法计算,您需要相应地限制其分母:
>>> Fraction(1360, 768)
Fraction(85, 48)
>>> Fraction(1360, 768).limit_denominator(10)
Fraction(16, 9)
请记住,如果您正在处理移动设备的垂直屏幕,则应交换尺寸,使第一个尺寸大于下一个尺寸。您可以将此逻辑封装在一个可重用的函数中:
from fractions import Fraction
def aspect_ratio(width, height, max_denominator=10):
if height > width:
width, height = height, width
ratio = Fraction(width, height).limit_denominator(max_denominator)
return f"{ratio.numerator}:{ratio.denominator}"
无论参数顺序如何,这都将确保一致的纵横比:
>>> aspect_ratio(1080, 2400)
'20:9'
>>> aspect_ratio(2400, 1080)
'20:9'
无论您是查看水平屏幕还是垂直屏幕的测量值,纵横比都是相同的。
到目前为止,宽度和高度都是整数,但是小数值呢?例如,某些佳能相机具有 APS-C 裁切传感器,其尺寸为 22.8 毫米 x 14.8 毫米。分数会阻塞浮点数和十进制数,但您可以将它们转换为有理数近似值:
>>> aspect_ratio(22.2, 14.8)
Traceback (most recent call last):
...
raise TypeError("both arguments should be "
TypeError: both arguments should be Rational instances
>>> aspect_ratio(Fraction("22.2"), Fraction("14.8"))
'3:2'
在这种情况下,纵横比正好是 1.5 或 3:2,但许多相机使用稍长的传感器宽度,其比例为 1.555…或 14:9。当您进行数学运算时,您会发现它是宽幅 (16:9)和四分之三制 (4:3)的算术平均值,这是一种折衷,可以让您以可接受的方式显示图片这两种流行的格式。
计算照片的曝光值
在数字图像中嵌入元数据的标准格式Exif(可交换图像文件格式)使用比率来存储多个值。一些最重要的比率描述了照片的曝光度:
- 光圈值
- 接触时间
- 曝光偏差
- 焦距
- 光圈
- 快门速度
快门速度通俗地与曝光时间同义,但它使用基于对数刻度的APEX 系统作为元数据中的一小部分存储。这意味着相机会取曝光时间的倒数,然后计算它的对数以 2 为底。因此,例如,第二个曝光时间的 1/200 将作为 7643856/1000000 写入文件。计算方法如下:
>>> from fractions import Fraction
>>> exposure_time = Fraction(1, 200)
>>> from math import log2, trunc
>>> precision = 1_000_000
>>> trunc(log2(Fraction(1, exposure_time)) * precision)
7643856
如果您在没有任何外部库帮助的情况下手动读取此元数据,您可以使用 Python 分数来恢复原始曝光时间:
>>> shutter_speed = Fraction(7643856, 1_000_000)
>>> Fraction(1, round(2 ** shutter_speed))
Fraction(1, 200)
当您将拼图的各个部分(即光圈、快门速度和 ISO 速度)组合在一起时,您将能够计算出单个曝光值 (EV),它描述了捕获光的平均量。然后,您可以使用它来推导出拍摄场景中亮度的对数平均值,这在后期处理和应用特殊效果方面非常宝贵。
曝光值的计算公式如下:
from math import log2
def exposure_value(f_stop, exposure_time, iso_speed):
return log2(f_stop ** 2 / exposure_time) - log2(iso_speed / 100)
请记住,它没有考虑其他因素,例如您的相机可能适用的曝光偏差或闪光灯。无论如何,尝试一些示例值:
>>> exposure_value(
... f_stop=Fraction(28, 5),
... exposure_time=Fraction(1, 750),
... iso_speed=400
... )
12.521600439723727
>>> exposure_value(f_stop=5.6, exposure_time=1/750, iso_speed=400)
12.521600439723727
您可以将分数或其他数字类型用于输入值。这种情况下,曝光值在+13左右,比较亮。这张照片是在阳光明媚的日子在外面拍摄的,尽管是在阴凉处。
解决变革问题
您可以使用分数来解决计算机科学的经典变革问题,您可能会在求职面试中遇到这些问题。它要求最少数量的硬币来获得一定数量的钱。例如,如果您考虑最流行的美元硬币,那么您可以将 2.67美元表示为十个四分之一 (10 × $0.25)、一角硬币 (1 × $0.10)、一镍 (1 × $0.05) 和两便士 (2 × 0.01 美元)。
分数可以是一种方便的工具来表示钱包或收银机中的硬币。您可以通过以下方式定义美元的硬币:
from fractions import Fraction
penny = Fraction(1, 100)
nickel = Fraction(5, 100)
dime = Fraction(10, 100)
quarter = Fraction(25, 100)
其中一些会自动减少自己,但这没关系,因为您将使用十进制表示法对其进行格式化。您可以使用这些硬币来计算您钱包的总价值:
>>> wallet = [8 * quarter, 5 * dime, 3 * nickel, 2 * penny]
>>> print(f"${float(sum(wallet)):.2f}")
$2.67
您的钱包价值 2.67 美元,但里面有多达 18 个硬币。可以使用更少的硬币获得相同的数量。解决变革问题的一种方法是使用贪婪算法,例如:
def change(amount, coins):
while amount > 0:
for coin in sorted(coins, reverse=True):
if coin <= amount:
amount -= coin
yield coin
break
else:
raise Exception("There's no solution")
该算法尝试找到面额最高且不大于剩余金额的硬币。虽然实施起来相对简单,但它可能无法在所有硬币系统中提供最佳解决方案。以下是美元硬币的示例:
>>> from collections import Counter
>>> amount = Fraction("2.67")
>>> usd = [penny, nickel, dime, quarter]
>>> for coin, count in Counter(change(amount, usd)).items():
... print(f"{count:>2} × ${float(coin):.2f}")
...
10 × $0.25
1 × $0.10
1 × $0.05
2 × $0.01
必须使用有理数才能找到解决方案,因为浮点值不会削减它。由于change()
是生成可能重复的硬币的生成器函数,因此您可以Counter
将它们分组。
您可以通过提出一个稍微不同的问题来修改这个问题。例如,考虑到总价、顾客的硬币和收银机中可用的卖方硬币,最佳的硬币集是什么?
生产和扩大连续馏分
在本教程的开头,您了解到无理数可以表示为无限连分数。此类分数需要无限量的内存才能存在,但您可以选择何时停止生成它们的系数以获得合理的近似值。
以下生成器函数将以惰性求值的方式无休止地产生给定数字的系数:
1def continued_fraction(number):
2 while True:
3 yield (whole_part := int(number))
4 fractional_part = number - whole_part
5 try:
6 number = 1 / fractional_part
7 except ZeroDivisionError:
8 break
该函数截断数字并继续将剩余分数表示为作为输入反馈的倒数。为了消除代码重复,它在第 3 行使用赋值表达式,通常称为Python 3.8 中引入的walrus 运算符。
有趣的是,您也可以为有理数创建连分数:
>>> list(continued_fraction(42))
[42]
>>> from fractions import Fraction
>>> list(continued_fraction(Fraction(3, 4)))
[0, 1, 3]
数字 42 只有一个系数,没有小数部分。相反,3/4 没有整数部分,而是由 1 超过 1 + 1/3 组成的连分数:
像往常一样,您应该注意在切换到 时可能会出现的浮点表示错误float
:
>>> list(continued_fraction(0.75))
[0, 1, 3, 1125899906842624]
虽然您可以忠实地用二进制表示 0.75,但它的倒数尽管是有理数,但具有非终止的十进制扩展。当您检查其余的系数时,您最终会在分母中达到这个巨大的幅度,代表一个可以忽略不计的小值。那是你的近似误差。
您可以通过用 Python 分数替换实数来消除此错误:
from fractions import Fraction
def continued_fraction(number):
while True:
yield (whole_part := int(number))
fractional_part = Fraction(number) - whole_part
try:
number = Fraction(1, fractional_part)
except ZeroDivisionError:
break
这个小改动可以让您可靠地生成对应于十进制数的连分数的系数。否则,即使终止十进制扩展,您也可能陷入无限循环。
好的,让我们做一些更有趣的事情,生成无理数的系数,其中的小数展开式在小数点后第 50 位被砍掉。为了精确起见,将它们定义为Decimal
实例:
>>> from decimal import Decimal
>>> pi = Decimal("3.14159265358979323846264338327950288419716939937510")
>>> sqrt2 = Decimal("1.41421356237309504880168872420969807856967187537694")
>>> phi = Decimal("1.61803398874989484820458683436563811772030917980576")
现在,您可以使用熟悉的islice()
迭代器检查其连分数的前几个系数:
>>> from itertools import islice
>>> numbers = {
... " π": pi,
... "√2": sqrt2,
... " φ": phi
... }
>>> for label, number in numbers.items():
... print(label, list(islice(continued_fraction(number), 20)))
...
π [3, 7, 15, 1, 292, 1, 1, 1, 2, 1, 3, 1, 14, 2, 1, 1, 2, 2, 2, 2]
√2 [1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]
φ [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
pi 的前四个系数给出了惊人的好近似值,后面是微不足道的余数。然而,另外两个常数的连分数看起来很奇特。他们一遍又一遍地重复相同的数字,直到无穷大。知道了这一点,您可以通过将这些系数扩展回十进制形式来近似它们:
def expand(coefficients):
if len(coefficients) > 1:
return coefficients[0] + Fraction(1, expand(coefficients[1:]))
else:
return Fraction(coefficients[0])
递归地定义这个函数很方便,这样它就可以在连续较小的系数列表上调用自己。在基本情况下,只有一个整数,这是可能的最粗略的近似值。如果有两个或更多,则结果是第一个系数的总和,然后是其余系数的倒数展开。
您可以通过在相反的返回值上调用它们来验证这两个函数是否按预期工作:
>>> list(continued_fraction(3.14159))
[3, 7, 15, 1, 25, 1, 7, 4, 851921, 1, 1, 2, 880, 1, 2]
>>> float(expand([3, 7, 15, 1, 25, 1, 7, 4, 851921, 1, 1, 2, 880, 1, 2]))
3.14159
完美的!如果你输入continued_fraction()
to的结果expand()
,那么你会得到你在开始时的初始值。但是,在某些情况下,您可能需要将扩展分数转换为Decimal
类型而不是float
更高的精度。
结论
在阅读本教程之前,您可能从未想过计算机如何存储小数。毕竟,也许你的好老朋友似乎可以很好float
地处理它们。然而,历史表明,这种误解最终可能导致灾难性的失败,而这可能会花费大量资金。
使用 PythonFraction
是避免此类灾难的一种方法。您已经了解了分数表示法的优缺点、它的实际应用以及在 Python 中使用它的方法。现在,您可以就哪种数字类型最适合您的用例做出明智的选择。
在本教程中,您学习了如何:
- 在十进制和小数表示法之间转换
- 执行有理数算术
- 近似无理数
- 以无限精度精确表示分数
- 知道什么时候选择
Fraction
了Decimal
或float
- 点赞
- 收藏
- 关注作者
评论(0)