decimal --- 十進位固定點和浮點運算

原始碼:Lib/decimal.py


decimal 模組提供對快速且正確捨入的十進位浮點運算的支援。它提供了優於 float 資料型別的幾項優勢:

  • Decimal 「是基於一個浮點模型,該模型在設計時考量了人類的需求,並且必然有一個最重要的指導原則——電腦必須提供一種與人們在學校學到的算術相同運作方式的算術。」——摘自十進位算術規格。

  • Decimal 數字可以被精確表示。相對地,像 1.12.2 這樣的數字在二進位浮點數中沒有精確的表示方式。終端使用者通常不會期望 1.1 + 2.2 顯示為 3.3000000000000003,但這正是二進位浮點數的表現。

  • 精確性延續到算術運算中。在十進位浮點數中,0.1 + 0.1 + 0.1 - 0.3 完全等於零。在二進位浮點數中,結果是 5.5511151231257827e-017。雖然接近零,但這些差異會妨礙可靠的相等性測試,並且差異可能累積。因此,在具有嚴格相等不變量的會計應用程式中,decimal 是首選。

  • decimal 模組包含了有效位數的概念,因此 1.30 + 1.20 等於 2.50。尾隨零被保留以表示有效性。這是金融應用程式的慣用呈現方式。對於乘法,「教科書」方法使用被乘數中的所有數字。例如,1.3 * 1.2 得到 1.56,而 1.30 * 1.20 得到 1.5600

  • 與基於硬體的二進位浮點數不同,decimal 模組具有使用者可變的精度(預設為 28 位),可以根據給定問題的需要設定得足夠大:

    >>> from decimal import *
    >>> getcontext().prec = 6
    >>> Decimal(1) / Decimal(7)
    Decimal('0.142857')
    >>> getcontext().prec = 28
    >>> Decimal(1) / Decimal(7)
    Decimal('0.1428571428571428571428571429')
    
  • 二進位和十進位浮點數都是根據已發布的標準實作的。雖然內建的 float 型別只暴露其功能的一小部分,但 decimal 模組暴露了標準的所有必要部分。必要時,程式設計師對捨入和訊號處理有完全的控制權。這包括透過使用例外來阻止任何不精確運算以強制執行精確算術的選項。

  • decimal 模組被設計為支援「不帶偏見地同時支援精確無捨入的十進位算術(有時稱為固定點算術)和有捨入的浮點算術。」——摘自十進位算術規格。

模組設計圍繞三個概念:decimal 數字、算術情境和訊號。

decimal 數字是不可變的。它有符號、係數數字和指數。為了保持有效性,係數數字不會截斷尾隨零。Decimal 也包含特殊值,如 Infinity-InfinityNaN。標準也區分 -0+0

算術情境是一個指定精度、捨入規則、指數限制、指示運算結果的旗標,以及決定訊號是否被視為例外的陷阱啟用器的環境。捨入選項包括 ROUND_CEILINGROUND_DOWNROUND_FLOORROUND_HALF_DOWNROUND_HALF_EVENROUND_HALF_UPROUND_UPROUND_05UP

訊號是在計算過程中產生的例外條件群組。根據應用程式的需求,訊號可能被忽略、視為資訊性質,或被視為例外。decimal 模組中的訊號包括:ClampedInvalidOperationDivisionByZeroInexactRoundedSubnormalOverflowUnderflowFloatOperation

每個訊號都有一個旗標和一個陷阱啟用器。當遇到訊號時,其旗標被設定為一,然後,如果陷阱啟用器被設定為一,就會引發例外。旗標是黏性的,因此使用者需要在監控計算之前重設它們。

也參考

快速入門教學

使用 decimal 的通常起始步驟是引入模組、使用 getcontext() 檢視目前的情境,以及若有必要的話,設定精度、捨入方式或啟用陷阱的新值:

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

>>> getcontext().prec = 7       # 設定新的精度

Decimal 實例可以從整數、字串、浮點數或 tuple 建構。從整數或浮點數建構會對該整數或浮點數的值進行精確轉換。Decimal 數字包括特殊值,例如代表「非數字」的 NaN、正負 Infinity-0::

>>> getcontext().prec = 28
>>> Decimal(10)
Decimal('10')
>>> Decimal('3.14')
Decimal('3.14')
>>> Decimal(3.14)
Decimal('3.140000000000000124344978758017532527446746826171875')
>>> Decimal((0, (3, 1, 4), -2))
Decimal('3.14')
>>> Decimal(str(2.0 ** 0.5))
Decimal('1.4142135623730951')
>>> Decimal(2) ** Decimal('0.5')
Decimal('1.414213562373095048801688724')
>>> Decimal('NaN')
Decimal('NaN')
>>> Decimal('-Infinity')
Decimal('-Infinity')

如果 FloatOperation 訊號被捕捉,在建構函式或排序比較中意外混用 decimal 和 float 會引發例外:

>>> c = getcontext()
>>> c.traps[FloatOperation] = True
>>> Decimal(3.14)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
>>> Decimal('3.5') < 3.7
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.FloatOperation: [<class 'decimal.FloatOperation'>]
>>> Decimal('3.5') == 3.5
True

