全国报名热线

021-6769 0939

首页>Python>正文

Python学习:深入理解python浮点数

时间:2017-12-29 12:01:27   来源:雨痕   阅读:

Python浮点数

默认 float 类型存储双精度(double)浮点数,可表达 16 到 17 个小数位。

>>> 1/3
0.3333333333333333

>>> 0.1234567890123456789
0.12345678901234568

从实现方式看,浮点数以二进制存储十进制数的近似值。这可能会导致执行结果与编码预期不符,造成算法结果不一致性缺陷。对精度有要求的场合,应选择固定精度类型。

可通过 float.hex 方法输出实际存储值的十六进制格式字符串,以检查执行结果为何不同。 另外,还可用该方式实现浮点值的精确传递,避免精度丢失。

>>> 0.1 * 3 == 0.3
False

>>> (0.1 * 3).hex()  # 显然两个存储内容并不相同。
0x1.3333333333334p-2

>>> (0.3).hex()
0x1.3333333333333p-2

 

>>> s = (1/3).hex()

>>> float.fromhex(s)  # 反向转换回浮点数。
0.3333333333333333

对于简单比较操作,可尝试将浮点数限制在有效固定精度内。但考虑到 round 算法实现的一些问题,更精确做法是用 decimal.Decimal 类型。

>>> round(0.1 * 3, 2) == round(0.3, 2)  # 避免不确定性,左右都使用固定精度。
True

>>> round(0.1, 2) * 3 == round(0.3, 2)  # 将 round 返回值作为操作数,导致精度再次丢失。
False

不同类型的数字之间,可直接进行加减法和比较等运算。

>>> 1.1 + 2
3.1

>>> 1.1 < 2
True

>>> 1.1 == 1
False

 

转换

将整数或字符串转换为浮点数很简单,且能自动处理字符串内正负符号和空白符。只是超出有效精度时,结果与字符串内容存在差异。

>>> float(100)
100.0

>>> float("-100.123")  # 符号
-100.123

>>> float("\t 100.123\n")  # 空白符
100.123

>>> float("1.234E2")  # 科学记数法
123.4

 

>>> float("0.1234567890123456789")
0.12345678901234568

反过来,将浮点数转换为整数时,有多种不同方案可供选择。可截掉(int, trunc) 小数部分,或分别往大(ceil)、小(floor) 方向取临近整数。

>>> int(2.6), int(-2.6)
(2, -2)

 

>>> from math import trunc, floor, ceil

>>> trunc(2.6), trunc(-2.6)  # 截断小数部分。
(2, -2)

>>> floor(2.6), floor(-2.6)  # 往小数字方向取最近整数。
(2, -3)

>>> ceil(2.6), ceil(-2.6)  # 往大数字方向取最近整数。
(3, -2)

 

 

 

十进制浮点数

相比 float 基于硬件的二进制浮点类型,decimal.Decimal 是十进制实现,最高可提供 28 位有效精度。能准确表达十进制数和运算,不存在二进制近似值问题。

>>> 1.1 + 2.2   # 结果与 3.3 近似。
3.3000000000000003

>>> (0.1 + 0.1 + 0.1 - 0.3) == 0  # 同样二进制近似值计算结果与十进制预期不符。
False

 

>>> from decimal import Decimal

>>> Decimal("1.1") + Decimal("2.2")
Decimal('3.3')

>>> (Decimal("0.1") + Decimal("0.1") + Decimal("0.1") - Decimal("0.3")) == 0
True

在创建 Decimal 实例时,应该传入一个准确的数值,比如整数或字符串等。如果是 float 类型,那么要知道在构建之前,其精度就已丢失。

>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

>>> Decimal("0.1")
Decimal('0.1')

在需要时,可通过上下文(context) 修改 Decimal 默认的 28 位精度。

>>> from decimal import Decimal, getcontext

>>> getcontext()
Context(prec=28, ...)

>>> getcontext().prec = 2

>>> Decimal(1) / Decimal(3)
Decimal('0.33')

或者用 localcontext 限制某个区域的精度。

>>> from decimal import localcontext

>>> getcontext().prec = 28

>>> with localcontext() as ctx:  # 在该范围内将精度修改为 2。
        ctx.prec = 2
        print(getcontext().prec)
        print(Decimal(1) / Decimal(3))

2
0.33

>>> getcontext().prec    # 不会影响外部精度。
28

除非有明确需求,否则不要用 Decimal 替代 float,要知道前者运算速度要慢许多。

 

四舍五入

同样因为近似值和精度问题,造成对 float 进行 “四舍五入”(round)操作存在不确定性,其结果会导致一些不易察觉的陷阱。

>>> round(0.5) # 5 舍
0

>>> round(1.5) # 5 入
2

首先,按照 round 算法规则,按临近数字(舍入后) 的距离远近来考虑是否进位。如此,四舍六入就是确定的,相关问题都集中在两边距离相等的 5 是否进位。

以 0.4 为例,其舍入后的相邻数字是 0 和 1,从距离上看自然是 0 更近一些。

对于 5,还要考虑后面是否还有小数位。如果有,那么左右距离就不可能是相等的,这自然是要进位的。

>>> round(0.5)   # 与 0、1 距离相等,不确定。
0

>>> round(0.500001)   # 哪怕 5 后面的小数部分再小,那也表示它更接近 1。
1

>>> round(1.25, 1)
1.2

>>> round(1.25001, 1)
1.3

剩下的,要看是返回整数还是浮点数。如果是整数,取临近的偶数。

>>> round(0.5)  # 0 ---> 0.5 ---> 1
0

>>> round(1.5)  # 1 ---> 1.5 ---> 2
2

>>> round(2.5)  # 2 ---> 2.5 ---> 3
2

在不同版本下,规则存在差异。比如 Python 2.7,round 2.5 返回 3.0。从这点来看,我们应谨慎对待此类行为差异,并严格测试其造成的影响。

如果依旧返回浮点数,事情就变的有点莫名其妙。有些文章宣称 “奇舍偶入” 或 “五成双” 等,也就是看数字 5 前一位小数奇偶来决定是否进位,但事实未必如此。

>>> round(1.25, 1)  # 偶舍
1.2

>>> round(1.245, 2)  # 偶进
1.25

>>> round(2.675, 2)  # 和下面的都是奇数 7,但有舍有入。
2.67

>>> round(2.375, 2)
2.38

对此,官方文档 《Floating Point Arithmetic: Issues and Limitations》宣称并非错误,而属事出有因。对此,我们可改用 Decimal,按需求选取可控制的进位方案。

>>> from decimal import Decimal, ROUND_HALF_UP

>>> def roundx(x, n):
       return Decimal(x).quantize(Decimal(n), ROUND_HALF_UP) # 严格按照四舍五人进行。

 

>>> roundx("1.24", ".1")
Decimal('1.2')

>>> roundx("1.25", ".1")
Decimal('1.3')

>>> roundx("1.26", ".1")
Decimal('1.3')

 

>>> roundx("1.245", ".01")
Decimal('1.25')

>>> roundx("2.675", ".01")
Decimal('2.68')

>>> roundx("2.375", ".01")
Decimal('2.38')

 

 


分享:0