Decimal 常见问题¶

Q. 总是输入 decimal.Decimal('1234.5') 是否过于笨拙。 在使用交互解释器时有没有最小化输入量的方式?

A. 有些用户会将构造器简写为一个字母:

>>>D = decimal.Decimal
>>>D('1.23') + D('3.45')
Decimal('4.68')

Q. 在带有两个十进制位的定点数应用中,有些输入值具有许多位,需要被舍入。 另一些数则不应具有多余位,需要验证有效性。 这种情况应该用什么方法?

A. 用 quantize() 方法舍入到固定数量的十进制位。 如果设置了 Inexact 陷阱,它也适用于验证有效性:

>>>TWOPLACES = Decimal(10) ** -2 # same as Decimal('0.01')
>>># Round to two places
>>>Decimal('3.214').quantize(TWOPLACES)
Decimal('3.21')
>>># Validate that a number does not exceed two places
>>>Decimal('3.21').quantize(TWOPLACES, context=Context(traps=[Inexact]))
Decimal('3.21')
>>>Decimal('3.214').quantize(TWOPLACES, context=Context(traps=[Inexact]))
Traceback (most recent call last):
...
Inexact: None

Q. 当我使用两个有效位的输入时,我要如何在一个应用中保持有效位不变?

A. 某些运算例如与整数相加、相减和相乘将会自动保留固定的小数位数。 其他运算,例如相除和非整数相乘则将会改变小数位数,需要再加上 quantize() 处理步骤:

>>>a = Decimal('102.72') # Initial fixed-point values
>>>b = Decimal('3.17')
>>>a + b # Addition preserves fixed-point
Decimal('105.89')
>>>a - b
Decimal('99.55')
>>>a * 42 # So does integer multiplication
Decimal('4314.24')
>>>(a * b).quantize(TWOPLACES) # Must quantize non-integer multiplication
Decimal('325.62')
>>>(b / a).quantize(TWOPLACES) # And quantize division
Decimal('0.03')

在开发定点数应用时,更方便的做法是定义处理 quantize() 步骤的函数:

>>>def mul(x, y, fp=TWOPLACES):
... return (x * y).quantize(fp)
>>>def div(x, y, fp=TWOPLACES):
... return (x / y).quantize(fp)
>>>mul(a, b) # Automatically preserve fixed-point
Decimal('325.62')
>>>div(b, a)
Decimal('0.03')

Q. 表示同一个值有许多方式。 数字 200, 200.000, 2E2 和 02E+4 的值都相同但有精度不同。 是否有办法将它们转换为一个可识别的规范值?

A. 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')]

Q. 有些十进制值总是被打印为指数表示形式。 是否有办法得到一个非指数表示形式?

A. 对于某些值来说,指数表示形式是表示系数中有效位的唯一办法。 例如,将 5.0E+3 表示为 5000 可以让值保持恒定,但是无法显示原本的两位有效数字。

如果一个应用不必关心追踪有效位,则可以很容易地移除指数和末尾的零,丢弃有效位但让值保持不变:

>>>def remove_exponent(d):
... return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
>>>remove_exponent(Decimal('5E+3'))
Decimal('5000')

Q. 是否有办法将一个普通浮点数转换为 Decimal?

A. 是的,任何二进制浮点数都可以精确地表示为 Decimal 值,但完全精确的转换可能需要比平常感觉更高的精度:

>>>Decimal(math.pi)
Decimal('3.141592653589793115997963468544185161590576171875')

Q. 在一个复杂的计算中,我怎样才能保证不会得到由精度不足和舍入异常所导致的虚假结果。

A. 使用 decimal 模块可以很容易地检测结果。 最好的做法是使用更高的精度和不同的舍入模式重新进行计算。 明显不同的结果表明存在精度不足、舍入模式问题、不符合条件的输入或是结果不稳定的算法。

Q. 我发现上下文精度的应用只针对运算结果而不针对输入。在混合使用不同精度的值时有什么需要注意的吗?

A. 是的。 原则上所有值都会被视为精确值,在这些值上进行的算术运算也是如此。 只有结果会被舍入。 对于输入来说其好处是“所输入即所得”。 而其缺点则是如果你忘记了输入没有被舍入,结果看起来可能会很奇怪:

>>>getcontext().prec = 3
>>>Decimal('3.104') + Decimal('2.104')
Decimal('5.21')
>>>Decimal('3.104') + Decimal('0.000') + Decimal('2.104')
Decimal('5.20')

解决办法是提高精度或使用单目加法运算对输入执行强制舍入:

>>>getcontext().prec = 3
>>>+Decimal('1.23456789') # unary plus triggers rounding
Decimal('1.23')
>>>Context(prec=5, rounding=ROUND_DOWN).create_decimal('1.2345678')
Decimal('1.2345')
Q. CPython 实现对于

巨大数字是否足够快速?

A. 是的。 在 CPython 和 PyPy3 实现中,decimal 模块的 C/CFFI 版本集成了高速 libmpdec 库用于实现任意精度正确舍入的十进制浮点算术 1。 libmpdec 会对中等大小的数字使用 Karatsuba 乘法 而对非常巨大的数字使用 数字原理变换。

必须要对任意精度算术适配上下文。 Emin 和 Emax 应当总是设为最大值,clamp 应当总是设为 0 (默认值)。 设置 prec 需要十分谨慎。

进行大数字算术的最便捷方式也是使用 prec 的最大值 2:

>>>setcontext(Context(prec=MAX_PREC, Emax=MAX_EMAX, Emin=MIN_EMIN))
>>>x = Decimal(2) ** 256
>>>x / 128
Decimal('904625697166532776746648320380374280103671755200316906558262375061821325312')

对于不精确的结果,在 64 位平台上 MAX_PREC 的值太大了,可用的内存将会不足:

>>>Decimal(1) / 3
Traceback (most recent call last):
File "", line 1, in 
MemoryError

在具有超量分配的系统上 (即 Linux),一种更复杂的方式根据可用的 RAM 大小来调整 prec。 假设你有 8GB 的 RAM 并期望同时有 10 个操作数,每个最多使用 500MB:

>>>import sys
>>>
>>># Maximum number of digits for a single operand using 500MB in 8-byte words
>>># with 19 digits per word (4-byte and 9 digits for the 32-bit build):
>>>maxdigits = 19 * ((500 * 1024**2) // 8)
>>>
>>># Check that this works:
>>>c = Context(prec=maxdigits, Emax=MAX_EMAX, Emin=MIN_EMIN)
>>>c.traps[Inexact] = True
>>>setcontext(c)
>>>
>>># Fill the available precision with nines:
>>>x = Decimal(0).logical_invert() * 9
>>>sys.getsizeof(x)
524288112
>>>x + 2
Traceback (most recent call last):
File "", line 1, in 
decimal.Inexact: []

总体而言(特别是在没有超量分配的系统上),如果期望所有计算都是精确的则推荐预估更严格的边界并设置 Inexact 陷阱。

3.3 新版功能.

在 3.9 版更改:此方式现在适用于除了非整数乘方以外的所有精确结果。