02. 数値の正確な扱い
テーマ: 消費税の内税・外税と端数処理
学習点: float の誤差, decimal.Decimal, 丸めモード(ROUND_HALF_UP / ROUND_HALF_EVEN)
依存: 標準ライブラリのみ / 難易度: 基礎
実行方法
uv run 02_decimal_tax.py
スクリプト冒頭の PEP 723 メタデータ(# /// script)により、必要なライブラリは
uv が自動的に仮想環境へ導入します。事前の pip install は不要です。
解説
何をするプログラムか
お金の計算では「1 円の誤差」も許されません。ところが Python の float で 0.1 + 0.2 を計算すると 0.30000000000000004 になり、0.3 と一致しません。このスクリプトはまずその現象を実演し、会計計算の正攻法である decimal.Decimal を使って、消費税の外税計算(本体価格 → 税額・税込価格)、内税の逆算(税込価格 → 本体価格・税額)、端数 0.5 円が発生するときの丸めモードの違い、軽減税率 8% と標準税率 10% が混在するレシートの合計、という実務でそのまま登場する 4 つの計算を行います。
コードの読みどころ
- 税率は
RATE = Decimal("0.10")のように文字列から Decimal を作っています。Decimal(0.1)と書くと float の誤差を取り込んでしまうため、文字列渡しが鉄則です。 yen()関数が丸め処理を一手に引き受けます。x.quantize(Decimal("1"), rounding=mode)は「1 円単位に、指定モードで丸める」という意味で、丸め方針をコード上で明示できるのが Decimal の強みです。tax_exclusive()とtax_inclusive()はtuple[Decimal, Decimal]を返し、呼び出し側でtax, total = ...とアンパックします。内税の逆算はtotal / (1 + rate)を丸めてから差額を税とする、実務どおりの手順です。- 丸めモード比較の節では、同じ
2.5がROUND_HALF_UP(四捨五入)で 3、ROUND_HALF_EVEN(銀行家丸め)で 2 になることを確認します。 - レシート計算の
sum(yen(p * r) for _, p, r in items)は、商品ごとに税額を丸めてから合計しています。丸めのタイミング(明細ごとか合計後か)で結果が変わり得ることを意識させる書き方です。
理論的背景
float は IEEE 754 の二進浮動小数点です。0.1 は十進では有限桁でも二進では無限循環小数になるため、有限の 64 ビットに収めた時点で必ず誤差が生じます。1 回の計算では 10⁻¹⁷ 程度の微小な差でも、比較(==)や大量の集計では顕在化します。そこで会計処理では十進で正確に表現できる Decimal を使うのが原則です。
丸めモードのうち ROUND_HALF_EVEN(端数 0.5 は偶数側に丸める)は、常に切り上げる四捨五入が持つ上方向への系統的な偏りを打ち消すため、統計・金融の集計で好まれ、IEEE 754 の既定の丸め方式でもあります。
実行結果の見方
冒頭の [float の誤差] で False と 0.30000000000000004 が表示され、同じ計算が Decimal では True になることが出発点です。[丸めモードの違い] では 0.5・2.5 が四捨五入と銀行家丸めで異なる値になり(1.5・3.5 は一致)、偶数側へ丸めるという規則が確認できます。最後のレシートでは、税率の異なる 3 商品の税額が明細ごとに丸められ、小計 1,050 円・消費税 90 円・合計 1,140 円と 1 円の狂いもなく集計されています。
ソースコード
# /// script
# requires-python = ">=3.11"
# dependencies = []
# ///
"""02: 数値の正確な扱い ---------------------------------------------------
テーマ: 消費税の内税・外税と端数処理
学習点: float の誤差, decimal.Decimal, 丸めモード(ROUND_HALF_UP / ROUND_HALF_EVEN)
根拠: 二進浮動小数点(IEEE 754)では 0.1 を有限桁で表現できないため誤差が出る。
会計処理では十進固定小数点(Decimal)を使うのが原則。
"""
from decimal import Decimal, ROUND_HALF_UP, ROUND_HALF_EVEN, getcontext
getcontext().prec = 28
RATE = Decimal("0.10") # 標準税率10%
REDUCED = Decimal("0.08") # 軽減税率8%
def yen(x: Decimal, mode=ROUND_HALF_UP) -> Decimal:
"""円未満を指定モードで丸める(1円単位)。"""
return x.quantize(Decimal("1"), rounding=mode)
def tax_exclusive(price: Decimal, rate: Decimal = RATE) -> tuple[Decimal, Decimal]:
"""外税: 本体価格から税額と税込価格を返す。"""
tax = yen(price * rate)
return tax, price + tax
def tax_inclusive(total: Decimal, rate: Decimal = RATE) -> tuple[Decimal, Decimal]:
"""内税: 税込価格から本体価格と税額を逆算する。"""
body = yen(total / (1 + rate))
return body, total - body
def main() -> None:
print("[float の誤差]")
print(" 0.1 + 0.2 == 0.3 ->", 0.1 + 0.2 == 0.3, " 実際:", repr(0.1 + 0.2))
print(" Decimal では ->", Decimal("0.1") + Decimal("0.2") == Decimal("0.3"))
print("\n[外税計算]")
for p in ["980", "1250", "3333", "105"]:
tax, total = tax_exclusive(Decimal(p))
print(f" 本体 {p:>5}円 -> 税 {tax:>4}円 / 税込 {total:>5}円")
print("\n[内税からの逆算]")
for t in ["1078", "1000", "550"]:
body, tax = tax_inclusive(Decimal(t))
print(f" 税込 {t:>5}円 -> 本体 {body:>5}円 / 税 {tax:>4}円")
print("\n[丸めモードの違い] 0.5円が発生するケース")
for v in ["0.5", "1.5", "2.5", "3.5"]:
d = Decimal(v)
print(f" {v}: 四捨五入={yen(d, ROUND_HALF_UP)} "
f"銀行家丸め={yen(d, ROUND_HALF_EVEN)}")
print("\n[軽減税率との混在レシート]")
items = [("弁当", Decimal("580"), REDUCED), ("お茶", Decimal("150"), REDUCED),
("文具", Decimal("320"), RATE)]
subtotal = sum(p for _, p, _ in items)
tax_sum = sum(yen(p * r) for _, p, r in items)
for n, p, r in items:
print(f" {n:<4}{p:>5}円 (税率{r:.0%})")
print(f" 小計 {subtotal}円 / 消費税 {tax_sum}円 / 合計 {subtotal + tax_sum}円")
if __name__ == "__main__":
main()
実行結果
[float の誤差]
0.1 + 0.2 == 0.3 -> False 実際: 0.30000000000000004
Decimal では -> True
[外税計算]
本体 980円 -> 税 98円 / 税込 1078円
本体 1250円 -> 税 125円 / 税込 1375円
本体 3333円 -> 税 333円 / 税込 3666円
本体 105円 -> 税 11円 / 税込 116円
[内税からの逆算]
税込 1078円 -> 本体 980円 / 税 98円
税込 1000円 -> 本体 909円 / 税 91円
税込 550円 -> 本体 500円 / 税 50円
[丸めモードの違い] 0.5円が発生するケース
0.5: 四捨五入=1 銀行家丸め=0
1.5: 四捨五入=2 銀行家丸め=2
2.5: 四捨五入=3 銀行家丸め=2
3.5: 四捨五入=4 銀行家丸め=4
[軽減税率との混在レシート]
弁当 580円 (税率8%)
お茶 150円 (税率8%)
文具 320円 (税率10%)
小計 1050円 / 消費税 90円 / 合計 1140円