十进制定点和浮点运算#

decimal 模块提供了快速正确舍入的十进制浮点数算术支持。它比 float 数据类型具有以下优势:

  • decimal.Decimal 类型的“设计是基于考虑人类习惯的浮点数模型,并且因此具有以下最高指导原则 —— 计算机必须提供与人们在学校所学习的算术相一致的算术。” —— 摘自 decimal 算术规范描述。

  • 十进制数可以精确表示。相比之下,像 1.12.2 这样的数字在二进制浮点数中没有精确的表示。

终端用户通常不会期望 1.1 + 2.2 显示的像二进制浮点数一样:

1.1 + 2.2
3.3000000000000003
  • 精确性延伸到算术运算中。在十进制浮点数中,0.1 + 0.1 + 0.1 - 0.3 等于零。在二进制浮点数中,结果是 5.5511151231257827e-017。虽然接近于零,但差异会防止可靠的相等性测试,并且差异可能会累积。因此,在具有严格相等性的会计应用中,十进制优于二进制。

0.1 + 0.1 + 0.1 - 0.3
5.551115123125783e-17
  • 十进制模块包含了有效数字的概念,因此 1.30+1.20 等于 2.50。尾随零用于表示有效数字。这是货币应用的惯用表示法。对于乘法,“教科书”方法使用乘数中的所有数字。例如,1.3*1.2 给出 1.56,而 1.30*1.20 给出 1.5600

  • 与基于硬件的二进制浮点不同,十进制模块具有用户可更改的精度(默认为 28 位),可以与给定问题所需的一样大:

from decimal import getcontext, Decimal

getcontext().prec = 6
Decimal(1) / Decimal(7)
Decimal('0.142857')
getcontext().prec = 28
Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')
  • 二进制和 decimal 浮点数都是根据已发布的标准实现的。虽然内置浮点类型只公开其功能的一小部分,但 decimal 模块公开了标准的所有必需部分。 在需要时,程序员可以完全控制舍入和信号处理。 这包括通过使用异常来阻止任何不精确操作来强制执行精确算术的选项。

  • decimal 模块旨在支持“无偏差,精确无舍入的十进制算术(有时称为定点数算术)和有舍入的浮点数算术”。 —— 摘自 decimal 算术规范说明

该模块的设计以三个概念为中心:decimal 数值,算术上下文和信号。

十进制数是不可变的。它具有符号、系数位和小数点后的指数。为了保留有效数字,系数位不会截断尾随零。小数还包括特殊值,如 Infinity-Infinity 和非数字(NaN)。标准还区分了 -0+0

算术的上下文 是指定精度、舍入规则、指数限制、指示运算结果的标志以及确定符号是否被视为异常的陷阱启用器的环境。舍入选项包括 decimal.ROUND_CEILINGdecimal.ROUND_DOWNdecimal.ROUND_FLOORdecimal.ROUND_HALF_DOWNdecimal.ROUND_HALF_EVEN、ROUND_HALF_UP、decimal.ROUND_UP 以及 decimal.ROUND_05UP

信号是在计算过程中出现的异常条件组。根据应用程序的需要,信号可能会被忽略,被视为信息,或被视为异常。十进制模块中的信号有:decimal.Clampeddecimal.InvalidOperationdecimal.DivisionByZerodecimal.Inexactdecimal.Roundeddecimal.Subnormaldecimal.Overflowdecimal.Underflow 以及 decimal.FloatOperation

Decimal(100).as_tuple()
DecimalTuple(sign=0, digits=(1, 0, 0), exponent=0)
Decimal('NaN').as_tuple()
DecimalTuple(sign=0, digits=(), exponent='n')
Decimal('Infinity').as_tuple()
DecimalTuple(sign=0, digits=(0,), exponent='F')
Decimal('-0').as_tuple()
DecimalTuple(sign=1, digits=(0,), exponent=0)

定点数#

decimal.Decimal.quantize() 方法将数字舍入到固定的小数位数。如果设置了不精确陷阱,它也适用于验证:

TWOPLACES = Decimal(10) ** -2

舍入两位:

Decimal('3.214').quantize(TWOPLACES), Decimal('3.215').quantize(TWOPLACES)
(Decimal('3.21'), Decimal('3.22'))

验证一个数字是否不超过两位:

from decimal import Context, Inexact
Decimal('3.21').quantize(TWOPLACES, context=Context(traps=[Inexact]))
Decimal('3.21')
Decimal('3.214').quantize(TWOPLACES, context=Context(traps=[Inexact]))
---------------------------------------------------------------------------
Inexact                                   Traceback (most recent call last)
Cell In[34], line 1
----> 1 Decimal('3.214').quantize(TWOPLACES, context=Context(traps=[Inexact]))

Inexact: [<class 'decimal.Inexact'>]

如何在应用中保持有效位不变?#

一些运算,如加法、减法和整数乘法,将自动保留定点。其他运算,如除法和非整数乘法,将更改小数位数并需要使用 decimal.Decimal.quantize() 步骤进行后续处理。

初始化 fixed-point 值:

a = Decimal('102.72')
b = Decimal('3.17')
a + b, a - b, a * 42
(Decimal('105.89'), Decimal('99.55'), Decimal('4314.24'))
102.72 * 3.17
325.62239999999997

必须对非整数乘法以及除法进行量化:

(a * b).quantize(TWOPLACES), (b / a).quantize(TWOPLACES)
(Decimal('325.62'), Decimal('0.03'))

在开发定点应用程序时,定义处理 decimal.Decimal.quantize() 步骤的函数是很方便的。

def mul(x, y, fp=TWOPLACES):
    return (x * y).quantize(fp)

def div(x, y, fp=TWOPLACES):
    return (x / y).quantize(fp)

规范化输出#

有很多方法可以表示相同的值。数字 200200.0002E2.02E+4 在不同精度下具有相同的值。有没有一种方法可以将它们转换为一个可识别的规范值?

decimal.Decimal.normalize() 方法将所有等价值映射到单一的表示:

values = map(Decimal, '200 200.000 2E2 .02E+4'.split())
[v.normalize() for v in values]
[Decimal('2E+2'), Decimal('2E+2'), Decimal('2E+2'), Decimal('2E+2')]