Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 327 – 十进制数据类型

作者:
Facundo Batista <facundo at taniquetil.com.ar>
状态:
最终
类型:
标准跟踪
创建:
2003年10月17日
Python 版本:
2.4
历史记录:
2003年11月30日,2004年1月2日,2004年1月29日

目录

摘要

想法是拥有一个 Decimal 数据类型,用于任何需要十进制但二进制浮点数精度不够的场合。

Decimal 数据类型将支持 Python 标准函数和运算,并且必须符合十进制算术 ANSI 标准 X3.274-1996 [1]

Decimal 将是浮点数(而不是定点数),并且具有有界精度(精度是结果中有效数字的上限)。但是,精度是用户可设置的,并且支持有效尾随零的概念,因此也可以使用定点数。

这项工作基于 Eric Price、Aahz 和 Tim Peters 编写的代码和测试函数。在 Python 2.4a1 发布前不久,decimal.py 参考实现 被移至标准库;连同文档和测试套件,这是 Raymond Hettinger 的工作。本 PEP 中的大部分解释都来自 Cowlishaw 的著作 [2]、comp.lang.python 和 python-dev。

动机

在这里,我将说明我认为为什么需要 Decimal 数据类型以及为什么其他数值数据类型不够。

我想要一个 Money 数据类型,在 comp.lang.python 中提出一个 PEP 之前,社区同意拥有一个具有所需算术行为的数值数据类型,然后在其上构建 Money:所有关于小数点后位数、舍入等的考虑都将通过 Money 处理。本 PEP 的目的不是拥有一个无需进一步努力即可用作 Money 的数据类型。

实施标准的最大优势之一是,已经有人为您考虑了所有棘手的情况。GvR 将我引导至一个标准:Mike Cowlishaw 的通用十进制算术规范 [2]。本文档定义了通用十进制算术。此规范的正确实现将符合 ANSI/IEEE 标准 854-1987 中定义的十进制算术,除了某些微小的限制外,还将提供未舍入的十进制算术和整数算术作为适当的子集。

二进制浮点数的问题

在十进制数学中,许多数字无法用固定数量的十进制数字表示,例如 1/3 = 0.3333333333……。

在以 2 为底(标准浮点数的计算方式)中,1/2 = 0.1,1/4 = 0.01,1/8 = 0.001,等等。十进制 0.2 等于 2/10 等于 1/5,导致二进制分数 0.001100110011001… 您可以看到,问题在于某些十进制数字无法精确地表示为二进制,从而导致小的舍入误差。

因此,我们需要一个十进制数据类型来精确地表示十进制数字。我们需要十进制数据类型,而不是二进制数据类型。

为什么是浮点数?

因此,我们转向十进制,但为什么是浮点数

浮点数使用固定数量的数字(精度)来表示一个数字,当数字变得太大或太小时使用指数。例如,精度为 5

  1234 ==>   1234e0
 12345 ==>  12345e0
123456 ==>  12346e1

(请注意,在最后一行,数字已舍入以适应五位数字)。

相反,我们以具有无限精度的long整数为例,这意味着您可以根据需要拥有任意大的数字,并且永远不会丢失任何信息。

在定点数中,小数点的位数是固定的。有关定点数据类型,请查看 Tim Peter 在 SourceForge 上的 FixedPoint [4]。我选择浮点数是因为更容易实现标准的算术行为,然后您可以在 Decimal 上实现定点数据类型。

但是为什么我们不能拥有无限精度的浮点数呢?这并不容易,因为除法不精确。例如:1/3 = 0.3333333333333… 无限循环。在这种情况下,您应该存储无限数量的 3,这需要太多内存,;)。

John Roth 提出消除除法运算符并强制用户使用显式方法,只是为了避免此类问题。这在 comp.lang.python 中引起了负面反应,因为每个人都希望在数值数据类型中支持/运算符。

有了这些说明,您可能在想“嘿!我们可以将 1 和 3 作为分子和分母存储吗?”,这将我们引向下一要点。

为什么不是有理数?

有理数使用两个整数(分子和分母)存储。这意味着算术运算不能直接执行(例如,要添加两个有理数,您首先需要计算公分母)。

引用 Alex Martelli

