Akses atribut pada Python dapat disela melalui protokol yang terdiri atas __getattr__, __setattr__, dan __getattribute__, sedangkan __slots__ mengganti penyimpanan atribut yang longgar dengan daftar nama yang ditetapkan sejak definisi kelas. Dua mekanisme ini bekerja pada lapisan yang sama, yaitu lapisan penentuan bagaimana obj.atribut dibaca dan ditulis, tetapi tujuannya berbeda, yaitu yang pertama menambah perilaku, sedangkan yang kedua memangkas kebutuhan memori. Topik ini membahas keduanya beserta jebakan rekursi dan pewarisannya.
getattr untuk akses yang gagal#
Metode __getattr__ dipanggil hanya ketika pencarian atribut biasa gagal, yaitu setelah __dict__ instans dan seluruh kelasnya diperiksa dan nama itu tidak ditemukan. Metode ini dengan demikian cocok sebagai jaring pengaman, misalnya mendelegasikan akses atribut ke dictionary internal dan menerjemahkan kegagalan menjadi pesan yang jelas.
class Delegasi:
def __init__(self):
self.data = {"nama": "Citra", "umur": 24}
def __getattr__(self, nama):
try:
return self.data[nama]
except KeyError:
raise AttributeError(f"tidak ada atribut {nama}")
d = Delegasi()
print(d.nama)
print(d.umur)
try:
print(d.alamat)
except AttributeError as e:
print(type(e).__name__, str(e))Citra
24
AttributeError tidak ada atribut alamatPembacaan d.nama dan d.umur selesai di __dict__ karena keduanya diisi oleh __init__, sehingga __getattr__ tidak pernah terlibat. Nama alamat tidak ada di mana pun, sehingga barulah __getattr__ dipanggil dan mencari di self.data. Ketika kunci itu juga tidak ada, metode menimbulkan AttributeError dengan pesan miliknya sendiri, yang lebih informatif daripada pesan bawaan untuk kasus delegasi semacam ini.
setattr dan jebakan rekursi#
Metode __setattr__ dipanggil untuk setiap penugasan atribut, termasuk penugasan yang terjadi di dalam __init__. Karena itu, implementasi khususnya wajib menugaskan melalui object.__setattr__ atau super().__setattr__, dan bukan melalui self.nama = nilai, sebab bentuk terakhir memanggil __setattr__ lagi dan berputar tanpa akhir.
class SuhuTerjaga:
def __setattr__(self, nama, nilai):
if nama == "celsius" and not isinstance(nilai, (int, float)):
raise TypeError("celsius harus angka")
object.__setattr__(self, nama, nilai)
s = SuhuTerjaga()
s.celsius = 30
print(s.celsius)
try:
s.celsius = "panas"
except TypeError as e:
print(type(e).__name__, str(e))30
TypeError celsius harus angkaPenugasan yang sah berjalan melalui object.__setattr__, sehingga atribut tersimpan normal dan tetap dapat dibaca. Penugasan dengan string ditolak sebelum sampai ke penyimpanan, dan pesan errornya menyebut aturan yang dilanggar. Bandingkan dengan versi yang tidak melakukannya.
class Rekursif:
def __setattr__(self, nama, nilai):
self.nilai = nilai
r = Rekursif()
try:
r.nilai = 1
except RecursionError:
print("RecursionError")RecursionErrorBaris self.nilai = nilai di dalam __setattr__ memanggil __setattr__ lagi dengan nama yang sama, dan setiap panggilan menumpuk bingkai baru sampai batas rekursi tercapai. Pemanggilan object.__setattr__(self, nama, nilai) memutus lingkaran itu karena penugasannya tidak lagi melalui protokol instans.
slots dan penghematan memori#
Secara default, setiap instans membawa __dict__ untuk menampung atribut yang boleh bertambah kapan saja. Deklarasi __slots__ berupa tuple nama mengganti dictionary itu dengan deskriptor tetap untuk setiap nama. Instans tidak lagi membawa __dict__, sehingga memori yang dipakai menyusut secara berarti untuk kelas yang diinstansiasi dalam jumlah besar. Dokumentasi resmi merangkum kegunaannya sebagai penghematan ruang pada objek kecil yang banyak, dengan konsekuensi atribut baru tidak dapat ditambahkan.
class TitikBebas:
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
a = TitikBebas(1, 2)
b = TitikSlots(1, 2)
print(hasattr(a, "__dict__"), hasattr(b, "__dict__"))
print(TitikSlots.__slots__)
b.x = 5
print(b.x, b.y)
try:
b.z = 9
except AttributeError as e:
print(type(e).__name__, str(e))True False
('x', 'y')
5 2
AttributeError 'TitikSlots' object has no attribute 'z'Instans a masih membawa __dict__, sedangkan b tidak, dan itulah sumber penghematannya. Atribut yang terdaftar pada slots tetap dapat dibaca dan ditulis seperti biasa, sebagaimana b.x = 5, tetapi nama z di luar daftar ditolak dengan AttributeError. Batasan ini sengaja dipilih oleh penulis kelas untuk mencegah penambahan atribut sembarang, dan jumlah memorinya tidak dibandingkan pada contoh karena angka pastinya berbeda antar mesin dan versi Python.
Konsekuensi praktisnya muncul pada alat yang membaca __dict__ secara langsung. Fungsi vars() menolak instans tanpa dictionary, sehingga serialisasi lewat vars(obj) perlu diganti dengan perakitan manual dari daftar slots.
class TitikSlots:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
b = TitikSlots(1, 2)
try:
vars(b)
except TypeError as e:
print(type(e).__name__, str(e))
print({nama: getattr(b, nama) for nama in TitikSlots.__slots__})TypeError vars() argument must have __dict__ attribute
{'x': 1, 'y': 2}Keluaran pertama menegaskan penolakan vars(), sedangkan baris kedua memperlihatkan penggantinya, yaitu dictionary yang dirakit dari nama-nama slots dengan getattr(). Serialisasi tetap mungkin dengan pola ini, tetapi daftar atributnya kini eksplisit dan mengikuti deklarasi kelas.
Kesalahan umum#
Kesalahan pertama adalah menaruh atribut kelas dengan nama yang sama dengan salah satu slots. Python menolak definisi kelasnya langsung, karena dua mekanisme penyimpanan berbeda tidak boleh memakai satu nama yang sama.
try:
class Salah:
__slots__ = ("x",)
x = 10
except ValueError as e:
print(type(e).__name__, str(e))ValueError 'x' in __slots__ conflicts with class variablePerbaikannya adalah memilih salah satu, yaitu membuang atribut kelas itu atau mengeluarkan namanya dari __slots__. Bila nilai yang sama perlu dimiliki semua instans, nilai tersebut dapat ditampung sebagai atribut kelas pada nama lain yang tidak terdaftar di slots.
Kesalahan kedua terjadi saat mewarisi. Subkelas yang tidak mendeklarasikan __slots__ otomatis memperoleh __dict__ kembali, sehingga seluruh penghematan dari kelas induknya hilang tanpa peringatan apa pun.
class Induk:
__slots__ = ("a",)
class Anak(Induk):
pass
c = Anak()
c.a = 1
c.b = 2
print(hasattr(c, "__dict__"))TrueInstans c menerima atribut b yang tidak dikenal induknya karena __dict__-nya kembali hadir. Perbaikannya adalah mendeklarasikan __slots__ = () pada subkelas yang tidak menambah atribut, sehingga rantai kelas tetap tanpa dictionary. Aturan yang sama berlaku pada setiap tingkat, karena satu kelas tanpa slots di sepanjang rantai pewarisan sudah cukup untuk membatalkan penghematannya.
Ringkasan#
__getattr__dipanggil hanya setelah pencarian normal gagal, sehingga tepat untuk delegasi dan pesan error kustom, sedangkan__getattribute__menyela setiap akses.__setattr__menyela setiap penugasan atribut dan wajib menugaskan melaluiobject.__setattr__untuk menghindariRecursionError.__slots__mengganti__dict__instans dengan daftar nama tetap, sehingga memori per instans menyusut dan atribut di luar daftar ditolak.- Pewarisan menuntut setiap kelas dalam rantai mendeklarasikan
__slots__, karena satu subkelas tanpa slots mengembalikan__dict__dan membatalkan penghematannya.
Referensi#
- Customizing attribute access — Python Software Foundation — Kontrak getattr, setattr, dan getattribute pada referensi bahasa.
- slots — Python Software Foundation — Dokumentasi slots beserta interaksinya dengan dict.
- Notes on using slots — Python Software Foundation — Daftar catatan resmi tentang keterbatasan slots, termasuk pewarisannya.