Inheritance dan super()

Subclassing, overriding, dan pemanggilan super() secara kooperatif.

Daftar isi

Pewarisan (inheritance) memungkinkan sebuah kelas menyerap atribut dan metode kelas lain, lalu menambahkan atau mengubah sebagiannya. Kelas yang menyerap disebut subclass, sedangkan kelas yang diserap disebut kelas induk. Topik ini membahas pembuatan subclass, pengubahan perilaku melalui overriding, serta pemanggilan super() untuk merangkai inisialisasi antar kelas secara kooperatif.

Subclass mewarisi perilaku induk#

Subclass dideklarasikan dengan menuliskan nama kelas induknya di dalam tanda kurung setelah nama kelasnya. Seluruh metode dan atribut kelas induk menjadi tersedia pada subclass tanpa menulis ulang apa pun, sehingga perilaku yang sama cukup didefinisikan satu kali. Fungsi isinstance() menunjukkan bahwa sebuah instans juga dianggap bagian dari tipe induknya, dan sifat ini dipakai banyak pustaka untuk mengenali objek.

python Contoh: subclass tanpa penulisan ulang
class Bentuk:
    def deskripsi(self):
        return "sebuah bentuk"

class Lingkaran(Bentuk):
    pass

l = Lingkaran()
print(l.deskripsi())
print(isinstance(l, Bentuk))
text Keluaran
sebuah bentuk
True

Kelas Lingkaran badannya kosong, tetapi instansnya tetap dapat memanggil deskripsi() karena metode itu ditemukan pada kelas induknya. Hasil True dari isinstance(l, Bentuk) menegaskan bahwa setiap Lingkaran juga berupa Bentuk. Sebenarnya setiap kelas di Python berakar pada kelas object, sehingga pewarisan tunggal ini hanya memperpanjang rantai yang sudah ada, dan atribut __bases__ pada kelas menampilkan induk langsungnya. Apabila perilaku bawaan sudah cukup untuk sebagian kasus, subclass hanya perlu mendefinisikan hal yang benar-benar berbeda.

Overriding dan rantai init#

Overriding adalah penulisan ulang metode induk pada subclass dengan nama yang sama, sehingga versi subclass yang dipanggil. Kelas turunan juga wajib mengurus inisialisasinya sendiri, dan di sinilah super() berperan, karena pemanggilan super().__init__(...) meneruskan sebagian argumen ke kelas induk. Tanpa pemanggilan itu, atribut yang ditugaskan oleh kelas induk tidak pernah ada pada instans turunannya. Berkas berikut membangun hierarki bentuk dengan rantai __init__ dua tingkat.

python bentuk.py
class Bentuk:
    def __init__(self, nama):
        self.nama = nama

    def luas(self):
        return 0

    def deskripsi(self):
        return f"{self.nama} dengan luas {self.luas()}"


class PersegiPanjang(Bentuk):
    def __init__(self, panjang, lebar):
        super().__init__("persegi panjang")
        self.panjang = panjang
        self.lebar = lebar

    def luas(self):
        return self.panjang * self.lebar


class Persegi(PersegiPanjang):
    def __init__(self, sisi):
        super().__init__(sisi, sisi)
        self.nama = "persegi"

koleksi = [PersegiPanjang(4, 5), Persegi(3)]
for b in koleksi:
    print(b.deskripsi())
text Keluaran
persegi panjang dengan luas 20
persegi dengan luas 9

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

bash
# menjalankan program hierarki bentuk
python3 bentuk.py
text Keluaran
persegi panjang dengan luas 20
persegi dengan luas 9

Metode luas() dioverride di PersegiPanjang, dan deskripsi() milik Bentuk otomatis memakai versi terbaru tersebut tanpa perubahan apa pun. Inilah polimorfisme (polymorphism) yang bekerja di sini, yaitu pemanggilan metode yang sama menghasilkan perilaku yang menyesuaikan kelas instansnya. Pola ini menunjukkan bahwa deskripsi() tidak perlu ditulis ulang sama sekali karena hanya bergantung pada atribut dan luas().

Pembuatan Persegi(3) menjalankan __init__ dua tingkat, yaitu Persegi memanggil PersegiPanjang, dan PersegiPanjang memanggil Bentuk melalui super(). Hasil akhirnya adalah instans dengan panjang dan lebar bernilai sama, kemudian nama diperbaiki menjadi persegi. Sebaliknya, apabila __init__ induk sudah cocok, subclass tidak perlu menulis __init__ sama sekali, dan versi warisannya dipakai apa adanya.

Memperluas metode dengan super()#

