阅读 67

「Python3学习笔记」读书笔记--float类型

原文链接: m-in.coding.me

本文为「Python3学习笔记」一书的读书总结,以后每学习完一小节做一次记录。

在Python中 float 类型默认存储双精度浮点数(也就是其他语言中的 double ),可一表达16到17位浮点数。

>>> 1/3
0.3333333333333333
>>> 0.1234567890123456789
0.12345678901234568
复制代码

从实现方式上来看,浮点数是以二进制的方式来存储十进制数的近似值。这就可能导致执行的结果与预期不符合,造成不一致缺陷。所以,在对精度有严格要求的场合,应该选择使用固定精度类型,如:decimal.Decimal 。

可通过 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函数,但round函数在实现上有一定的问题,这里更加准确的问题是使用 decimal.Decimal 模块。

>>> round(0.1 * 3, 2) == round(0.3, 2)
True
>>> round(0.1, 2) * 3 == round(0.3, 2)
False
复制代码

decimal.Decimal模块

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

>>> (0.1 + 0.1 + 0.1 - 0.3) == 0      # 二进制近似值计算结果与十进制预期不符合
False
>>> from decimal import Decimal
>>> (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')
复制代码

在需要的时候,可以通过上下文修改 Decimal 默认的28位精度或用 localcontext修改某个区域的精度。

>>> from decimal import Decimal, getcontext, localcontext

>>> getcontext()
Context(prec=28, ...)
>>> getcontext().prec = 2
>>> Decimal(1) / Decimal(3)
Decimal('0.33')

>>> with localcontext() as ctx:
... print(getcontext().prec)
... getcontext().prec = 3
... print(getcontext().prec)
... print(Decimal(1) / Decimal(3))
...
2
3
0.333
>>> getcontext().prec
2
复制代码

Decimal 虽然有着准确的精度,但它的运算速度会慢很多,所以除非有明确的需求,否则还是不要用 Decimal 代替 float 是用。

round 函数

因为精度和近似值问题,在使用round函数对 float 类型的值进行“四舍五入”的操作存在不确定性,结果会有一些不易察觉的陷阱。

>>> round(0.5)        # 五舍
0
>>> round(1.5) # 五入
2
复制代码

这是因为round函数的算法规则是按临近的数字的距离远近来考虑是否进位的,如:以 0.4 为例,其舍入后相邻的数字分别是 0 和 1 ,从距离上来看自然是离 0 更近一些,所以“四舍五入”的结果为 0。如此一来,“四舍六入”就是确定的,相关问题都集中在两边距离相等的5是否进位上了。

对于5是否进位,首先要考虑的是它后面是否还有小数位。如果有,那么左右距离自然是不想等的,这种情况肯定是会进位的。

>>> round(0.5)
0
>>> round(0.500001)
1
复制代码

如果没有,就要看进位后是整数还是浮点数了。如果是整数,就取临近的偶数。

>>> round(0.5)
0
>>> round(1.5)
2
复制代码

不同的Python版本,规则存在着差异性,如:在Python2.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
复制代码

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

转换

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

>>> float(100)
100.0
>>> float('-100.123')
-100.123
>>> float('\t -100.123\n')
-100.123
>>> float('1.23E2')
123.0
>>> float('0.12345678901234567890') # 超出精度
0.12345678901234568
复制代码

反过来,将浮点数转换为整数时,有多种方案可供选择。

  1. 可直接截掉小数部分

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

    >>> from math import trunc
    >>> trunc(2.6), trunc(-2.6)
    (2, -2)
    复制代码
  2. 分别向大小两个方向取临近整数

    >>> from math import floor, ceil
    >>> floor(2.6), floor(-2.6) # 向小数字方向取最近整数
    (2, -3)
    >>> ceil(2.6), ceil(-2.6) # 向大数字方向取最近整数
    (3, -2)
    复制代码
2018年5月27日 个人思考
文章分类
后端