Decimal 常见问题¶
问:输入 decimal.Decimal('1234.5') 很麻烦。在使用交互式解释器时,有没有办法减少输入?
答:一些用户将构造函数缩写为一个字母:
>>> D = decimal.Decimal
>>> D('1.23') + D('3.45')
Decimal('4.68')
问:在一个有两位小数的定点应用中,一些输入有很多位小数需要四舍五入。另一些则不应该有多余的数字,需要进行验证。应该使用什么方法?
答: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
问:一旦我有了有效的两位小数输入,如何在整个应用程序中保持这种不变性?
答:一些操作,如加法、减法和与整数相乘,会自动保留定点。其他操作,如除法和非整数乘法,会改变小数位数,需要后续使用 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')
问:有很多方法可以表示相同的值。数字 200、200.000、2E2 和 .02E+4 在不同精度下都具有相同的值。有没有办法将它们转换为一个可识别的规范值?
答: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')]
问:计算中何时发生四舍五入?
答:它发生在计算之后。十进制规范的理念是,数字被认为是精确的,并且独立于当前上下文创建。它们甚至可以具有比当前上下文更高的精度。计算使用这些精确的输入进行处理,然后将四舍五入(或其他上下文操作)应用于计算的结果。
>>> getcontext().prec = 5
>>> pi = Decimal('3.1415926535') # More than 5 digits
>>> pi # All digits are retained
Decimal('3.1415926535')
>>> pi + 0 # Rounded after an addition
Decimal('3.1416')
>>> pi - Decimal('0.00005') # Subtract unrounded numbers, then round
Decimal('3.1415')
>>> pi + 0 - Decimal('0.00005'). # Intermediate values are rounded
Decimal('3.1416')
问:有些十进制值总是以指数表示法打印。有没有办法得到非指数表示法?
答:对于某些值,指数表示法是表示系数中有效位数的唯一方法。例如,将 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')
问:有没有办法将常规浮点数转换为 Decimal?
答:是的,任何二进制浮点数都可以精确地表示为 Decimal,尽管精确转换可能需要比直觉上认为的更高的精度。
>>> Decimal(math.pi)
Decimal('3.141592653589793115997963468544185161590576171875')
问:在一个复杂的计算中,我如何确保我没有因为精度不足或四舍五入异常而得到虚假结果?
答:decimal 模块使得测试结果变得容易。最佳实践是使用更高的精度和不同的四舍五入模式重新运行计算。结果差异很大表明精度不足、四舍五入模式问题、输入条件不佳或算法数值不稳定。
问:我注意到上下文精度应用于操作的结果,但不应用于输入。混合不同精度的值时有什么需要注意的吗?
答:是的。原则是所有值都被认为是精确的,对这些值的算术也是精确的。只有结果会被四舍五入。对输入的好处是“所输即所得”。缺点是,如果你忘记了输入没有被四舍五入,结果可能会看起来很奇怪。
>>> 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.create_decimal() 方法对输入进行四舍五入:
>>> Context(prec=5, rounding=ROUND_DOWN).create_decimal('1.2345678')
Decimal('1.2345')
问:CPython 实现对于大数运算快吗?
答:是的。在 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 "
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 "
decimal.Inexact: [
总的来说(尤其是在没有超额分配的系统上),建议估算更紧的界限,并在所有计算都预期是精确的情况下设置 Inexact 陷阱。
[1]
在 3.3 版本加入。
[2]
在 3.9 版更改: 此方法现在适用于除整数次幂以外的所有精确结果。