Overriding tidak selalu berarti mengganti seluruh perilaku induk. Apabila subclass ingin menambah langkah sebelum atau sesudah versi induknya, metode baru memanggil super().nama_metode(...) lalu melanjutkan pekerjaannya. Bentuk ini disebut pemanggilan kooperatif, karena setiap kelas hanya mengurus bagian sendiri dan menyerahkan sisanya ke kelas di atasnya.

python Contoh: memperluas perilaku metode induk
class Notifikasi:
    def kirim(self, pesan):
        print("catat:", pesan)


class NotifikasiEmail(Notifikasi):
    def kirim(self, pesan):
        super().kirim(pesan)
        print("email:", pesan)

n = NotifikasiEmail()
n.kirim("cadangan selesai")
text Keluaran
catat: cadangan selesai
email: cadangan selesai

Pemanggilan n.kirim() menjalankan versi NotifikasiEmail, yang lebih dahulu meneruskan pesan ke versi Notifikasi melalui super().kirim(). Keluaran catat: berasal dari kelas induk, sedangkan email: berasal dari subclass. Urutan eksekusinya mengikuti letak pemanggilan super(), sehingga memindahkannya ke akhir metode membuat tambahan subclass berjalan lebih dahulu. Apabila suatu hari ada kelas di antara keduanya, pemanggilan super() tetap benar tanpa perlu menyunting subclass di bawahnya.

Satu kedisiplinan kecil dibutuhkan agar pola ini tetap aman, yaitu mempertahankan tanda tangan metode yang dioverride. Parameter induk yang tidak dibutuhkan subclass sebaiknya tetap diterima dan diteruskan, sehingga kode yang memanggil melalui tipe induk tidak pernah kehilangan argumen. Pelanggaran disiplin ini biasanya baru terlihat jauh dari sumbernya, saat pemanggil lama mengirim argumen yang versi barunya tidak menampung lagi.

Kesalahan umum#

Kesalahan pertama adalah menulis __init__ baru pada subclass tanpa memanggil super().__init__(), sehingga atribut yang diisi kelas induk hilang.

python Contoh: lupa memanggil super().__init__
class Kendaraan:
    def __init__(self, roda):
        self.roda = roda

class Sepeda(Kendaraan):
    def __init__(self, roda, gigi):
        self.gigi = gigi

s = Sepeda(2, 21)
try:
    print(s.roda)
except AttributeError as e:
    print(type(e).__name__, str(e))
text Keluaran
AttributeError 'Sepeda' object has no attribute 'roda'

Definisi __init__ pada Sepeda menggantikan definisi Kendaraan sepenuhnya, sehingga baris self.roda = roda tidak pernah dijalankan. Perbaikannya adalah menambahkan super().__init__(roda) sebelum penugasan lain, agar inisialisasi induk dan subclass berjalan berurutan. Aturan praktisnya sederhana, yaitu setiap __init__ subclass memanggil super().__init__() kecuali ada alasan yang sangat jelas untuk menolak inisialisasi induknya. Kesalahan ini tidak menimbulkan error karena Python tidak memberi peringatan saat __init__ ditulis ulang, sehingga pengujian yang mengisi seluruh atribut menjadi penangkapnya yang paling awal.

Kesalahan kedua adalah memanggil kelas induk dengan menuliskan namanya secara langsung, misalnya Notifikasi.kirim(self, pesan), alih-alih super().kirim(pesan). Kode semacam itu bekerja pada pewarisan tunggal, tetapi gagal ketika hierarkinya berubah, karena nama induk yang ditulis keras tidak mengikuti susunan kelas yang sebenarnya. Pemanggilan super() menelusuri susunan kelas instans yang bersangkutan, sehingga tetap benar pada rantai pewarisan yang lebih dalam maupun pada pewarisan ganda. Kesalahan ketiga adalah mengoverride metode induk lalu melupakan perilaku aslinya, misalnya __init__ yang tidak meneruskan argumen wajib, sehingga instans menjadi tidak lengkap. Sebelum mengoverride, periksa dulu tanggung jawab metode induknya, lalu putuskan apakah seluruhnya diganti atau hanya diperluas.

Ringkasan#

  • Subclass mewarisi seluruh metode dan atribut kelas induknya, sehingga perilaku yang sama cukup didefinisikan sekali.
  • Overriding menulis ulang metode induk dengan nama yang sama, dan versi subclass yang dipanggil.
  • super().__init__() merangkai inisialisasi dari subclass ke induknya, sehingga atribut milik induk tetap terisi.
  • Pemanggilan super() mengikuti susunan kelas instans yang bersangkutan, bukan nama kelas yang ditulis keras, sehingga aman dipakai secara kooperatif.

Referensi#