将两个有理数(分别占用 O(M) 和 O(N) 空间)相加的事实会导致占用 O(M+N) 内存空间的有理数,其性能影响实在太麻烦了。在纯 Python 和扩展(例如,gmpy)中都有优秀的 Rational 实现,但它们在我看来将永远是“利基市场”。可能值得撰写 PEP,但无需 Decimal 就没有意义——这是表示金额的正确方法,在现实世界中,这是一个真正主要的用例。

无论如何,如果您对这种数据类型感兴趣,您可能希望查看PEP 239:向 Python 添加有理数类型。

那么,我们有什么?

结果是一个具有有界精度和浮点数的 Decimal 数据类型。

它有用吗?我无法比 Alex Martelli 描述得更好

Python(开箱即用)不允许您拥有任意指定精度的二进制浮点数:您仅限于硬件提供的精度。无论是用作定点数还是浮点数,Decimal 都不会受到此类限制:您在数字创建时指定的任何有界精度(只要您的内存允许)都应该同样有效。编程简单性的大部分开销可以隐藏在应用程序程序之外,并放置在一个合适的十进制算术类型中。根据http://speleotrove.com/decimal/单个数据类型可用于整数、定点和浮点十进制算术——以及不会让应用程序程序员抓狂的货币算术。

这种数据类型有几个用途。如前所述,我将用它作为 Money 的基础。在这种情况下,有界精度不是问题;引用 Tim Peters

20 位的精度足以表示自有史以来世界经济总产出,精确到便士。

通用十进制算术规范

在这里,我将包含规范 [2](数字的结构、上下文等)中包含的信息和描述。本节中包含的所有要求都不需要讨论(除了拼写错误或其他错误),因为它们都在标准中,而 PEP 只是为了实现标准。

由于版权限制,我不能在此处复制从规范中提取的解释,因此我将尝试用我自己的话解释它。我强烈建议您阅读原始规范文档 [2] 以获取详细信息或如果您有任何疑问。

算术模型

该规范基于十进制算术模型,如相关标准所定义:IEEE 854 [3]、ANSI X3-274 [1] 和 IEEE 754 的拟议修订版 [5] [6]

该模型有三个组成部分

  • 数字:运算使用作为输入或输出的值。
  • 运算:加法、乘法等。
  • 上下文:用户可以选择的一组参数和规则,这些参数和规则控制运算的结果(例如,要使用的精度)。

数字

数字可能是有限的或特殊值。前者可以精确表示。后者是无穷大和未定义的(例如 0/0)。

有限数字由三个参数定义

  • 符号:0(正)或 1(负)。
  • 系数:非负整数。
  • 指数:带符号整数,系数乘数的 10 的幂。

有限数字的数值由下式给出

(-1)**sign * coefficient * 10**exponent

特殊值的命名如下

  • 无穷大:无限大的值。可以是正的或负的。
  • 静默 NaN(“qNaN”):表示未定义的结果(非数字)。不会导致无效操作条件。NaN 中的符号没有意义。
  • 信号 NaN(“sNaN”):也是非数字,但如果在任何运算中使用,将导致无效操作条件。

上下文

上下文是一组用户可以选择并控制运算结果的参数和规则(例如,要使用的精度)。

上下文之所以这样命名,是因为它围绕着 Decimal 数字,上下文的部分作为运算的输入和输出。应用程序可以处理一个或多个上下文,但最终的想法不是为每个 Decimal 数字获取一个上下文。例如,典型用法是在程序开始时将上下文的精度设置为 20 位,并且以后不再显式使用上下文。

这些定义不会影响 Decimal 数字的内部存储,只会影响算术运算的执行方式。

