User:Xyy23330121/Python/decimal

来自维基学院


decimal 是一种特殊的浮点数,它可以用于解决 float 把十进制小数转换成二进制无限循环小数,再舍入时产生的误差。在 Python 中,decimal 的内容由 decimal 模块提供。

decimal[编辑 | 编辑源代码]

float 储存数字的方式,简单来讲就是用二进制的方式储存三部分内容:正负号、二进制数字m、二进制数字n。来表示形如 +m * 2**n-m * 2**n的内容。decimal 与之类似,但它表示的是形如 +m * 10**n 的内容。

由于科学计数法中、底数的作用, float 保留的有效位数,是二进制的有效位数。十进制的有限小数、不一定能由二进制的有限小数表示出来。此时,取二进制小数中、有限部分的内容,一定会产生误差。而 Decimal 保留的有效位数是十进制的有效位数。这种方式使得计算机的数字计算与人的直觉相符。

这并不代表 Decimal 不会产生舍入误差,只是舍入误差以符合直觉的方式出现——也就能够简单的以符合直觉的方式解决。

Decimal 也被一些语言称作是货币值,这是因为货币运算一般不包含除法、也不会有超出限制的位数,从而 Decimal 能精确地计算商品的价格等内容。

创建 Decimal[编辑 | 编辑源代码]

import decimal as d
print(d.Decimal(10))          #10
print(d.Decimal(0.1))         #0.1000000000000000055511151231257827021181583404541015625
print(d.Decimal('0.1'))       #0.1
print(d.Decimal('1.1e-1'))    #0.11
print(d.Decimal('-1e-1'))     #-0.1
print(d.Decimal('-inf'))      #-Infinity
print(d.Decimal('nan'))       #NaN
print(d.Decimal('-nan114514'))#-NaN114514

Decimal 可以由以上“类型构造器”方式,从整数、浮点数和字符串中生成。生成时,推荐使用字符串或整数。

对于浮点数,它会连同误差的部分也精确地表现——从而误差并没有减小。

也可以用元组来创建 Decimal,我们见以下方式:

import decimal as d
#           符号位 有效数位  指数
num_tuple = (0,    (1, 2, 3), -1)
print(d.Decimal(num_tuple)) #12.3

用元组创建 Decimal 时,元组需要为一个三元组。

其中,第一个元素为符号位,其数值为 01,分别代表正数或负数。上面例子中是正数。

第二个元素为包含所有有效数位的元组,十进制的每一位都要分开表示,上面例子中、该元组表示整数 123

第三个元素表示指数部分的整数,上面例子使用了 -1,从而指数部分是

从而,以上元组表示的数字是:

Decimal 的运算[编辑 | 编辑源代码]

Decimal 支持一切数字的运算。包括:

  1. 四则运算、带余除法、绝对值
  2. 用 int() 、 float() 等类型构造器,转换为 int 或 float。

Decimal 在数学运算时,不能和 float 混用。在比较运算时是可以的。

Decimal 的带余除法[编辑 | 编辑源代码]

Decimal 之间在进行 Python 带余除法时,取余数的符号和被除数符号相同。

这个行为与浮点数的带余除法或整数的带余除法是不同的,而与 math.fmod 函数的行为相同。

Decimal 的方法[编辑 | 编辑源代码]

Decimal 对象支持许多方法,包括用于数学运算的对数函数和向整数四舍五入等。读者可以阅读相关文档[1]来了解其内容。

decimal 与 上下文对象[编辑 | 编辑源代码]

Decimal 的具体运算,比如精度等,是由上下文对象decimal.Context规定的。

上下文对象的属性[编辑 | 编辑源代码]

decimal 的上下文对象包含以下内容:

内容
名称 注释
计算精度
prec Decimal 在计算时的有效位数。
这个精度对用类型构造器创建 Decimal 时的精度没有影响。
使用 32 位运行 Python 时,此项为 1 ~ 425000000 的整数,
使用 64 位时,此项为 1 ~ 999999999999999999 的整数。
舍入模式
rounding 决定运算需要舍入时,Decimal的舍入行为。具体可以为以下模式:
模式
decimal.ROUND_CEILING 向上舍入
decimal.ROUND_FLOOR 向下舍入
decimal.ROUND_DOWN 向零舍入
decimal.ROUND_UP 向无穷大舍入
decimal.ROUND_HALF_DOWN 舍入到最接近的数字
如果有两个数字同样接近,则向零舍入
decimal.ROUND_HALF_UP 舍入到最接近的数字
如果有两个数字同样接近,则向无穷大舍入
decimal.ROUND_HALF_EVEN 舍入到最接近的数字
如果有两个数字同样接近,则向偶数舍入
decimal.ROUND_05UP 如果舍入后最后一位
为 0 或 5,则向无穷
大舍入;
否则,向零舍入。
报错设置
traps 一个字典,具体见“异常处理方式”章节
错误提醒
flags 一个字典,具体见“错误提示”章节
指数范围
Emin 创建 Decimal 时,指数部分的最小值
使用 32 位运行 Python 时,此项为 -425000000 ~ 0 的整数,
使用 64 位时,此项为 -999999999999999999 ~ 0 的整数。
Emax 创建 Decimal 时,指数部分的最大值
使用 32 位运行 Python 时,此项为 0 ~ 425000000 的整数,
使用 64 位时,此项为 0 ~ 999999999999999999 的整数。
夹板
clamp 若设为 1,则当指数部分过大时,会减小其指数部分,并移除数字末尾的零。
这使得指数部分的限制为是Emin - prec + 1 ~ Emax - prec + 1 的整数。
若设为 0,则不进行这类操作,此时指数部分限制为是Emin ~ Emax 的整数。
大小写
capitals 若设为 1,则在打印 1*10^1 时,打印为 1E+1
若设为 0,则打印为 1e1

