Python Decimal 高精度小數

在 Python 中,雖然內建的 float(浮點數)非常方便且運算速度快,但它在處理某些十進制小數時會出現「精度問題」。如果你正在開發涉及金錢計算會計系統或任何需要極高精度的小數運算,內建的 decimal 模組是你的救星。

為什麼需要 Decimal?(浮點數的陷阱)

電腦內部的浮點數是由二進制表示的,這導致某些十進制的小數無法被精確地表示。

# 浮點數的陷阱
print(0.1 + 0.2)           # 輸出: 0.30000000000000004
print(0.1 + 0.2 == 0.3)    # 輸出: False

對於一般的工程計算,這點微小的誤差通常可以接受;但對於銀行系統,這 $0.00000000000000004$ 的誤差累積起來會導致巨大的財務問題。

基礎用法:建立 Decimal

使用 Decimal 前必須先從 decimal 模組匯入。

重點: 建立 Decimal 物件時,請務必傳入字串 (String) 而非直接傳入浮點數。如果傳入浮點數,則會連同浮點數本身的誤差也一併傳入。
from decimal import Decimal

# 1. 建議做法:傳入字串
d1 = Decimal('0.1')
d2 = Decimal('0.2')
print(d1 + d2)    # 輸出: 0.3 (正確!)

# 2. 錯誤示範:直接傳入 float (還是會有誤差)
bad_d = Decimal(0.1)
print(bad_d)      # 輸出: 0.100000000000000005551115...

常用特性

1. 精確控制有效位元

你可以全域設定所有 Decimal 運算時的精確度(不包含整數部分)。

from decimal import Decimal, getcontext

# 設定全局精確度為 4 位
getcontext().prec = 4

print(Decimal('1') / Decimal('3'))  # 輸出: 0.3333

2. 多樣化的進位策略 (Rounding)

Decimal 的精髓在於 quantize() 方法,它能精確控制數字該如何「收尾」。

常見的進位模式:

模式說明
ROUND_HALF_UP常見的四捨五入
ROUND_CEILING無條件進位 (向正無窮大方向進位)。
ROUND_FLOOR無條件捨去 (向負無窮大方向捨去)。
ROUND_UP遠離零的方向進位 (無論正負,絕對值變大)。
ROUND_DOWN朝向零的方向捨去 (無論正負,絕對值變小)。
ROUND_HALF_EVEN銀行家捨入法 (五取最接近的偶數),能減少大數據計算時的累積誤差。
from decimal import Decimal, ROUND_CEILING, ROUND_FLOOR, ROUND_HALF_UP

num = Decimal('3.14159')
neg_num = Decimal('-3.14159')

# 1. 四捨五入到小數兩位
print(num.quantize(Decimal('0.00'), rounding=ROUND_HALF_UP))  # 3.14

# 2. 無條件進位 (Ceiling)
print(num.quantize(Decimal('0.00'), rounding=ROUND_CEILING)) # 3.15
print(neg_num.quantize(Decimal('0.00'), rounding=ROUND_CEILING)) # -3.14 (往大的方向走)

# 3. 無條件捨去 (Floor)
print(num.quantize(Decimal('0.00'), rounding=ROUND_FLOOR))   # 3.14
print(neg_num.quantize(Decimal('0.00'), rounding=ROUND_FLOOR)) # -3.15 (往小的方向走)

實踐範例:複雜的金融運算

假設我們正在計算一筆含稅訂單,且稅金必須強制無條件進位到整數,而總金額則需要進行四捨五入。

from decimal import Decimal, ROUND_CEILING, ROUND_HALF_UP

# 定義參數
price = Decimal('199.45')
tax_rate = Decimal('0.05')  # 5% 營業稅
quantity = 3

# 1. 計算未稅總價
subtotal = price * quantity  # 598.35

# 2. 計算稅金 (強制無條件進位到整數)
tax_raw = subtotal * tax_rate # 29.9175
tax = tax_raw.quantize(Decimal('1'), rounding=ROUND_CEILING) # 30

# 3. 計算最終總金額 (四捨五入到整數)
total = (subtotal + tax).quantize(Decimal('1'), rounding=ROUND_HALF_UP)

print(f"小計: {subtotal}")   # 598.35
print(f"稅金: {tax}")        # 30
print(f"總計: {total}")      # 628

Decimal 與 Float 的比較

特性Float (內建)Decimal (模組)
精確度二進制近似,有誤差十進制精確,無誤差
速度非常快 (硬體加速)較慢
記憶體較高
適用場景科學計算、圖形處理金融、會計、高報表精度需求

常問問題:為什麼我不能直接讓 Decimalfloat 一起運算?

因為這兩種型別背後的計算邏輯完全不同。Python 為了避免開發者不自覺地引入浮點數誤差,會禁止它們直接運算。如果非要一起算,請先將 float 轉換為 Decimal 或整數。

# 會報錯 TypeError
# Decimal('1.1') + 0.1

# 正確做法
Decimal('1.1') + Decimal(str(0.1))

總結

  • 錢財無小事:只要涉及金錢,請永遠使用 Decimal
  • 字串初始化:建立時務必使用引號,例如 Decimal('0.1')
  • 靈活進位:根據業務需求選擇 ROUND_CEILING (進位) 或 ROUND_FLOOR (捨去)。
  • 效能考量:雖然 Decimalfloat 慢,但在現代電腦上,除非你要處理數億次的科學運算,否則這點效能差異在金融應用中是可以忽略不計的。

掌握了 Decimal,你就能寫出讓會計師和客戶都放心的專業財務系統。