在 3.3 版被加入.

新 Decimal 的精度僅由輸入的數字位數決定。情境精度和捨入只會在算術運算期間發揮作用。

>>> getcontext().prec = 6
>>> Decimal('3.0')
Decimal('3.0')
>>> Decimal('3.1415926535')
Decimal('3.1415926535')
>>> Decimal('3.1415926535') + Decimal('2.7182818285')
Decimal('5.85987')
>>> getcontext().rounding = ROUND_UP
>>> Decimal('3.1415926535') + Decimal('2.7182818285')
Decimal('5.85988')

如果超過 C 版本的內部限制,建構 decimal 會引發 InvalidOperation

>>> Decimal("1e9999999999999999999")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
decimal.InvalidOperation: [<class 'decimal.InvalidOperation'>]

在 3.3 版的變更.

Decimal 與 Python 的其他部分互動良好。以下是一個小小的 decimal 浮點數展示:

>>> data = list(map(Decimal, '1.34 1.87 3.45 2.35 1.00 0.03 9.25'.split()))
>>> max(data)
Decimal('9.25')
>>> min(data)
Decimal('0.03')
>>> sorted(data)
[Decimal('0.03'), Decimal('1.00'), Decimal('1.34'), Decimal('1.87'),
 Decimal('2.35'), Decimal('3.45'), Decimal('9.25')]
>>> sum(data)
Decimal('19.29')
>>> a,b,c = data[:3]
>>> str(a)
'1.34'
>>> float(a)
1.34
>>> round(a, 1)
Decimal('1.3')
>>> int(a)
1
>>> a * 5
Decimal('6.70')
>>> a * b
Decimal('2.5058')
>>> c % a
Decimal('0.77')

Decimal 可以(透過 format() 內建函式或 f-string(f 字串))使用與內建 float 型別相同的格式化語法(見 格式規格 (Format Specification) 迷你語言)以固定點或科學記號格式化:

>>> format(Decimal('2.675'), "f")
'2.675'
>>> format(Decimal('2.675'), ".2f")
'2.68'
>>> f"{Decimal('2.675'):.2f}"
'2.68'
>>> format(Decimal('2.675'), ".2e")
'2.68e+0'
>>> with localcontext() as ctx:
...     ctx.rounding = ROUND_DOWN
...     print(format(Decimal('2.675'), ".2f"))
...
2.67

而且 Decimal 也提供一些數學函式:

>>> getcontext().prec = 28
>>> Decimal(2).sqrt()
Decimal('1.414213562373095048801688724')
>>> Decimal(1).exp()
Decimal('2.718281828459045235360287471')
>>> Decimal('10').ln()
Decimal('2.302585092994045684017991455')
>>> Decimal('10').log10()
Decimal('1')

quantize() 方法將數字捨入到固定的指數。此方法對於經常將結果捨入到固定位數的金融應用程式很有用:

>>> Decimal('7.325').quantize(Decimal('.01'), rounding=ROUND_DOWN)
Decimal('7.32')
>>> Decimal('7.325').quantize(Decimal('1.'), rounding=ROUND_UP)
Decimal('8')

如上所示,getcontext() 函式存取目前的情境並允許變更設定。這種方法滿足大多數應用程式的需求。

對於更進階的工作,使用 Context() 建構函式來建立替代的 context 可能會很有用。要啟用替代的 context,使用 setcontext() 函式。

根據標準,decimal 模組提供了兩個可直接使用的標準 context,BasicContextExtendedContext。前者對除錯特別有用,因為許多陷阱都被啟用了:

>>> myothercontext = Context(prec=60, rounding=ROUND_HALF_DOWN)
>>> setcontext(myothercontext)
>>> Decimal(1) / Decimal(7)
Decimal('0.142857142857142857142857142857142857142857142857142857142857')

>>> ExtendedContext
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
        capitals=1, clamp=0, flags=[], traps=[])
>>> setcontext(ExtendedContext)
>>> Decimal(1) / Decimal(7)
Decimal('0.142857143')
>>> Decimal(42) / Decimal(0)
Decimal('Infinity')

>>> setcontext(BasicContext)
>>> Decimal(42) / Decimal(0)
Traceback (most recent call last):
  File "<pyshell#143>", line 1, in -toplevel-
    Decimal(42) / Decimal(0)
DivisionByZero: x / 0

情境也有訊號旗標用於監控計算過程中遇到的例外條件。旗標會保持設定狀態直到明確清除,因此最好在每組受監控的計算之前使用 clear_flags() 方法清除旗標:

>>> setcontext(ExtendedContext)
>>> getcontext().clear_flags()
>>> Decimal(355) / Decimal(113)
Decimal('3.14159292')
>>> getcontext()
Context(prec=9, rounding=ROUND_HALF_EVEN, Emin=-999999, Emax=999999,
        capitals=1, clamp=0, flags=[Inexact, Rounded], traps=[])