上下文主要由以下参数定义(有关所有上下文属性,请参阅上下文属性

  • 精度:算术运算可能产生的最大有效数字数(整数 > 0)。此值没有最大值。
  • 舍入:当需要舍入时要使用的算法的名称,其中之一为“round-down”、“round-half-up”、“round-half-even”、“round-ceiling”、“round-floor”、“round-half-down”和“round-up”。请参阅下面的舍入算法

  • 标志和陷阱使能器:异常情况被分组为信号,可以单独控制,每个信号都包含一个标志(布尔值,当信号发生时设置)和一个陷阱使能器(控制行为的布尔值)。信号包括:“钳位”、“除以零”、“不精确”、“无效操作”、“溢出”、“舍入”、“次正规”和“下溢”。

默认上下文

规范定义了两种默认上下文,用户应该能够轻松地选择它们。

基本默认上下文

  • 标志:全部设置为 0
  • 陷阱使能器:不精确、舍入和次正规设置为 0;其他全部设置为 1
  • 精度:设置为 9
  • 舍入:设置为四舍五入

扩展默认上下文

  • 标志:全部设置为 0
  • 陷阱使能器:全部设置为 0
  • 精度:设置为 9
  • 舍入:设置为四舍六入

异常情况

下表列出了算术运算期间可能出现的异常情况、相应的信号和定义的结果。有关详细信息,请参阅规范 [2]

条件 信号 结果
钳位 clamped 参见规范 [2]
除以零 division-by-zero [符号,inf]
不精确 inexact 不变
无效操作 invalid-operation [0,qNaN](或 [s,qNaN] 或 [s,qNaN,d],当原因是信号 NaN 时)
溢出 overflow 取决于舍入模式
舍入 rounded 不变
次正规 subnormal 不变
下溢 underflow 参见规范 [2]

注意:当标准谈到“存储不足”时,只要这是关于没有足够存储空间来保存数字内部结构的特定于实现的行为,此实现将引发 MemoryError。

关于溢出和下溢,在 python-dev 中关于人为限制进行了长时间的讨论。普遍共识是仅在有重要理由这样做的情况下才保留人为限制。Tim Peters 给我们提供了三个理由

…实际上消除了指数的界限,意味着溢出(和下溢)永远不会发生。但溢出现实生活中浮点数使用的宝贵安全网,就像煤矿中的金丝雀一样,在程序变得疯狂时尽早发出危险信号。

几乎所有 854 的实现都使用(并且正如 IBM 的标准甚至建议的那样)“禁止”的指数值来编码非有限数字(无穷大和 NaN)。有界指数可以在几乎不增加存储成本的情况下做到这一点。如果指数是无界的,则必须使用额外的位。这种成本在尝试更高效的时/空实现之前一直隐藏着。

IBM 标准虽然庞大,但仅仅是提供完整数值功能的微小开端。指数大小没有界限将极大地复杂化例如十进制 sin() 和 cos() 的实现(然后就没有先验的限制来确定为了执行参数缩减而需要知道多少位 pi)。

Edward Loper 为我们提供了一个需要跨越限制的例子:概率。

也就是说,Robert Brewer 和 Andrew Lentvorski 希望用户能够轻松地修改这些限制。实际上,这是完全可能的

>>> d1 = Decimal("1e999999999")     # at the exponent limit
>>> d1
Decimal("1E+999999999")
>>> d1 * 10                         # exceed the limit, got infinity
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in ?
    d1 * 10
  ...
  ...
Overflow: above Emax
>>> getcontext().Emax = 1000000000  # increase the limit
>>> d1 * 10                         # does not exceed any more
Decimal("1.0E+1000000000")
>>> d1 * 100                        # exceed again
Traceback (most recent call last):
  File "<pyshell#3>", line 1, in ?
    d1 * 100
  ...
  ...
Overflow: above Emax

舍入算法

round-down:丢弃的数字被忽略;结果不变(向 0 舍入,截断)

1.123 --> 1.12
1.128 --> 1.12
1.125 --> 1.12
1.135 --> 1.13

round-half-up:如果丢弃的数字表示大于或等于一半(0.5),则结果应增加 1;否则丢弃的数字将被忽略

1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.13
1.135 --> 1.14

round-half-even:如果丢弃的数字表示大于一半(0.5),则结果系数增加 1;如果它们表示小于一半,则结果不调整;否则,如果结果的最右边的数字为偶数,则结果保持不变,如果结果的最右边的数字为奇数,则结果增加 1(以使数字为偶数)

1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.12
1.135 --> 1.14

round-ceiling:如果所有丢弃的数字都为零,或者符号为负,则结果不变;否则,结果增加 1(向正无穷大舍入)

 1.123 -->  1.13
 1.128 -->  1.13
-1.123 --> -1.12
-1.128 --> -1.12

round-floor:如果所有丢弃的数字都为零,或者符号为正,则结果不变;否则,结果的绝对值增加 1(向负无穷大舍入)

 1.123 -->  1.12
 1.128 -->  1.12
-1.123 --> -1.13
-1.128 --> -1.13

round-half-down:如果丢弃的数字表示大于一半(0.5),则结果增加 1;否则丢弃的数字将被忽略

1.123 --> 1.12
1.128 --> 1.13
1.125 --> 1.12
1.135 --> 1.13

round-up:如果所有丢弃的数字都为零,则结果不变,否则结果增加 1(远离 0 舍入)

1.123 --> 1.13
1.128 --> 1.13
1.125 --> 1.13
1.135 --> 1.14

基本原理

我必须将需求分为两个部分。第一个是遵守 ANSI 标准。所有这些要求都指定在 Mike Cowlishaw 的工作 [2] 中。他还提供了一套非常庞大的测试用例。

第二部分需求(标准 Python 函数支持、可用性等)从这里详细说明,我将包含所有做出的决策以及原因,以及所有仍在讨论的主题。

显式构造

显式构造不受上下文的影响(没有舍入,没有精度限制等),因为上下文仅影响操作的结果。唯一的例外是当您从上下文创建时。

从 int 或 long

没有损失,也不需要指定任何其他信息

Decimal(35)
Decimal(-124)

从字符串

将支持包含 Python 十进制整数字面量和 Python 浮点字面量的字符串。在此转换中,不会丢失信息,因为字符串直接转换为 Decimal(没有通过浮点数进行中间转换)

Decimal("-12")
Decimal("23.2e-7")

此外,您可以通过这种方式构造所有特殊值(无穷大和非数字)

Decimal("Inf")
Decimal("NaN")

从浮点数

关于此项目的初始讨论是将浮点数传递给构造函数时应该发生什么

  1. Decimal(1.1) == Decimal('1.1')
  2. Decimal(1.1) == Decimal('110000000000000008881784197001252...e-51')
  3. 引发异常

一些人声称 (1) 是这里更好的选择,因为这是您在编写 Decimal(1.1) 时所期望的。引用 John Roth 的话,它很容易实现

找到实际数字结束的位置和模糊开始的位置一点也不难。您可以通过视觉方式来实现,并且执行此操作的算法非常有名。

但是如果我真的希望我的数字是 Decimal('110000000000000008881784197001252...e-51'),为什么我不能写 Decimal(1.1)?为什么我应该期望 Decimal 对其进行“舍入”?请记住,1.1二进制浮点数,因此我可以预测结果。对于初学者来说,这并不直观,但事实就是这样。

无论如何,Paul Moore 表明 (1) 行不通,因为

(1) says  D(1.1) == D('1.1')
but       1.1 == 1.1000000000000001
so        D(1.1) == D(1.1000000000000001)
together: D(1.1000000000000001) == D('1.1')

这是错误的,因为如果我写 Decimal('1.1') 它就是精确的,而不是 D(1.1000000000000001)。他还建议对浮点数进行显式转换。bokr 说您需要在构造函数中放入精度,mwilson 表示同意

d = Decimal (1.1, 1)  # take float value to 1 decimal place
d = Decimal (1.1)  # gets `places` from pre-set context

但 Alex Martelli 说

使用某些指定的精度进行构造会很好。因此,我认为“使用某些默认精度从浮点数构造”存在很大的欺骗天真用户的风险。

因此,通过 c.l.p 接受的解决方案是您不能使用浮点数调用 Decimal。相反,您必须使用一个方法:Decimal.from_float()。语法

Decimal.from_float(floatNumber, [decimal_places])

其中 floatNumber 是构造的浮点数来源,decimal_places 是小数点后应用四舍五入舍入的位数(如果有)。通过这种方式,您可以例如

Decimal.from_float(1.1, 2): The same as doing Decimal('1.1').
Decimal.from_float(1.1, 16): The same as doing Decimal('1.1000000000000001').
Decimal.from_float(1.1): The same as doing Decimal('1100000000000000088817841970012523233890533447265625e-51').

根据后续讨论,决定从 Py2.4 的 API 中省略 from_float()。一些想法促成了这个思考过程

  • 十进制和二进制浮点数之间的交互迫使用户处理表示和舍入的棘手问题。避免这些问题是模块存在的首要原因。
  • 模块的第一个版本应侧重于安全、最少和必要的方面。
  • 虽然理论上很好,但在现实世界中,浮点数和十进制数之间交互的使用案例却很少。Java 包含 float/decimal 转换以处理一个模糊的案例,其中即使遗留数据结构要求输入和输出存储在二进制浮点数中,计算也最好在十进制中执行。
  • 如果需要,用户可以使用字符串表示作为中间类型。这种方法的优点是它明确了关于精度和表示的假设(无需猜测幕后发生了什么)。
  • BigDecimal(double val) 的 Java 文档反映了他们使用构造函数的经验
    The results of this constructor can be somewhat
    unpredictable and its use is generally not recommended.
    

从元组

Aahz 建议从元组构造:它更容易实现 eval() 的往返过程,并且“拥有表示十进制数的数值的人不需要将它们转换为字符串。”

结构将是一个包含三个元素的元组:符号、数字和指数。符号为 1 或 0,数字为十进制数字的元组,指数为带符号的 int 或 long

Decimal((1, (3, 2, 2, 5), -2))     # for -32.25

当然,您可以通过这种方式构造所有特殊值

Decimal( (0, (0,), 'F') )          # for Infinity
Decimal( (0, (0,), 'n') )          # for Not a Number

从 Decimal

这里没有神秘之处,只是一个副本。

所有情况的语法

Decimal(value1)
Decimal.from_float(value2, [decimal_places])

其中 value1 可以是 int、long、string、3 元组或 Decimal,value2 只能是 float,decimal_places 是一个可选的非负整数。

从上下文创建

此项目在 python-dev 中来自两个并行的来源。Ka-Ping Yee 建议在实例创建时将上下文作为参数传递(他希望传递的上下文仅在创建时使用:“它不会持久”。)Tony Meyer 要求 from_string 如果收到参数“honour_context”且值为 True,则遵守上下文。(我不喜欢它,因为文档指定要遵守上下文,并且我不希望该方法根据参数的值来遵守规范。)

Tim Peters 给我们提供了使用上下文的创建的理由

在一般的数值计算中,字面量可以给出高精度,但这种精度不是免费的,而且通常不需要

Casey Duncan 想要使用另一种方法,而不是布尔参数

我发现布尔参数是一种通用的反模式,特别是考虑到我们有类方法。为什么不使用像 Decimal.rounded_to_context(“3.14159265”) 这样的替代构造函数呢?

在决定其语法的过程中,Tim 想出了一个更好的主意:他建议不要在 Decimal 中使用方法来使用不同的上下文创建,而是改为在 Context 中使用方法来创建 Decimal 实例。基本上,不是

D.using_context(number, context)

它将是

context.create_decimal(number)

来自 Tim

规范中除了两个字符串转换操作之外的所有操作都使用上下文,但规范中没有操作支持可选的本地上下文。Decimal() 构造函数默认忽略上下文是规范的扩展。我们必须提供一个符合上下文的字符串转换操作来满足规范。我建议在任何操作中都不要使用“本地上下文”的概念——它会使模型复杂化,而且没有必要。

因此,我们决定使用上下文方法来创建一个 Decimal,该 Decimal 将仅在特定上下文中使用(仅用于创建)(在后续操作中,它将使用线程的上下文)。但是,使用什么名称的方法呢?

Tim Peters 提出了三种从不同来源创建的方法(from_string、from_int、from_float)。我建议使用一种方法,create_decimal(),而不关心数据类型。Michael Chermside:“这个名字很符合我的思维方式。它使用上下文的事实从它是 Context 方法这一事实中就很明显了”。

社区对此表示赞同。我认为这没问题,因为新手不会从 Context 中使用创建方法(Decimal 中用于从浮点数构造的单独方法只是为了防止新手遇到二进制浮点数问题)。

因此,简而言之,如果您想使用特定上下文创建 Decimal 实例(该上下文仅在创建时使用,而不在任何其他时间使用),则必须使用该上下文的某个方法。

# n is any datatype accepted in Decimal(n) plus float
mycontext.create_decimal(n)

示例

>>> # create a standard decimal instance
>>> Decimal("11.2233445566778899")
Decimal("11.2233445566778899")
>>>
>>> # create a decimal instance using the thread context
>>> thread_context = getcontext()
>>> thread_context.prec
28
>>> thread_context.create_decimal("11.2233445566778899")
Decimal("11.2233445566778899")
>>>
>>> # create a decimal instance using other context
>>> other_context = thread_context.copy()
>>> other_context.prec = 4
>>> other_context.create_decimal("11.2233445566778899")
Decimal("11.22")

隐式构造

由于隐式构造是操作的结果,因此它将受到上下文的影響,如每个要点中所述。

John Roth 建议“其他类型应该以 decimal() 构造函数处理它的方式相同的方式处理”。但 Alex Martelli 认为

这种完全违反 Python 传统的行为将是一个可怕的错误。23+“43” 的处理方式与 23+int(“45”) 不同,而且这非常好。对于用户来说,显式地指示他们想要构造(转换)与仅仅碰巧对两个对象求和(其中一个错误地可能是字符串)是完全不同的。

因此,在这里我再次为每种数据类型定义行为。

从 int 或 long

int 或 long 被视为从 Decimal(str(x)) 在当前上下文中显式构造的 Decimal(这意味着应用了舍入的字符串转换规则,并设置了相应的标志)。这保证了像 Decimal('1234567') + 13579 这样的表达式与 Decimal('1234567') + Decimal('13579') 的心理模型相匹配。该模型有效,因为所有整数都可以在没有表示错误的情况下表示为字符串。

从字符串

每个人都同意在这里引发异常。

从浮点数

Aahz 强烈反对与浮点数交互,建议进行显式转换。

问题在于 Decimal 能够提供比浮点数更高的精度、准确性和范围。

有效的 Python 表达式 35 + 1.1 的示例似乎表明 Decimal(35) + 1.1 也应该有效。但是,仔细观察表明它仅演示了整数到浮点数转换的可行性。因此,十进制浮点数的正确类比是 35 + Decimal(1.1)。两种强制转换,int-to-float 和 int-to-Decimal,都可以在不产生表示错误的情况下完成。

二进制浮点数和十进制浮点数之间如何强制转换的问题更为复杂。我建议允许与浮点数交互,进行精确转换,并在超过当前上下文的精度时引发 ValueError(这可能过于复杂,例如,当精度为 9 时,Decimal(35) + 1.2 可以,但 Decimal(35) + 1.1 会引发错误)。

这证明过于复杂了。复杂到 c.l.p 同意在这种情况下引发 TypeError:不能混合使用 Decimal 和浮点数。

从 Decimal

这里没有任何问题。

上下文的使用

在上次 PEP 预稿中,我说过“上下文必须无所不在,这意味着对其的更改会影响所有当前和未来的 Decimal 实例”。我错了。作为回应,John Roth 说

上下文应该可以针对特定用法进行选择。也就是说,应用程序应该能够同时使用多个不同的上下文。

在 comp.lang.python 中,Aahz 解释说,这个想法是“每个线程一个上下文”。因此,线程的所有实例都属于一个上下文,并且您可以更改线程 A 中的上下文(以及该线程实例的行为),而不会更改线程 B 中的任何内容。

此外,再次纠正我,他说

(该)上下文仅适用于操作,而不适用于 Decimal 实例;如果不对现有实例进行任何操作,则更改上下文不会影响它们。

在讨论需要使用与当前上下文规则不同的其他规则执行操作的特殊情况时,Tim Peters 说上下文将把操作作为方法。这样,用户“可以创建它需要的任何私有上下文对象,并将算术运算写成对其私有上下文对象的显式方法调用,以便不会咨询或修改默认的线程上下文对象”。

Python 可用性

  • Decimal 应该支持以下情况下的基本算术运算(+, -, *, /, //, **, %, divmod)和比较运算(==, !=, <, >, <=, >=, cmp)(查看隐式构造以了解 OtherType 可以是什么类型,以及每种情况下会发生什么)
    • Decimal op Decimal
    • Decimal op otherType
    • otherType op Decimal
    • Decimal op= Decimal
    • Decimal op= otherType
  • Decimal 应该支持一元运算符(-, +, abs)。
  • repr() 应该往返,这意味着
    m = Decimal(...)
    m == eval(repr(m))
    
  • Decimal 应该是不可变的。
  • Decimal 应该支持内置方法
    • min、max
    • float、int、long
    • str、repr
    • hash
    • bool(0 为假,否则为真)

在 python-dev 中,关于 hash() 行为有一些讨论。社区一致认为,如果值相同,则这些值的哈希值也应该相同。因此,虽然 Decimal(25) == 25 为 True,但 hash(Decimal(25)) 应该等于 hash(25)。

细节是您不能将 Decimal 与浮点数或字符串进行比较,因此我们不必担心它们产生相同的哈希值。简而言之

hash(n) == hash(Decimal(n))   # Only if n is int, long, or Decimal

关于 str() 和 repr() 行为,Ka-Ping Yee 建议 repr() 的行为与 str() 相同,而 Tim Peters 建议 str() 的行为类似于规范中的 to-scientific-string 操作。

这是可能的,因为(来自 Aahz):“字符串形式已经包含了重建 Decimal 对象所需的所有信息”。

并且它也符合规范;Tim Peters

没有要求必须有一个名为“to_sci_string”的方法,唯一的要求是必须提供某种方法来表达 to-sci-string 的功能。to-sci-string 的含义由标准精确指定,并且是 str(Decimal) 和 repr(Decimal) 的良好选择。

文档

本节解释了 Decimal 和 Context 的所有公共方法和属性。

Decimal 属性

Decimal 没有公共属性。内部信息存储在插槽中,不应由最终用户访问。

Decimal 方法

以下是规范中定义的转换和算术运算,以及如何使用实际实现来实现该功能。

  • to-scientific-string:使用内置函数 str()
    >>> d = Decimal('123456789012.345')
    >>> str(d)
    '1.23456789E+11'
    
  • to-engineering-string:使用方法 to_eng_string()
    >>> d = Decimal('123456789012.345')
    >>> d.to_eng_string()
    '123.456789E+9'
    
  • to-number:使用 Context 方法 create_decimal()。不能使用标准构造函数或 from_float() 构造函数,因为这些构造函数不使用上下文(如规范中为此转换指定的)。
  • abs:使用内置函数 abs()
    >>> d = Decimal('-15.67')
    >>> abs(d)
    Decimal('15.67')
    
  • add:使用运算符 +
    >>> d = Decimal('15.6')
    >>> d + 8
    Decimal('23.6')
    
  • subtract:使用运算符 -
    >>> d = Decimal('15.6')
    >>> d - 8
    Decimal('7.6')
    
  • compare:使用方法 compare()。此方法(而不是内置函数 cmp())仅应在处理特殊值时使用。
    >>> d = Decimal('-15.67')
    >>> nan = Decimal('NaN')
    >>> d.compare(23)
    '-1'
    >>> d.compare(nan)
    'NaN'
    >>> cmp(d, 23)
    -1
    >>> cmp(d, nan)
    1
    
  • divide:使用运算符 /
    >>> d = Decimal('-15.67')
    >>> d / 2
    Decimal('-7.835')
    
  • divide-integer:使用运算符 //
    >>> d = Decimal('-15.67')
    >>> d // 2
    Decimal('-7')
    
  • max:使用方法 max()。仅在处理特殊值时使用此方法(而不是内置函数 max())。
    >>> d = Decimal('15')
    >>> nan = Decimal('NaN')
    >>> d.max(8)
    Decimal('15')
    >>> d.max(nan)
    Decimal('NaN')
    
  • min:使用方法 min()。仅在处理特殊值时使用此方法(而不是内置函数 min())。
    >>> d = Decimal('15')
    >>> nan = Decimal('NaN')
    >>> d.min(8)
    Decimal('8')
    >>> d.min(nan)
    Decimal('NaN')
    
  • minus:使用一元运算符 -
    >>> d = Decimal('-15.67')
    >>> -d
    Decimal('15.67')
    
  • plus:使用一元运算符 +
    >>> d = Decimal('-15.67')
    >>> +d
    Decimal('-15.67')
    
  • multiply:使用运算符 *
    >>> d = Decimal('5.7')
    >>> d * 3
    Decimal('17.1')
    
  • normalize:使用方法 normalize()
    >>> d = Decimal('123.45000')
    >>> d.normalize()
    Decimal('123.45')
    >>> d = Decimal('120.00')
    >>> d.normalize()
    Decimal('1.2E+2')
    
  • quantize:使用方法 quantize()
    >>> d = Decimal('2.17')
    >>> d.quantize(Decimal('0.001'))
    Decimal('2.170')
    >>> d.quantize(Decimal('0.1'))
    Decimal('2.2')
    
  • remainder:使用运算符 %
    >>> d = Decimal('10')
    >>> d % 3
    Decimal('1')
    >>> d % 6
    Decimal('4')
    
  • remainder-near:使用方法 remainder_near()
    >>> d = Decimal('10')
    >>> d.remainder_near(3)
    Decimal('1')
    >>> d.remainder_near(6)
    Decimal('-2')
    
  • round-to-integral-value:使用方法 to_integral()
    >>> d = Decimal('-123.456')
    >>> d.to_integral()
    Decimal('-123')
    
  • same-quantum:使用方法 same_quantum()
    >>> d = Decimal('123.456')
    >>> d.same_quantum(Decimal('0.001'))
    True
    >>> d.same_quantum(Decimal('0.01'))
    False
    
  • square-root:使用方法 sqrt()
    >>> d = Decimal('123.456')
    >>> d.sqrt()
    Decimal('11.1110756')
    
  • power:使用运算符 **
    >>> d = Decimal('12.56')
    >>> d ** 2
    Decimal('157.7536')
    

以下是其他方法及其存在的原因

  • adjusted():返回调整后的指数。此概念在规范中定义:当数字以科学记数法表示(小数点前一位数字)时,调整后的指数是数字指数的值。
    >>> d = Decimal('12.56')
    >>> d.adjusted()
    1
    
  • from_float():类方法,用于从浮点数据类型创建实例。
    >>> d = Decimal.from_float(12.35)
    >>> d
    Decimal('12.3500000')
    
  • as_tuple():显示 Decimal 的内部结构,即三元组。规范不需要此方法,但 Tim Peters 提出了它,并且社区同意使用它(它对开发和调试很有用)。
    >>> d = Decimal('123.4')
    >>> d.as_tuple()
    (0, (1, 2, 3, 4), -1)
    >>> d = Decimal('-2.34e5')
    >>> d.as_tuple()
    (1, (2, 3, 4), 3)
    

上下文属性

这些是可以更改以修改上下文的属性。

  • prec(int):精度
    >>> c.prec
    9
    
  • rounding(str):舍入类型(如何舍入)
    >>> c.rounding
    'half_even'
    
  • trap_enablers(dict):如果 trap_enablers[exception] = 1,则在发生异常时引发异常。
    >>> c.trap_enablers[Underflow]
    0
    >>> c.trap_enablers[Clamped]
    0
    
  • flags(dict):当发生异常时,flags[exception] 会递增(无论是否设置了 trap_enabler)。应由 Decimal 实例的用户重置。
    >>> c.flags[Underflow]
    0
    >>> c.flags[Clamped]
    0
    
  • Emin(int):最小指数
    >>> c.Emin
    -999999999
    
  • Emax(int):最大指数
    >>> c.Emax
    999999999
    
  • capitals (int):布尔标志,用于在字符串中使用“E”(True/1)或“e”(False/0)(例如,“1.32e+2”或“1.32E+2”)
    >>> c.capitals
    1
    

上下文方法

以下方法符合规范中 Decimal 的功能。请注意,通过特定上下文调用的操作使用该上下文,而不是线程上下文。

要使用这些方法,请注意当运算符是二元或一元时语法会发生变化,例如

>>> mycontext.abs(Decimal('-2'))
'2'
>>> mycontext.multiply(Decimal('2.3'), 5)
'11.5'

因此,以下是规范中的操作和转换,以及如何通过上下文实现它们(其中d 是 Decimal 实例,n 是可在隐式构造中使用的数字)

  • 转换为科学计数法字符串:to_sci_string(d)
  • 转换为工程计数法字符串:to_eng_string(d)
  • 转换为数字:create_decimal(number),有关number,请参见显式构造
  • 绝对值:abs(d)
  • 加法:add(d, n)
  • 减法:subtract(d, n)
  • 比较:compare(d, n)
  • 除法:divide(d, n)
  • 整数除法:divide_int(d, n)
  • 最大值:max(d, n)
  • 最小值:min(d, n)
  • 负数:minus(d)
  • 正数:plus(d)
  • 乘法:multiply(d, n)
  • 规范化:normalize(d)
  • 量化:quantize(d, d)
  • 求余:remainder(d)
  • 最近余数:remainder_near(d)
  • 四舍五入到整数:to_integral(d)
  • 相同量子:same_quantum(d, d)
  • 平方根:sqrt(d)
  • 幂运算:power(d, n)

divmod(d, n) 方法通过 Context 支持十进制功能。

这些方法从 Context 返回有用的信息

  • Etiny():考虑精度时的最小指数。
    >>> c.Emin
    -999999999
    >>> c.Etiny()
    -1000000007
    
  • Etop():考虑精度时的最大指数。
    >>> c.Emax
    999999999
    >>> c.Etop()
    999999991
    
  • copy():返回上下文的副本。

参考实现

从 Python 2.4-alpha 开始,代码已检入标准库。最新版本可从以下网址获取:

http://svn.python.org/view/python/trunk/Lib/decimal.py

测试用例位于此处

http://svn.python.org/view/python/trunk/Lib/test/test_decimal.py

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0327.rst

上次修改时间:2023-09-09 17:39:29 GMT