Setiap instance class Python biasa membawa sebuah dictionary __dict__ — itulah sebabnya kita bisa menambah atribut baru kapan saja, seperti yang dibahas pada Chapter Instance Attributes. Fleksibilitas tersebut dibayar dengan memori: dictionary harus siap menampung jumlah atribut yang tidak terbatas. Untuk aplikasi yang membuat ratusan ribu objek kecil — titik peta, node graf, baris data — biaya tersebut menjadi nyata. Deklarasi __slots__ menukar fleksibilitas itu dengan ruang: atribut instance dialokasikan tetap, dan __dict__ hilang sama sekali.
Menutup dict dengan slots #
__slots__ dideklarasikan sebagai atribut class berisi tuple nama atribut yang diizinkan:
class TitikTanpaSlots:
def __init__(self, x, y):
self.x = x
self.y = y
class Titik:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
bebas = TitikTanpaSlots(1, 2)
bebas.z = 3
print(bebas.__dict__)
ketat = Titik(1, 2)
print(ketat.x, ketat.y)
print(hasattr(ketat, "__dict__"))
try:
ketat.z = 3
except AttributeError as e:
print("AttributeError:", e)Output yang Dihasilkan:
{'x': 1, 'y': 2, 'z': 3}
1 2
False
AttributeError: 'Titik' object has no attribute 'z'Mari kita telaah perilaku kedua class tersebut:
- Baris 16 (
bebas.z = 3): class tanpa__slots__menerima atribut baru dengan senang hati —__dict__instance tumbuh menampungz. - Baris 21 (
print(hasattr(ketat, "__dict__"))): instance classTitiktidak lagi memiliki__dict__; slotxdanytersimpan langsung pada memori instance, danhasattrmelaporkanFalse. - Baris 24 (
ketat.z = 3): penambahan atribut di luar deklarasi ditolak keras denganAttributeError. Kesalahan pengetikan nama atribut yang biasanya senyap —titik.xx = 5— kini menjadi galat yang langsung terlihat.
Batasan ini juga menyentuh fitur lain: tanpa __dict__, atribut tidak bisa ditambahkan secara dinamis, dan fungsi weakref.ref() gagal dibuat kecuali string "__weakref__" ikut dideklarasikan di dalam __slots__.
Penghematan Memori Nyata #
Berapa besar manfaatnya? Mari kita ukur langsung dengan sys.getsizeof:
import sys
class TitikDict:
def __init__(self, x, y):
self.x = x
self.y = y
class TitikSlots:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
titik_dict = TitikDict(1, 2)
titik_slots = TitikSlots(1, 2)
ukuran_dict = sys.getsizeof(titik_dict) + sys.getsizeof(titik_dict.__dict__)
ukuran_slots = sys.getsizeof(titik_slots)
print("Instance + __dict__ :", ukuran_dict, "byte")
print("Instance __slots__ :", ukuran_slots, "byte")
print("Hemat :", round((1 - ukuran_slots / ukuran_dict) * 100), "%")
jumlah = 100_000
daftar_dict = [TitikDict(i, i) for i in range(jumlah)]
daftar_slots = [TitikSlots(i, i) for i in range(jumlah)]
total_dict = sum(sys.getsizeof(p) + sys.getsizeof(p.__dict__) for p in daftar_dict)
total_slots = sum(sys.getsizeof(p) for p in daftar_slots)
print(f"Total {jumlah:,} instance : {total_dict / 1_000_000:.1f} MB vs {total_slots / 1_000_000:.1f} MB")Output yang Dihasilkan:
Instance + __dict__ : 344 byte
Instance __slots__ : 48 byte
Hemat : 86 %
Total 100,000 instance : 13.6 MB vs 4.8 MBSebuah instance dengan dua atribut plus __dict__-nya menghabiskan 344 byte, sedangkan instance ber-__slots__ hanya 48 byte — penghematan 86 persen. Pada 100.000 instance, selisihnya mencapai 8.8 MB untuk class yang sama sederhananya.
Jebakan Inheritance #
__slots__ hanya efektif bila seluruh rantai inheritance bebas dari __dict__. Satu class induk yang lupa mendeklarasikan __slots__ cukup untuk membuka kembali keran atribut dinamis:
class Hewan:
# TIDAK mendefinisikan __slots__: instance tetap punya __dict__
def __init__(self, nama):
self.nama = nama
class Kucing(Hewan):
__slots__ = ("warna",)
kucing = Kucing("Oyen")
kucing.warna = "oranye"
kucing.umur = 3 # masih bisa! __dict__ diwarisi dari Hewan
print(kucing.__dict__)
class HewanKetat:
__slots__ = ("nama",)
def __init__(self, nama):
self.nama = nama
class KucingKetat(HewanKetat):
__slots__ = ("warna",)
ketat = KucingKetat("Oyen")
ketat.nama = "Oyen"
ketat.warna = "oranye"
try:
ketat.umur = 3
except AttributeError as e:
print("AttributeError:", e)Output yang Dihasilkan:
{'nama': 'Oyen', 'umur': 3}
AttributeError: 'KucingKetat' object has no attribute 'umur'Class Kucing mendeklarasikan __slots__ = ("warna",), tetapi induk Hewan tidak — instance Kucing mewarisi __dict__ dari Hewan, sehingga kucing.umur = 3 tetap lolos. Baru ketika HewanKetat juga mendeklarasikan __slots__, rantai menjadi rapat dan penolakan AttributeError bekerja.
Batasan kedua menyangkut multiple inheritance: dua class yang sama-sama memakai __slots__ dengan layout berbeda tidak bisa digabungkan.
class A:
__slots__ = ("a",)
class B:
__slots__ = ("b",)
# dua parent sama-sama memakai __slots__ dengan layout berbeda
try:
class C(A, B):
pass
except TypeError as e:
print("TypeError:", e)Output yang Dihasilkan:
TypeError: multiple bases have instance lay-out conflictInterpreter menolak class C karena satu instance tidak bisa menyimpan dua layout slot yang berbeda sekaligus — tema konflik layout yang dibahas pada Chapter Multiple Inheritance.
Kapan slots Layak Dipakai #
__slots__ adalah optimasi yang mempertukarkan fleksibilitas dengan efisiensi, sehingga keputusannya mudah dipetakan:
| Situasi | Keputusan |
|---|---|
| Aplikasi membuat puluhan ribu instance atau lebih dari class kecil | __slots__ layak dipertimbangkan |
| Instance berjumlah ratusan atau atributnya banyak dan sering berubah | class biasa lebih sederhana |
Objek perlu __dict__ untuk introspeksi dinamis atau monkey patching | hindari __slots__ |
| Perlu weak reference ke objek | tambahkan "__weakref__" ke dalam __slots__ |
Mulailah tanpa __slots__, ukur dengan pengukuran memori seperti contoh di atas, lalu tambahkan deklarasi hanya jika angkanya memang bermasalah. Mengorbankan kemampuan menambah atribut untuk penghematan yang belum terbukti adalah optimasi prematur.