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:
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)Output yang Dihasilkan:
0.30000000000000004
FalseInterpreter 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:
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:
45000.00
4950.00
49950.00Mari kita bedah kode di atas:
- Baris 3 (
Decimal("15000.00")): membuatDecimaldari string sehingga nilai15000.00tersimpan persis, termasuk dua digit desimalnya. MenulisDecimal(15000.00)justru menanam pembulatan biner dari float ke dalamDecimal. - Baris 7 (
bruto = harga_satuan * jumlah):Decimalbisa dikalikan integer biasa; hasilnya tetap menjaga dua digit desimal (45000.00). - Baris 8 (
.quantize(Decimal("0.01"))): functionquantize()membulatkan hasil ke dua digit desimal — pola standar untuk uang Rupiah. Pembulatannya pun bisa dikontrol, misalnyaROUND_HALF_UPuntuk aturan pembulatan komersial.
Mengatur Presisi dengan getcontext #
Module decimal punya pengaturan global bernama context. Presisi bawaannya 28 digit significant, dan kita bisa mengubahnya:
from decimal import Decimal, getcontext
print(getcontext().prec)
print(Decimal(1) / Decimal(7))
getcontext().prec = 6
print(Decimal(1) / Decimal(7))Output yang Dihasilkan:
28
0.1428571428571428571428571429
0.142857Pemanggilan 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:
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:
1/2
True
22/7
3.142857142857143Class 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:
| Kebutuhan | Pilihan | Alasan |
|---|---|---|
| Uang, faktur, pembayaran | Decimal | Presisi basis sepuluh, pembulatan terkontrol |
| Pecahan eksak, matematika rasional | Fraction | Tidak ada pembulatan sama sekali |
| Pengukuran fisik, statistik, grafik | float | Cepat, presisi ganda cukup untuk sensor |
| Perhitungan ilmiah intensif | float (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.