Menjumlahkan dua objek uang dengan + dan mengurutkan nomor versi dengan sorted() membuat kode domain terasa alami: total = harga + pajak terbaca lebih bersahabat daripada total = harga.tambah(pajak). Python mengizinkan class buatan kita mengambil alih operator bawaan melalui operator overloading — implementasi special method seperti __add__ dan __lt__ yang diperkenalkan pada Chapter Special Methods dan Data Model. Chapter ini membangun class Money lengkap dengan aritmetika, perbandingan, dan kontrak __eq__-__hash__ yang menyertainya.
Operator Adalah Special Method #
Setiap operator adalah gula sintaksis untuk pemanggilan special method pada tipe operand kiri. Ekspresi a + b dieksekusi sebagai type(a).__add__(a, b); jika method tersebut mengembalikan konstanta NotImplemented, interpreter Python mencoba versi reflected milik operand kanan, type(b).__radd__(b, a). Bila keduanya menolak, interpreter mengangkat TypeError.
Dua aturan praktis mengikuti mekanisme ini:
- Kembalikan
NotImplemented— bukanraise TypeError— untuk operand yang tidak dikenal, agar operand kanan masih diberi kesempatan merespons. - Method overload mengembalikan objek baru; memutasi operand sendiri di dalam
__add__mengejutkan pembaca kode.
Aritmetika pada Class Money #
Berikut class uang dengan penjumlahan, pengurangan, dan perkalian — nilai disimpan sebagai integer rupiah agar bebas dari jebakan presisi float:
class Money:
def __init__(self, jumlah, mata_uang="IDR"):
self.jumlah = jumlah
self.mata_uang = mata_uang
def __repr__(self):
return f"Money({self.jumlah}, {self.mata_uang!r})"
def __add__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.mata_uang != other.mata_uang:
raise ValueError("mata uang berbeda, konversi dulu")
return Money(self.jumlah + other.jumlah, self.mata_uang)
def __sub__(self, other):
if not isinstance(other, Money):
return NotImplemented
if self.mata_uang != other.mata_uang:
raise ValueError("mata uang berbeda, konversi dulu")
return Money(self.jumlah - other.jumlah, self.mata_uang)
def __mul__(self, faktor):
if not isinstance(faktor, int):
return NotImplemented
return Money(self.jumlah * faktor, self.mata_uang)
def __rmul__(self, faktor):
return self * faktor
dompet = Money(150_000)
bonus = Money(50_000)
print(dompet + bonus)
print(dompet - bonus)
print(dompet * 2)
print(3 * dompet)
try:
print(dompet + Money(10, "USD"))
except ValueError as e:
print("ValueError:", e)
try:
print(dompet + 5000)
except TypeError as e:
print("TypeError:", e)Output yang Dihasilkan:
Money(200000, 'IDR')
Money(100000, 'IDR')
Money(300000, 'IDR')
Money(450000, 'IDR')
ValueError: mata uang berbeda, konversi dulu
TypeError: unsupported operand type(s) for +: 'Money' and 'int'Mari kita bedah method demi method:
- Baris 9 (
__add__): mengoperasikandompet + bonus. Operand non-MoneymenghasilkanNotImplemented; operandMoneyberbeda mata uang ditolak denganValueError— penjumlahan uang beda denominasi memang bukan operasi yang sah. - Baris 23 (
__mul__): perkalian hanya diizinkan dengan bilangan bulat —dompet * 2berarti dua kali lipat. Tipe lain mengembalikanNotImplemented. - Baris 28 (
__rmul__): menangani bentuk terbalik3 * dompet. Interpreter memanggilMoney.__rmul__(dompet, 3)karenaint.__mul__(3, dompet)tidak tahu apa-apa tentangMoney. - Baris 46 (
dompet + 5000):__add__mengembalikanNotImplemented,inttidak punya__radd__untukMoney, sehingga interpreter mengangkatTypeErrordengan pesan yang menjelaskan kedua tipe.
Perbandingan dengan total_ordering #
Python menyediakan enam operator perbandingan (==, !=, <, <=, >, >=), tetapi menulis semuanya manual itu repetitif. Decorator total_ordering dari module functools cukup diberi __eq__ dan satu perbandingan urutan — __lt__ atau __le__ — lalu sisanya diturunkan otomatis:
from functools import total_ordering
@total_ordering
class VersiRilis:
def __init__(self, utama, minor, tambal):
self.utama = utama
self.minor = minor
self.tambal = tambal
def __repr__(self):
return f"VersiRilis({self.utama}.{self.minor}.{self.tambal})"
def __eq__(self, other):
if not isinstance(other, VersiRilis):
return NotImplemented
return (self.utama, self.minor, self.tambal) == (other.utama, other.minor, other.tambal)
def __lt__(self, other):
if not isinstance(other, VersiRilis):
return NotImplemented
return (self.utama, self.minor, self.tambal) < (other.utama, other.minor, other.tambal)
lama = VersiRilis(3, 11, 4)
baru = VersiRilis(3, 12, 0)
print(lama < baru)
print(lama <= baru)
print(baru > lama)
print(lama >= baru)
print(lama == VersiRilis(3, 11, 4))
print(sorted([VersiRilis(3, 12, 0), VersiRilis(3, 11, 9), VersiRilis(4, 0, 0)]))Output yang Dihasilkan:
True
True
True
False
True
[VersiRilis(3.11.9), VersiRilis(3.12.0), VersiRilis(4.0.0)]Trik intinya ada di Baris 17 dan 22: perbandingan tuple (utama, minor, tambal) memakai perbandingan leksikografis bawaan Python — bandingkan utama dulu, jika sama lanjut minor, lalu tambal — sehingga __lt__ dan __eq__ masing-masing cukup satu baris. Decorator @total_ordering mengisi __le__, __gt__, dan __ge__ dari keduanya, dan sorted() pada baris terakhir memanfaatkan seluruh rangkaiannya.
Kontrak eq dan hash #
Aturan penting yang sering dilupakan: mendefinisikan __eq__ pada class membuat instance-nya kehilangan perilaku hash bawaan. Interpreter Python menganggap objek yang bisa dibandingkan isinya tidak lagi cocok di-hash secara default, karena hash dictionary dan set menuntut janji berikut:
Jika
a == bbernilaiTrue, makahash(a)harus sama denganhash(b).
Mari kita lihat konsekuensinya:
class SimbolTanpaHash:
def __init__(self, kode):
self.kode = kode
def __eq__(self, other):
return isinstance(other, SimbolTanpaHash) and self.kode == other.kode
a = SimbolTanpaHash("GOTO")
b = SimbolTanpaHash("GOTO")
try:
{a, b}
except TypeError as e:
print("TypeError:", e)
class Simbol:
def __init__(self, kode):
self.kode = kode
def __eq__(self, other):
return isinstance(other, Simbol) and self.kode == other.kode
def __hash__(self):
return hash(self.kode)
x = Simbol("GOTO")
y = Simbol("GOTO")
print(len({x, y})) # __eq__ sama -> dihitung satu
print(x in {y}) # __hash__ sama -> ditemukanOutput yang Dihasilkan:
TypeError: unhashable type: 'SimbolTanpaHash'
1
TrueAlurnya sebagai berikut:
- Baris 13 (
{a, b}): memasukkanSimbolTanpaHashke set memicuTypeError: unhashable type— definisi__eq__menonaktifkan__hash__bawaan. - Baris 25 (
def __hash__): classSimbolmemenuhi kontrak dengan meng-hash field yang sama dengan yang dibandingkan__eq__— yaitukode. Dua simbol"GOTO"kini punya hash yang identik. - Baris 32 (
len({x, y})): set menggunakan hash untuk menempatkanxdany, lalu__eq__untuk memastikan keduanya setara — hasilnya satu anggota. - Baris 33 (
x in {y}): pencarian berjalan cepat berkat hash yang cocok;Truetercetak.
Kapan Tidak Perlu Overload #
Operator yang baik membuat kode lebih terbaca; operator yang buruk menyembunyikan kejutan. Beberapa batasan yang layak dipegang:
- Operator harus terasa alami:
+untuk penjumlahan/penggabungan,==untuk kesetaraan makna. Bila__add__di classPenggunamenggabungkan dua akun, pembaca kode akan menebak salah. - Jangan menyerahkan operator ke domain yang berbeda:
list + listmenggabungkan,int + intmenjumlahkan — dua makna yang sudah membeku sebagai konvensi. ClassTagihanyang+-nya mengurangkan adalah bencana keterbacaan. - Method bernama biasa lebih jelas untuk operasi niche:
inv.laku()lebih ekspresif daripada~invatauinv << 3. - Pertahankan sifat yang diharapkan:
__eq__yang mengembalikan non-bool, atau__add__yang memutasi operand kiri, melanggar ekspektasi setiap pembaca Python.