异常处理方式[编辑 | 编辑源代码]

decimal 模块有一套特殊的异常处理方式,这个异常处理方式是由上下文对象的 traps 属性决定的。traps 为一个内容为 错误类型:是否报错 的字典。

当出现“可以认为出错了”的操作时,decimal 模块会查询对应错误类型的设置,如果 traps[错误类型] 的值为 True,则会引发错误实例以报错。

反之,如果 traps[错误类型] 的值为 False,则不进行报错。此时,操作的结果会被特殊处理。

以下是几种异常类型,以及不报错时的处理方法。

decimal.Clamped[编辑 | 编辑源代码]

当指数超出当前上下文对象给出的上限时出现。

不报错、且上下文中 clamp 为 1 时,会对指数进行修改,并向有效数字的末尾添加零,使实例正确反应数值。这对有效数字的位数有影响。

decimal.Subnormal[编辑 | 编辑源代码]

当指数超出当前上下文对象给出的下限时出现。

未报错时,会对指数进行修改,并减少有效数字部分末尾的零,使实例正确反应数值。这对有效数字的位数有影响。

decimal.Overflow[编辑 | 编辑源代码]

当数值超出当前上下文对象给出的限制中、可表示的最大数值时出现。

不报错时,会进行舍入。视舍入方向,可能有两个结果:

  1. 无穷大
  2. 可表示的最大数值

decimal.Underflow[编辑 | 编辑源代码]

当数值超出当前上下文对象给出的限制中、可表示的最小数值时出现。

不报错时,会进行舍入,并舍入为 0。

decimal.DivisionByZero[编辑 | 编辑源代码]

当被除数为有限数字,而除数为零时出现。有限数字指的是无穷大或 NaN

不报错时,会让计算结果变为无穷大。此时,当被除数与除数符号相同时,返回正无穷大;当被除数与除数符号不同时,返回负无穷大。

decimal.InvalidOperation[编辑 | 编辑源代码]

当产生无意义运算时出现。无意义运算可能为:

  • 无穷大乘无穷大
  • 无穷大乘零
  • 无穷大除无穷大
  • 以零为除数,作带余除法
  • 被除数为无穷大,除数为有限数字,作带余除法
  • 对负数求平方根
  • 零的零次幂
  • 有限数字的非整数次幂,或无穷大次幂

不报错时,计算结果为 NaN。

decimal.Rounded[编辑 | 编辑源代码]

当发生舍入时出现,不一定产生舍入误差。

不报错时,会进行舍入。

decimal.Inexact[编辑 | 编辑源代码]

当发生舍入、且会产生舍入误差时出现。

不报错时,会进行舍入,并产生舍入误差。

decimal.FloatOperation[编辑 | 编辑源代码]

当 float 和 Decimal 混合使用时出现。

不报错时,以下的计算可以进行:

  1. 类型构造器输入浮点数
    decimal.Decimal(float)
  2. 不等于的比较运算
    decimal.Decimal < float

如果此项设置为报错,又需要从 float 创建 Decimal 时,可以使用 classmethod decimal.Decimal.from_float(f)

decimal.DecimalException[编辑 | 编辑源代码]

这是以上所有错误的基类。它在默认情况下不在 flags 字典中,也不在 traps 字典中。

以下内容总结了各个信号的层级结构:

exceptions.ArithmeticError(exceptions.Exception)
    DecimalException
        Clamped
        DivisionByZero(DecimalException, exceptions.ZeroDivisionError)
        Inexact
            Overflow(Inexact, Rounded)
            Underflow(Inexact, Rounded, Subnormal)
        InvalidOperation
        Rounded
        Subnormal
        FloatOperation(DecimalException, exceptions.TypeError)

获取与修改当前上下文[编辑 | 编辑源代码]

decimal.getcontext()[编辑 | 编辑源代码]

获取当前使用的上下文。通过这个方法,也可以设置当前上下文的内容。比如:

import decimal as d
D = d.Decimal
current_context = d.getcontext()
print(current_context)
print(D(1)/D(7))
current_context.prec = 7
print(D(1)/D(7))

输出为:

Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])
0.1428571428571428571428571429
0.1428571

