Mengapa len(daftar) bekerja pada list, dan for bisa mengiterasi hampir semua objek? Jawabannya ada pada data model Python: setiap sintaks bahasa — pemanggilan, len(), in, for, print() — pada akhirnya diterjemahkan menjadi pemanggilan method tertentu pada objek. Method-method tersebut bernama special method atau dunder method (dari double underscore), dan dengan mengimplementasinya, class buatan kita ikut terhubung ke sintaks bawaan bahasa. Chapter ini memetakan mekanisme tersebut dan membangun class Playlist yang terasa seperti struktur data bawaan Python.

Data Model dan Lookup di Type #

Saat kita menulis len(daftar), interpreter Python tidak mencari atribut __len__ pada objek secara bebas — interpreter memanggil type(daftar).__len__(daftar). Pencarian dunder selalu terjadi pada tipe, bukan pada dictionary instance:

python
>>> daftar = [10, 20, 30]
>>> len(daftar)
3
>>> type(daftar).__len__(daftar)
3
>>> daftar.__len__()
3

Tiga ekspresi tersebut menghasilkan nilai yang sama; len() di baris pertama adalah gula sintaksis untuk pemanggilan di baris kedua. Konsekuensi dari pencarian lewat tipe ini: dunder yang disimpan sebagai atribut instance diabaikan sepenuhnya.

python
class Lagu:
    def __init__(self, judul):
        self.judul = judul


lagu = Lagu("Rehat")
lagu.__len__ = lambda: 99  # tersimpan di __dict__ instance

print(lagu.__len__())  # pemanggilan eksplisit: bekerja

try:
    print(len(lagu))  # len() hanya melihat type(lagu), bukan instance
except TypeError as e:
    print("TypeError:", e)

Output yang Dihasilkan:

text
99
TypeError: object of type 'Lagu' has no len()

Pemanggilan eksplisit lagu.__len__() mengembalikan 99 karena ekspresi tersebut membaca atribut biasa dari __dict__ instance. Namun len(lagu) menolaknya dengan TypeError — fungsi len() hanya berkonsultasi ke type(lagu), dan class Lagu tidak mendefinisikan __len__. Aturan yang sama menjelaskan mengapa objek hasil dari Chapter Memahami Object dan Reference berperilaku konsisten: protokol bahasa melekat pada tipe.

Class Playlist dengan Special Methods #

Mari kita bangun class yang memanfaatkan data model secara penuh — sebuah Playlist yang bisa diukur, diindeks, diiterasi, dan dites keanggotaannya:

python
class Playlist:
    def __init__(self, nama, lagu=None):
        self.nama = nama
        self.lagu = list(lagu) if lagu else []

    def __repr__(self):
        return f"Playlist({self.nama!r}, {len(self.lagu)} lagu)"

    def __len__(self):
        return len(self.lagu)

    def __getitem__(self, indeks):
        return self.lagu[indeks]

    def __contains__(self, judul):
        return any(judul == item for item in self.lagu)

    def __iter__(self):
        return iter(self.lagu)


favorit = Playlist("Santai Senja", ["Rehat", "Hujan Kemarin", "Senja Di Ambon"])

print(favorit)
print(len(favorit))
print(favorit[0])
print(favorit[-1])
print("Hujan Kemarin" in favorit)
print("Melompat" in favorit)

for judul in favorit:
    print("-", judul)

if favorit:
    print("playlist tidak kosong")

Output yang Dihasilkan:

text
Playlist('Santai Senja', 3 lagu)
3
Rehat
Senja Di Ambon
True
False
- Rehat
- Hujan Kemarin
- Senja Di Ambon
playlist tidak kosong

