Atribut adalah nilai yang menempel pada objek, dan Python membedakan atribut instans yang dimiliki satu objek dengan atribut kelas yang dibagi seluruh instans kelasnya. Perbedaan keduanya menentukan ke mana perubahan state atau keadaan objek mengalir, sehingga pemilihan yang keliru mudah menimbulkan bug yang sulit dilacak. Topik ini membahas cara kerja masing-masing, urutan pencarian atribut saat dibaca, serta jebakan state bersama beserta perbaikannya.
Atribut instans: milik satu objek#
Atribut instans didefinisikan dengan menugaskan nilai melalui self, umumnya di dalam __init__. Atribut semacam ini tersimpan pada objeknya, sehingga dua instans dapat memegang nilai yang berbeda tanpa saling mengganggu. Perubahan pada atribut milik satu instans tidak pernah terlihat oleh instans lainnya.
class SensorSuhu:
def __init__(self, lokasi, suhu):
self.lokasi = lokasi
self.suhu = suhu
s1 = SensorSuhu("gudang", 27.5)
s2 = SensorSuhu("laboratorium", 21.0)
print(s1.lokasi, s1.suhu)
print(s2.lokasi, s2.suhu)
s1.suhu = 30.0
print(s1.suhu, s2.suhu)gudang 27.5
laboratorium 21.0
30.0 21.0Penugasan s1.suhu = 30.0 hanya mengubah atribut pada s1, sedangkan s2 tetap membaca 21.0. Atribut instans bekerja seperti kotak penyimpanan pribadi setiap objek, sehingga data yang sifatnya per objek, seperti lokasi dan pembacaan suhu, memang sebaiknya diletakkan di sini. Pencarian s1.suhu juga menemukan atribut pada objeknya terlebih dahulu, sebelum Python melangkah ke tempat lain.
Letak fisik atribut instans dapat diperiksa melalui kamus __dict__ milik objek, yang memuat nama dan nilainya. Atribut baru juga dapat ditambahkan setelah instans dibuat, misalnya s1.catatan = "kalibrasi ulang", dan objeknya langsung menampungnya. Meskipun sah, kebiasaan ini menyulitkan penelusuran bentuk objek, sehingga seluruh atribut yang dijanjikan sebaiknya ditugaskan di dalam __init__.
Atribut kelas: dibagi seluruh instans#
Atribut kelas ditugaskan langsung di badan kelas, sejajar dengan definisi metodenya. Nilainya satu untuk seluruh kelas, sehingga pembacaan melalui kelas maupun melalui instans menunjuk nilai yang sama. Atribut semacam ini cocok untuk konstanta yang dipakai bersama atau data yang dilacak pada tingkat kelas.
class SensorSuhu:
jumlah_sensor = 0 # atribut kelas, dibagi seluruh instans
def __init__(self, lokasi):
self.lokasi = lokasi # atribut instans, milik satu objek
SensorSuhu.jumlah_sensor += 1
s1 = SensorSuhu("gudang")
s2 = SensorSuhu("laboratorium")
print(s1.lokasi, s2.lokasi)
print(SensorSuhu.jumlah_sensor)gudang laboratorium
2Atribut jumlah_sensor berada di badan kelas dan dinaikkan melalui nama kelasnya, yaitu SensorSuhu.jumlah_sensor += 1. Setiap pemanggilan __init__ menambah hitungan yang sama, sehingga nilai akhirnya adalah 2 setelah dua sensor dibuat. Pembacaan melalui instans juga sah, misalnya s1.jumlah_sensor menghasilkan nilai yang sama selama instans itu tidak menutupinya dengan atribut instans. Apabila penugasan dilakukan melalui nama kelas, perubahan terlihat oleh seluruh instans karena hanya ada satu salinan nilai tersebut.
Urutan pencarian atribut#
Pembacaan atribut mengikuti urutan tetap. Python mencari atribut pada instans terlebih dahulu, lalu pada kelasnya, kemudian pada kelas-kelas induknya, dan terakhir menimbulkan error AttributeError bila tidak ditemukan. Penugasan melalui instans tidak mengikuti urutan itu, karena penugasan selalu membuat atau mengubah atribut pada instans, tanpa menyentuh atribut kelas yang mungkin terlihat sebelumnya. Perbedaan membaca dan menulis ini menimbulkan perilaku yang disebut shadowing, yaitu atribut instans menutupi atribut kelas untuk instans yang bersangkutan. Apabila akses bersyarat dibutuhkan, fungsi getattr(objek, "nama", default) mengembalikan nilai default tanpa menimbulkan error, sedangkan hasattr() menguji keberadaan atributnya.
class Konfigurasi:
mode = "normal"
c = Konfigurasi()
d = Konfigurasi()
print(c.mode, d.mode)
c.mode = "hemat" # menugaskan atribut instans, bukan mengubah kelas
print(c.mode, d.mode)
print(Konfigurasi.mode)normal normal
hemat normal
normalSebelum penugasan, c.mode dan d.mode sama-sama jatuh ke atribut kelas dan menghasilkan normal. Penugasan c.mode = "hemat" membuat atribut instans baru pada c, sehingga c.mode membaca hemat sementara d.mode tetap membaca atribut kelas. Nilai Konfigurasi.mode juga tetap normal, yang membuktikan kelasnya tidak pernah berubah. Apabila semua instans memang perlu berubah, penugasan harus dilakukan melalui nama kelas.
Jebakan state bersama#
Atribut kelas menjadi berbahaya apabila nilainya berupa objek yang dapat diubah, seperti list atau dictionary. Mutasi objek semacam itu dilakukan pada satu salinan yang dibagi semua instans, sehingga perubahan lewat satu instans terlihat oleh instans lainnya tanpa tampak di kode pemanggilnya.
Berkas berikut membandingkan dua versi kelas arsip. Kelas pertama menyimpan catatan pada atribut kelas, sedangkan kelas kedua membuat list-nya sendiri di dalam __init__.
class ArsipRusak:
catatan = [] # atribut kelas berupa list, dibagi semua instans
def tambah(self, teks):
self.catatan.append(teks)
class ArsipBaik:
def __init__(self):
self.catatan = [] # atribut instans dibuat per objek
def tambah(self, teks):
self.catatan.append(teks)
rusak_a = ArsipRusak()
rusak_b = ArsipRusak()
rusak_a.tambah("nota pembelian")
print("rusak:", rusak_b.catatan)
baik_a = ArsipBaik()
baik_b = ArsipBaik()
baik_a.tambah("nota pembelian")
print("baik:", baik_b.catatan)rusak: ['nota pembelian']
baik: []Simpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.
# menjalankan program perbandingan arsip
python3 arsip-catatan.pyrusak: ['nota pembelian']
baik: []Pada ArsipRusak, metode tambah() tidak pernah menugaskan self.catatan, melainkan memanggil append pada list milik kelas. Karena salinannya hanya satu, rusak_b ikut menampung isi yang ditambahkan rusak_a. Pada ArsipBaik, setiap instans memegang list sendiri, sehingga baik_b tetap kosong. Pola ArsipRusak jarang disengaja dan hampir selalu berupa bug, sedangkan pola ArsipBaik adalah bentuk yang benar untuk data per objek.
Kesalahan umum#
Kesalahan pertama adalah menaikkan atribut kelas melalui self, yang diam-diam membuat atribut instans baru alih-alih mengubah nilai kelas.
class Penghitung:
nilai = 0
def naik(self):
self.nilai += 1
p = Penghitung()
p.naik()
p.naik()
q = Penghitung()
print(p.nilai, q.nilai, Penghitung.nilai)2 0 0Pernyataan self.nilai += 1 dibaca Python sebagai self.nilai = self.nilai + 1, yaitu membaca atribut kelas lalu menugaskan hasilnya pada instans. Akibatnya p memegang nilai sendiri berupa 2, sedangkan q dan kelasnya tetap 0. Perbaikannya adalah menulis Penghitung.nilai += 1, sehingga penugasan benar-benar menyentuh atribut kelas.
Kesalahan kedua adalah menganggap semua atribut kelas berbahaya. Konstanta yang tidak pernah diubah, seperti mode = "normal" pada contoh sebelumnya, tetap aman sebagai atribut kelas dan justru lebih ringkas. Kewaspadaan hanya diperlukan ketika nilainya diubah melalui metode instans atau ketika tipe datanya dapat dimutasi. Kamus __dict__ pada instans maupun pada kelas dapat dipakai untuk memeriksa letak setiap atribut apabila perilaku pencariannya terasa samar.
Ringkasan#
- Atribut instans ditugaskan melalui
selfdan tersimpan per objek, sedangkan atribut kelas ditugaskan di badan kelas dan dibagi semua instans. - Pembacaan atribut menelusuri instans terlebih dahulu, lalu kelasnya, sedangkan penugasan melalui instans selalu membuat atribut instans.
- Atribut instans menutupi atribut kelas yang namanya sama, tanpa mengubah nilai milik kelas.
- Atribut kelas berupa list atau dictionary dimutasi pada satu salinan bersama, sehingga nilai awal yang dapat berubah sebaiknya dibuat di dalam
__init__.
Referensi#
- Class and Instance Variables — Python Software Foundation — Bagian tutorial resmi yang membedakan atribut kelas dan atribut instans.
- Class objects — Python Software Foundation — Penjelasan atribut kelas dan pencarian atribut pada tutorial resmi.