错误提示[编辑 | 编辑源代码]

即便设置了不进行报错,有时也需要了解是否出现了问题。上下文对象的 flags 属性就是用于对异常进行提示的。

flags 是一个内容为 错误类型:是否出现过 的字典。当异常产生(但不一定报错)时,flags[错误类型] 的值会被改为 True

我们看以下示例:

import decimal as d
D = d.Decimal

#解除 decimal.DivisionByZero 的报错,使程序不会因报错而终止。
current_context = d.getcontext()
current_context.traps[d.DivisionByZero] = 0
print("引发信号前:")
print(current_context)
print(current_context.flags[d.DivisionByZero])

#引发 decimal.DivisionByZero 信号。
D(1)/D(0)

#查看引发信号后的结果
print("引发信号后:")
print(current_context)
print(current_context.flags[d.DivisionByZero])

#清除之前的提示
current_context.clear_flags()

#查看清除提示后的结果
print("清除提示后:")
print(current_context)
print(current_context.flags[d.DivisionByZero])

输出为:

引发信号前
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, Overflow])
False
引发信号后
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[DivisionByZero], traps=[InvalidOperation, Overflow])
True
清除提示后
Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, Overflow])
False

设置上下文[编辑 | 编辑源代码]

对于已有的上下文对象,可以用这个方法,将其设置为当前上下文: decimal.setcontext(c)

decimal 模块提供了几种预设的上下文:

  • decimal.DefaultContext 导入模块时自动使用的、默认上下文。
  • decimal.BasicContext 通用十进制算术规范描述所定义的标准上下文。
    精度为 9,舍入为 ROUND_HALF_UP,不会进行任何错误提示,但所有错误都会报错。
  • decimal.ExtendedContext 通用十进制算术规范描述所定义的标准上下文。
    精度为 9,舍入为 ROUND_HALF_UP,不会进行任何错误提示,且所有错误都不会报错。

读者也可以自行创建上下文对象。

创建上下文对象[编辑 | 编辑源代码]

decimal.Context(prec=None, rounding=None, Emin=None, Emax=None, capitals=None, clamp=None, flags=None, traps=None)

各个参数的内容如上面所示。特别的,默认值为 decimal.DefaultContext 的值,即:

Context(prec=28, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999, capitals=1, clamp=0, flags=[], traps=[InvalidOperation, DivisionByZero, Overflow])

在上述几个示例中,读者可能注意到:在打印时,traps 和 flags 并没有显示为字典。事实上,在创建上下文对象时,输入的 traps 和 flags 也不需要必须是字典。这是因为在创建上下文对象时,decimal 模块使用了以下代码来将列表转化为字典:

_signals = [Clamped, DivisionByZero, Inexact, Overflow, Rounded,
            Underflow, InvalidOperation, Subnormal, FloatOperation]

dc = DefaultContext

if traps is None:
    self.traps = dc.traps.copy()
elif not isinstance(traps, dict):
    self.traps = dict((s, int(s in traps)) for s in _signals + traps)
else:
    self.traps = traps

if flags is None:
    self.flags = dict.fromkeys(_signals, 0)
elif not isinstance(flags, dict):
    self.flags = dict((s, int(s in flags)) for s in _signals + flags)
else:
    self.flags = flags

通过阅读以上代码,如果读者需要设置自己的、使用 Decimal 的运算。读者应该了解如何向上下文对象添加运算中的信号。

上下文管理器[编辑 | 编辑源代码]

decimal 模块提供了一个上下文管理器 decimal.localcontext(ctx=None, **kwargs),它是这样使用的:

def sin(x):
    with localcontext() as ctx:
        ctx.prec += 2  #增加计算精度
        i, lasts, s, fact, num, sign = 0, 0, 1, 1, 1, 1
        while s != lasts:
            lasts = s
            i += 2
            fact *= i * (i-1)
            num *= x * x
            sign *= -1
            s += num / fact * sign
    return +s  #进行一次“正”运算,重新舍入到当前精度

在创建此管理器时,此管理器会保存当前上下文,并提供当前上下文的副本作为新的当前上下文。在 with 子句中对当前上下文的修改,都不会影响已保存的上下文。当 with 语句结束时,此管理器会把保存的上下文重新设置为当前上下文。

这使得我们可以简单地在局部增加计算精度。

特别的,如果输入了 kwargs,则管理器提供副本时,会对副本按照 kwargs 给出的属性进行修改。

kwargs 提供了上下文对象所不支持的属性时,会引发 TypeError。如果 kwargs 提供了无效的属性值,则会引发 TypeError 或 ValueError。

decimal 的使用示例[编辑 | 编辑源代码]

Python 文档中还提供了许多 decimal 的使用示例。包括正弦、余弦等。

结合之前已学过的知识,读者可以自行撰写一个模块来实现 decimal 的数学运算,或者使用 decimal 的特性来实现更多的功能。

  1. https://docs.python.org/zh-cn/3.12/library/decimal.html#decimal.Decimal.adjusted