Setiap baris pada bagian bawah memicu satu dunder — inilah pemetaannya:

  1. Baris 24 (print(favorit)): print() memanggil __str__; karena tidak didefinisikan, interpreter jatuh kembali ke __repr__ dan mencetak Playlist('Santai Senja', 3 lagu).
  2. Baris 25 (len(favorit)): diterjemahkan menjadi Playlist.__len__(favorit) yang mengembalikan jumlah lagu.
  3. Baris 26 (favorit[0]): operator subscript [] memanggil __getitem__; karena method tersebut mendelegasikan ke list internal, indeks negatif seperti favorit[-1] pada baris berikutnya pun bekerja.
  4. Baris 28-29 (in favorit): operator in memanggil __contains__ — satu-satunya lagu yang tidak ada, "Melompat", dilaporkan False.
  5. Baris 31 (for judul in favorit): loop for memanggil __iter__ untuk mendapatkan iterator.
  6. Baris 34 (if favorit:): pengujian truthiness memanggil __bool__; karena tidak ada, interpreter jatuh kembali ke __len__ — panjang 3 berarti True.

Class Playlist kini terasa seperti struktur data asli Python: ia bisa masuk ke len(), in, for, dan bahkan sorted().

str vs repr #

Kedua dunder ini sama-sama menghasilkan teks, tetapi untuk dua tujuan berbeda. __repr__ menyasar pembangun — teks yang tepat untuk debugging, idealnya berupa ekspresi yang bisa direkonstruksi. __str__ menyasar pembaca — teks yang ramah untuk manusia:

python
class Buku:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def __str__(self):
        return f"{self.judul} oleh {self.penulis}"

    def __repr__(self):
        return f"Buku({self.judul!r}, {self.penulis!r})"


buku = Buku("Laskar Pelangi", "Andrea Hirata")

print(buku)         # print() memakai __str__
print(repr(buku))   # repr() memakai __repr__
print([buku])       # container memakai __repr__ elemennya
print(f"{buku!r}")  # format spec !r memaksa repr()

Output yang Dihasilkan:

text
Laskar Pelangi oleh Andrea Hirata
Buku('Laskar Pelangi', 'Andrea Hirata')
[Buku('Laskar Pelangi', 'Andrea Hirata')]
Buku('Laskar Pelangi', 'Andrea Hirata')

Perhatikan dua baris terakhir: elemen di dalam container selalu dirender dengan __repr__, dan conversion spec !r pada f-string memaksa representasi penuh. Jika hanya ada satu yang sempat ditulis, pilih __repr__ — ia juga menjadi fallback untuk __str__ yang hilang.

Objek yang Bisa Dipanggil dan Tabel Dunder #

Dunder __call__ membuat objek bisa dipanggil layaknya function — pola untuk objek berstatus yang menyimpan konteks antar pemanggilan:

python
class Pemutar:
    """Contoh __call__: objek yang dipanggil layaknya function."""

    def __init__(self, lagu):
        self.lagu = list(lagu)
        self.posisi = 0

    def __call__(self):
        lagu = self.lagu[self.posisi % len(self.lagu)]
        self.posisi += 1
        return lagu


putar = Pemutar(["Rehat", "Hujan Kemarin"])

print(putar())
print(putar())
print(putar())

Output yang Dihasilkan:

text
Rehat
Hujan Kemarin
Rehat

Ekspresi putar() memanggil Pemutar.__call__(putar); objek putar menyimpan posisi lagu di antara pemanggilan sehingga urutan berputar kembali ke awal. Berikut dunder yang paling sering dijumpai:

Special methodDipicu olehPeran
__init__Produk(...)inisialisasi objek baru
__repr__repr(obj), container, REPLrepresentasi untuk debugging
__str__print(obj), f-stringrepresentasi untuk manusia
__len__len(obj)jumlah elemen
__bool__if obj:konversi truthy/falsy
__getitem__obj[k]pembacaan indeks/kunci
__setitem__obj[k] = vpenulisan indeks/kunci
__iter__for x in objprotokol iterasi
__contains__x in objtes keanggotaan
__call__obj()objek sebagai function
__eq__, __lt__==, <perbandingan
__hash__hash(obj)kunci dict/set

Dunder untuk operator aritmetika dan perbandingan mendapat pembahasan khusus pada Chapter Operator Overloading.