Sepuluh ribu ditambah sepuluh ribu seharusnya dua puluh ribu, bukan 19999.999999999996. Masalah ini muncul karena float menyimpan angka dalam biner, dan beberapa angka desimal tidak bisa direpresentasikan secara persis. Pada Chapter ini kita menyelesaikannya dengan dua module: Decimal untuk aritmetika uang yang presisi dan Fraction untuk aritmetika pecahan yang eksak.

Masalah 0.1 + 0.2 #

Demo klasik masalah floating-point sudah Anda kenal dari Chapter Tipe Data Numerik:

python
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)

Output yang Dihasilkan:

text
0.30000000000000004
False

Interpreter Python menyimpan 0.1 sebagai pembulatan biner terdekat; hasil penjumlahannya sedikit menyimpang dari 0.3 sehingga perbandingan == gagal. Selisih sepersejuta ini tidak masalah untuk skor game, tetapi fatal untuk total pembayaran.

Decimal untuk Aritmetika Uang #

Class Decimal menyimpan angka desimal secara persis, digit demi digit, dalam basis sepuluh. Kuncinya: selalu buat Decimal dari string, bukan dari float:

python
from decimal import Decimal

harga_satuan = Decimal("15000.00")
jumlah = 3
pajak_rate = Decimal("0.11")

bruto = harga_satuan * jumlah
pajak = (bruto * pajak_rate).quantize(Decimal("0.01"))

print(bruto)
print(pajak)
print(bruto + pajak)

Output yang Dihasilkan:

text
45000.00
4950.00
49950.00

Mari kita bedah kode di atas:

  1. Baris 3 (Decimal("15000.00")): membuat Decimal dari string sehingga nilai 15000.00 tersimpan persis, termasuk dua digit desimalnya. Menulis Decimal(15000.00) justru menanam pembulatan biner dari float ke dalam Decimal.
  2. Baris 7 (bruto = harga_satuan * jumlah): Decimal bisa dikalikan integer biasa; hasilnya tetap menjaga dua digit desimal (45000.00).
  3. Baris 8 (.quantize(Decimal("0.01"))): function quantize() membulatkan hasil ke dua digit desimal — pola standar untuk uang Rupiah. Pembulatannya pun bisa dikontrol, misalnya ROUND_HALF_UP untuk aturan pembulatan komersial.

Mengatur Presisi dengan getcontext #

Module decimal punya pengaturan global bernama context. Presisi bawaannya 28 digit significant, dan kita bisa mengubahnya:

python
from decimal import Decimal, getcontext

print(getcontext().prec)
print(Decimal(1) / Decimal(7))

getcontext().prec = 6
print(Decimal(1) / Decimal(7))

Output yang Dihasilkan:

text
28
0.1428571428571428571428571429
0.142857

Pemanggilan getcontext().prec = 6 memangkas hasil pembagian menjadi 6 digit significant. Perhatikan bahwa prec mengatur jumlah digit significant pada hasil operasi, bukan jumlah digit di belakang koma — untuk memaksa dua desimal tetap gunakan quantize().

Fraction untuk Aritmetika Eksak #

Class Fraction merepresentasikan pecahan sebagai pembilang dan penyebut integer, sehingga 1/3 + 1/6 dihitung persis seperti matematika sekolah:

python
from fractions import Fraction

print(Fraction(1, 3) + Fraction(1, 6))
print(Fraction(1, 3) + Fraction(1, 6) == Fraction(1, 2))
print(Fraction(22, 7))
print(float(Fraction(22, 7)))

Output yang Dihasilkan:

text
1/2
True
22/7
3.142857142857143

Class Fraction menjawab True untuk perbandingan == karena hasilnya memang eksak: 1/2. Function float() tersedia saat kita akhirnya butuh bentuk desimal — dengan sadar menerima pembulatan biner tersebut.

Kapan float Masih Cukup #

Decimal dan Fraction lebih lambat dan lebih verbose dibanding float. Tabel berikut membantu kita memilih:

KebutuhanPilihanAlasan
Uang, faktur, pembayaranDecimalPresisi basis sepuluh, pembulatan terkontrol
Pecahan eksak, matematika rasionalFractionTidak ada pembulatan sama sekali
Pengukuran fisik, statistik, grafikfloatCepat, presisi ganda cukup untuk sensor
Perhitungan ilmiah intensiffloat (atau NumPy)Kinerja jauh lebih tinggi

Aturan praktisnya: jika angka tersebut akan dicetak ke struk pembayaran atau dipakai membandingkan kesamaan eksak, jangan pakai float. Jika angka berasal dari pengukuran yang memang sudah mengandung ketidakpastian, float adalah pilihan yang wajar dan lebih cepat.