用戶: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.13/library/decimal.html#decimal.Decimal.adjusted