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.

__slots__ dideklarasikan sebagai atribut class berisi tuple nama atribut yang diizinkan:

python
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:

text
{'x': 1, 'y': 2, 'z': 3}
1 2
False
AttributeError: 'Titik' object has no attribute 'z'

Mari kita telaah perilaku kedua class tersebut:

  1. Baris 16 (bebas.z = 3): class tanpa __slots__ menerima atribut baru dengan senang hati — __dict__ instance tumbuh menampung z.
  2. Baris 21 (print(hasattr(ketat, "__dict__"))): instance class Titik tidak lagi memiliki __dict__; slot x dan y tersimpan langsung pada memori instance, dan hasattr melaporkan False.
  3. Baris 24 (ketat.z = 3): penambahan atribut di luar deklarasi ditolak keras dengan AttributeError. 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:

python
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:

text
Instance + __dict__ : 344 byte
Instance __slots__  : 48 byte
Hemat               : 86 %
Total 100,000 instance : 13.6 MB vs 4.8 MB

Sebuah 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:

python
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:

text
{'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.

python
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:

text
TypeError: multiple bases have instance lay-out conflict

Interpreter 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:

SituasiKeputusan
Aplikasi membuat puluhan ribu instance atau lebih dari class kecil__slots__ layak dipertimbangkan
Instance berjumlah ratusan atau atributnya banyak dan sering berubahclass biasa lebih sederhana
Objek perlu __dict__ untuk introspeksi dinamis atau monkey patchinghindari __slots__
Perlu weak reference ke objektambahkan "__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.