Atribut instans vs atribut kelas

Perbedaan atribut instans dan atribut kelas beserta jebakan state bersama.

Daftar isi

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.

python Contoh: atribut instans per objek
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)
text Keluaran
gudang 27.5
laboratorium 21.0
30.0 21.0

Penugasan 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.

python Contoh: penghitung jumlah instans
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)
text Keluaran
gudang laboratorium
2

Atribut 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.

python Contoh: atribut instans menutupi atribut kelas
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)
text Keluaran
normal normal
hemat normal
normal

Sebelum 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__.

python arsip-catatan.py
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)
text Keluaran
rusak: ['nota pembelian']
baik: []

Simpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.

bash
# menjalankan program perbandingan arsip
python3 arsip-catatan.py
text Keluaran
rusak: ['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.

python Contoh: self.nilai += 1 membuat atribut instans
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)
text Keluaran
2 0 0

Pernyataan 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 self dan 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#