flags 條目顯示對 pi 的有理逼近已被捨入(超出情境精度的數字被丟棄),並且結果是不精確的(一些被丟棄的數字非零)。

個別陷阱使用情境的 traps 屬性中的字典設定:

>>> setcontext(ExtendedContext)
>>> Decimal(1) / Decimal(0)
Decimal('Infinity')
>>> getcontext().traps[DivisionByZero] = 1
>>> Decimal(1) / Decimal(0)
Traceback (most recent call last):
  File "<pyshell#112>", line 1, in -toplevel-
    Decimal(1) / Decimal(0)
DivisionByZero: x / 0

大多數程式只在程式開始時調整一次目前情境。在許多應用程式中,資料在迴圈內透過單次轉換轉換為 Decimal。設定情境並建立 decimal 後,程式的大部分操作資料的方式與其他 Python 數值型別沒有什麼不同。

Decimal 物件

class decimal.Decimal(value='0', context=None)

基於 value 建構一個新的 Decimal 物件。

value 可以是整數、字串、元組、float 或其他 Decimal 物件。如果沒有提供 value,則回傳 Decimal('0')。如果 value 是字串,在移除前後空白字元以及整個字串中的底線後,它應該符合十進位數字字串語法:

sign           ::=  '+' | '-'
digit          ::=  '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
indicator      ::=  'e' | 'E'
digits         ::=  digit [digit]...
decimal-part   ::=  digits '.' [digits] | ['.'] digits
exponent-part  ::=  indicator [sign] digits
infinity       ::=  'Infinity' | 'Inf'
nan            ::=  'NaN' [digits] | 'sNaN' [digits]
numeric-value  ::=  decimal-part [exponent-part] | infinity
numeric-string ::=  [sign] numeric-value | [sign] nan

在上面出現 digit 的地方也允許其他 Unicode 十進位數字。這些包括來自各種其他字母表的十進位數字(例如阿拉伯-印度數字和天城數字)以及全形數字 '\uff10''\uff19'。大小寫不重要,所以例如 infInfINFINITYiNfINity 都是正無窮大的可接受拼寫。

如果 valuetuple,它應該有三個元件:一個符號(0 代表正數或 1 代表負數)、一個數字的 tuple,以及一個整數指數。例如,Decimal((0, (1, 4, 1, 4), -3)) 回傳 Decimal('1.414')

如果 valuefloat,二進位浮點數值會被無損轉換為其精確的十進位等價值。這種轉換通常需要 53 位或更多位數的精度。例如,Decimal(float('1.1')) 轉換為 Decimal('1.100000000000000088817841970012523233890533447265625')

context 精度不會影響儲存多少位數。這完全由 value 中的位數決定。例如,即使情境精度只有三位,Decimal('3.00000') 也會記錄所有五個零。

context 引數的目的是決定當 value 是格式不正確的字串時該怎麼辦。如果情境捕捉 InvalidOperation,就會引發例外;否則,建構函式會回傳一個值為 NaN 的新 Decimal。

一旦建構,Decimal 物件就是不可變的。

在 3.2 版的變更: 建構函式的引數現在允許是 float 實例。

在 3.3 版的變更: 如果設定了 FloatOperation 陷阱,float 引數會引發例外。預設情況下陷阱是關閉的。

在 3.6 版的變更: 允許使用底線進行分組,就像程式碼中的整數和浮點數字面值一樣。

Decimal 浮點數物件與其他內建數值型別(如 floatint)共享許多屬性。所有常用的數學運算和特殊方法都適用。同樣地,decimal 物件可以被複製、pickle 序列化、列印、用作字典鍵、用作集合元素、比較、排序,以及強制轉換為其他型別(如 floatint)。

Decimal 物件的算術運算與整數和浮點數的算術運算之間有一些小差異。當餘數運算子 % 應用於 Decimal 物件時,結果的符號是 被除數 的符號,而不是除數的符號:

>>> (-7) % 4
1
>>> Decimal(-7) % Decimal(4)
Decimal('-3')

整數除法運算子 // 的行為類似,回傳真商的整數部分(向零截斷)而不是其底數,以保持通常的恆等式 x == (x // y) * y + x % y

>>> -7 // 4
-2
>>> Decimal(-7) // Decimal(4)
Decimal('-1')

%// 運算子分別實作規格中描述的 remainderdivide-integer 運算。

Decimal 物件通常不能在算術運算中與浮點數或 fractions.Fraction 實例結合:例如,嘗試將 Decimalfloat 相加會引發 TypeError。但是,可以使用 Python 的比較運算子來比較 Decimal 實例 x 與另一個數字 y。這避免了在不同型別的數字之間進行相等比較時產生令人困惑的結果。

在 3.2 版的變更: Decimal 實例與其他數值型別之間的混合型別比較現在完全支援。

除了標準數值屬性外,decimal 浮點數物件也有許多專門的方法:

adjusted()

在移出係數的最右邊數字直到只剩下前導數字後,回傳調整後的指數:Decimal('321e+5').adjusted() 回傳七。用於確定最高有效數字相對於小數點的位置。

as_integer_ratio()

回傳一對表示給定