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 (模組) |
|---|---|---|
| 精確度 | 二進制近似,有誤差 | 十進制精確,無誤差 |
| 速度 | 非常快 (硬體加速) | 較慢 |
| 記憶體 | 低 | 較高 |
| 適用場景 | 科學計算、圖形處理 | 金融、會計、高報表精度需求 |
常問問題:為什麼我不能直接讓 Decimal 和 float 一起運算?
因為這兩種型別背後的計算邏輯完全不同。Python 為了避免開發者不自覺地引入浮點數誤差,會禁止它們直接運算。如果非要一起算,請先將 float 轉換為 Decimal 或整數。
# 會報錯 TypeError
# Decimal('1.1') + 0.1
# 正確做法
Decimal('1.1') + Decimal(str(0.1))
總結
- 錢財無小事:只要涉及金錢,請永遠使用
Decimal。 - 字串初始化:建立時務必使用引號,例如
Decimal('0.1')。 - 靈活進位:根據業務需求選擇
ROUND_CEILING(進位) 或ROUND_FLOOR(捨去)。 - 效能考量:雖然
Decimal比float慢,但在現代電腦上,除非你要處理數億次的科學運算,否則這點效能差異在金融應用中是可以忽略不計的。
掌握了 Decimal,你就能寫出讓會計師和客戶都放心的專業財務系統。