Interface #
Python tidak memiliki keyword interface seperti Java atau Go. Tapi Python memiliki sesuatu yang lebih fleksibel: tiga cara berbeda untuk mendefinisikan kontrak antara komponen — duck typing, Abstract Base Class (ABC), dan Protocol. Ketiganya menjawab pertanyaan yang sama: “Bagaimana saya memastikan sebuah objek memiliki metode yang saya butuhkan?” — tapi dengan tingkat keketatan dan ekspresi yang berbeda. Memahami kapan menggunakan masing-masing adalah kunci desain kode Python yang bersih dan dapat diperluas.
Duck Typing — Interface Implisit #
Sebelum membahas mekanisme formal, penting untuk memahami filosofi dasar Python: “Jika ia berjalan seperti bebek dan bersuara seperti bebek, maka ia adalah bebek.” Python tidak peduli tipe objek secara eksplisit — yang penting objek memiliki metode yang dibutuhkan.
# Tidak ada deklarasi interface — Python cukup panggil metodenya
class Anjing:
def bersuara(self):
return "Guk!"
class Kucing:
def bersuara(self):
return "Meow!"
class Bebek:
def bersuara(self):
return "Kwek!"
# Fungsi ini bekerja untuk semua objek yang punya metode bersuara()
# Tidak perlu inheritance atau interface deklarasi apapun
def buat_keributan(hewan_list):
for hewan in hewan_list:
print(hewan.bersuara()) # Python tidak cek tipe — langsung panggil
hewan = [Anjing(), Kucing(), Bebek()]
buat_keributan(hewan)
# → Guk!
# → Meow!
# → Kwek!
Duck typing sangat kuat dan fleksibel, tapi memiliki kelemahan: tidak ada mekanisme bawaan yang memastikan objek memang memiliki metode yang dibutuhkan sebelum runtime. Error baru muncul saat metode dipanggil — bukan saat objek dibuat.
Abstract Base Class (ABC) #
ABC adalah cara formal mendefinisikan interface di Python. Kelas yang mewarisi ABC dengan metode abstrak wajib mengimplementasikan semua metode abstrak tersebut — jika tidak, Python akan melempar TypeError saat mencoba membuat instance.
ABC Dasar #
from abc import ABC, abstractmethod
class Kendaraan(ABC):
"""Interface untuk semua jenis kendaraan."""
@abstractmethod
def bergerak(self) -> str:
"""Mengembalikan deskripsi gerakan kendaraan."""
...
@abstractmethod
def berhenti(self) -> None:
"""Menghentikan kendaraan."""
...
@abstractmethod
def kecepatan_maks(self) -> float:
"""Mengembalikan kecepatan maksimum dalam km/h."""
...
# Metode konkret di ABC — tersedia untuk semua subkelas
def info(self) -> str:
return f"{self.__class__.__name__}: kecepatan maks {self.kecepatan_maks()} km/h"
# ANTI-PATTERN: subkelas yang tidak mengimplementasikan semua metode abstrak
class Sepeda(Kendaraan):
def bergerak(self) -> str:
return "mengayuh pedal"
# berhenti() dan kecepatan_maks() belum diimplementasikan!
# sepeda = Sepeda()
# → TypeError: Can't instantiate abstract class Sepeda
# with abstract methods berhenti, kecepatan_maks
# BENAR: implementasikan semua metode abstrak
class Mobil(Kendaraan):
def __init__(self, merek: str, top_speed: float):
self.merek = merek
self._top_speed = top_speed
self._berjalan = False
def bergerak(self) -> str:
self._berjalan = True
return f"{self.merek} mulai bergerak"
def berhenti(self) -> None:
self._berjalan = False
def kecepatan_maks(self) -> float:
return self._top_speed
class Sepeda(Kendaraan):
def bergerak(self) -> str:
return "Mengayuh pedal"
def berhenti(self) -> None:
print("Menarik rem")
def kecepatan_maks(self) -> float:
return 30.0
class Kereta(Kendaraan):
def bergerak(self) -> str:
return "Melaju di rel"
def berhenti(self) -> None:
print("Rem pneumatik aktif")
def kecepatan_maks(self) -> float:
return 300.0
# Tidak bisa instantiasi ABC secara langsung
# kendaraan = Kendaraan() # → TypeError
mobil = Mobil("Toyota", 180.0)
sepeda = Sepeda()
kereta = Kereta()
print(mobil.bergerak()) # → Toyota mulai bergerak
print(mobil.info()) # → Mobil: kecepatan maks 180.0 km/h
print(sepeda.info()) # → Sepeda: kecepatan maks 30.0 km/h
print(kereta.info()) # → Kereta: kecepatan maks 300.0 km/h
# Polimorfisme — fungsi bekerja pada semua Kendaraan
def balapkan(kendaraan_list: list[Kendaraan]) -> None:
for k in sorted(kendaraan_list, key=lambda x: x.kecepatan_maks(), reverse=True):
print(f"{k.__class__.__name__}: {k.bergerak()} ({k.kecepatan_maks()} km/h)")
balapkan([mobil, sepeda, kereta])
# → Kereta: Melaju di rel (300.0 km/h)
# → Mobil: Toyota mulai bergerak (180.0 km/h)
# → Sepeda: Mengayuh pedal (30.0 km/h)
Abstract Property #
ABC juga mendukung abstract property — atribut yang wajib diimplementasikan oleh subkelas:
from abc import ABC, abstractmethod
class Bentuk(ABC):
"""Interface untuk bentuk geometri 2D."""
@property
@abstractmethod
def luas(self) -> float:
"""Luas bentuk dalam satuan persegi."""
...
@property
@abstractmethod
def keliling(self) -> float:
"""Keliling bentuk."""
...
def deskripsi(self) -> str:
return (
f"{self.__class__.__name__}: "
f"luas={self.luas:.2f}, keliling={self.keliling:.2f}"
)
class Lingkaran(Bentuk):
def __init__(self, jari_jari: float):
self.jari_jari = jari_jari
@property
def luas(self) -> float:
import math
return math.pi * self.jari_jari ** 2
@property
def keliling(self) -> float:
import math
return 2 * math.pi * self.jari_jari
class Persegi(Bentuk):
def __init__(self, sisi: float):
self.sisi = sisi
@property
def luas(self) -> float:
return self.sisi ** 2
@property
def keliling(self) -> float:
return 4 * self.sisi
class SegiTiga(Bentuk):
def __init__(self, alas: float, tinggi: float, sisi_a: float, sisi_b: float, sisi_c: float):
self.alas = alas
self.tinggi = tinggi
self._sisi = (sisi_a, sisi_b, sisi_c)
@property
def luas(self) -> float:
return 0.5 * self.alas * self.tinggi
@property
def keliling(self) -> float:
return sum(self._sisi)
bentuk_list: list[Bentuk] = [
Lingkaran(7),
Persegi(5),
SegiTiga(6, 4, 6, 5, 5),
]
for bentuk in bentuk_list:
print(bentuk.deskripsi())
# → Lingkaran: luas=153.94, keliling=43.98
# → Persegi: luas=25.00, keliling=20.00
# → SegiTiga: luas=12.00, keliling=16.00
ABC dengan Implementasi Default (Template Method Pattern) #
ABC tidak harus hanya berisi metode abstrak — bisa menyediakan implementasi default yang bisa di-override oleh subkelas:
from abc import ABC, abstractmethod
class Laporan(ABC):
"""Template untuk pembuatan laporan — pola Template Method."""
def buat(self) -> str:
"""Urutan langkah sudah ditentukan, detail diimplementasikan subkelas."""
bagian = [
self._header(),
self._isi(),
self._footer(),
]
return "\n".join(bagian)
@abstractmethod
def _header(self) -> str:
...
@abstractmethod
def _isi(self) -> str:
...
def _footer(self) -> str:
# Implementasi default — subkelas boleh override atau tidak
return "--- Akhir Laporan ---"
class LaporanPenjualan(Laporan):
def __init__(self, bulan: str, total: float):
self.bulan = bulan
self.total = total
def _header(self) -> str:
return f"=== LAPORAN PENJUALAN {self.bulan.upper()} ==="
def _isi(self) -> str:
return f"Total penjualan: Rp{self.total:,.0f}"
class LaporanInventaris(Laporan):
def __init__(self, produk_list: list):
self.produk_list = produk_list
def _header(self) -> str:
return "=== LAPORAN INVENTARIS ==="
def _isi(self) -> str:
baris = [f"- {p['nama']}: {p['stok']} unit" for p in self.produk_list]
return "\n".join(baris)
def _footer(self) -> str:
# Override footer untuk laporan inventaris
total_stok = sum(p['stok'] for p in self.produk_list)
return f"Total stok keseluruhan: {total_stok} unit"
lap_jual = LaporanPenjualan("Maret", 125_500_000)
print(lap_jual.buat())
# → === LAPORAN PENJUALAN MARET ===
# → Total penjualan: Rp125,500,000
# → --- Akhir Laporan ---
Virtual Subclass — Daftarkan Tanpa Mewarisi #
ABC juga mendukung virtual subclass — kelas yang dianggap sebagai implementasi ABC tanpa mewarisinya secara langsung. Berguna untuk mengintegrasikan kelas pihak ketiga ke dalam sistem tipe yang sudah ada.
from abc import ABC, abstractmethod
class Serializable(ABC):
@abstractmethod
def serialize(self) -> str:
...
@abstractmethod
def deserialize(self, data: str) -> None:
...
# Kelas pihak ketiga yang tidak mewarisi Serializable
class KelasLuarBiasa:
def serialize(self) -> str:
return '{"data": "ok"}'
def deserialize(self, data: str) -> None:
pass
# Daftarkan sebagai virtual subclass
Serializable.register(KelasLuarBiasa)
obj = KelasLuarBiasa()
print(isinstance(obj, Serializable)) # → True (meski tidak mewarisi)
print(issubclass(KelasLuarBiasa, Serializable)) # → True
Protocol — Structural Subtyping (Duck Typing Formal)
#
Protocol (diperkenalkan di Python 3.8 via PEP 544) adalah cara mendefinisikan interface berdasarkan struktur — bukan berdasarkan pewarisan. Kelas dianggap mengimplementasikan Protocol selama ia memiliki metode dan atribut yang diperlukan, tanpa perlu mewarisi atau mendaftarkan diri secara eksplisit.
from typing import Protocol, runtime_checkable
class Drawable(Protocol):
"""Protocol untuk objek yang bisa digambar."""
def draw(self) -> None:
...
def resize(self, factor: float) -> None:
...
# Kelas-kelas berikut TIDAK mewarisi Drawable — tapi dianggap mengimplementasikannya
# karena memiliki metode draw() dan resize()
class Lingkaran:
def __init__(self, r: float):
self.r = r
def draw(self) -> None:
print(f"Menggambar lingkaran r={self.r}")
def resize(self, factor: float) -> None:
self.r *= factor
class Kotak:
def __init__(self, w: float, h: float):
self.w = w
self.h = h
def draw(self) -> None:
print(f"Menggambar kotak {self.w}x{self.h}")
def resize(self, factor: float) -> None:
self.w *= factor
self.h *= factor
# Type checker (mypy) akan menerima keduanya sebagai Drawable
def render_semua(shapes: list[Drawable]) -> None:
for shape in shapes:
shape.draw()
shapes = [Lingkaran(5), Kotak(10, 8)]
render_semua(shapes)
# → Menggambar lingkaran r=5
# → Menggambar kotak 10x8
@runtime_checkable — Protocol yang Bisa Dicek dengan isinstance
#
from typing import Protocol, runtime_checkable
@runtime_checkable
class Dapat_Ditutup(Protocol):
def close(self) -> None:
...
class KoneksiDatabase:
def close(self) -> None:
print("Menutup koneksi database")
class FileHandler:
def close(self) -> None:
print("Menutup file")
class Timer:
def mulai(self) -> None: # tidak punya close()
pass
db = KoneksiDatabase()
fh = FileHandler()
tm = Timer()
print(isinstance(db, Dapat_Ditutup)) # → True
print(isinstance(fh, Dapat_Ditutup)) # → True
print(isinstance(tm, Dapat_Ditutup)) # → False
# Berguna untuk resource cleanup
def tutup_jika_bisa(obj: object) -> None:
if isinstance(obj, Dapat_Ditutup):
obj.close()
tutup_jika_bisa(db) # → Menutup koneksi database
tutup_jika_bisa(tm) # → (tidak ada output — Timer tidak punya close())
ABC vs Protocol — Kapan Menggunakan Yang Mana #
Gunakan ABC jika:
✓ Ingin memaksa implementasi metode saat kelas dibuat (bukan saat dipakai)
✓ ABC menyediakan implementasi default yang dibagikan ke subkelas
✓ Hubungan IS-A yang jelas dan eksplisit diperlukan
✓ Ingin menggunakan Template Method pattern
✓ Perlu virtual subclass untuk kelas pihak ketiga
Gunakan Protocol jika:
✓ Ingin duck typing yang terdokumentasi dan bisa dicek oleh type checker
✓ Kelas implementasi sudah ada dan tidak bisa diubah (tidak bisa tambahkan inheritance)
✓ Tidak perlu implementasi default — hanya mendefinisikan kontrak
✓ Ingin interface yang lebih ringan tanpa coupling ke hirarki kelas
✓ Bekerja dengan library pihak ketiga yang tidak bisa diubah
Untuk memperjelas perbedaan konseptual antara Nominal Subtyping (berdasarkan nama dan pewarisan eksplisit) dengan Structural Subtyping (berdasarkan kesamaan struktur tanpa pewarisan), perhatikan diagram kelas perbandingan di bawah ini:
classDiagram
class ABC_Model {
<<Abstract>>
+simpan(kunci, nilai)
+ambil(kunci)
}
class RedisStorage_ABC {
+simpan(kunci, nilai)
+ambil(kunci)
}
ABC_Model <|-- RedisStorage_ABC : Pewarisan Eksplisit (Nominal)
class Protocol_Model {
<<Protocol>>
+simpan(kunci, nilai)
+ambil(kunci)
}
class RedisStorage_Protocol {
+simpan(kunci, nilai)
+ambil(kunci)
}
Protocol_Model <.. RedisStorage_Protocol : Kesamaan Struktur (Structural)
Pada model ABC (Nominal), kelas RedisStorage harus secara eksplisit mewarisi PenyimpananABC. Sedangkan pada model Protocol (Structural), RedisStorage dianggap mengimplementasikan PenyimpananProtocol secara otomatis selama memiliki metode dengan struktur/signature yang sama, tanpa perlu mewarisi kelas apa pun.
Perbandingan langsung: ABC vs Protocol untuk interface yang sama #
# === Dengan ABC ===
from abc import ABC, abstractmethod
class PenyimpananABC(ABC):
@abstractmethod
def simpan(self, kunci: str, nilai: str) -> None: ...
@abstractmethod
def ambil(self, kunci: str) -> str | None: ...
class RedisStorage(PenyimpananABC): # HARUS mewarisi PenyimpananABC
def simpan(self, kunci, nilai): ...
def ambil(self, kunci): ...
# === Dengan Protocol ===
from typing import Protocol
class PenyimpananProtocol(Protocol):
def simpan(self, kunci: str, nilai: str) -> None: ...
def ambil(self, kunci: str) -> str | None: ...
class RedisStorage: # tidak perlu deklarasi apapun
def simpan(self, kunci, nilai): ...
def ambil(self, kunci): ...
# RedisStorage sudah kompatibel dengan PenyimpananProtocol
# tanpa mewarisi apapun — type checker akan memverifikasi strukturnya
Ringkasan #
- Duck typing adalah default Python — jika objek memiliki metode yang dibutuhkan, ia bisa digunakan tanpa deklarasi formal apapun.
- ABC memberikan kontrak eksplisit yang di-enforce saat kelas dibuat — subkelas yang tidak mengimplementasikan metode abstrak akan
TypeErrorsaat diinstansiasi, bukan saat metode dipanggil.@abstractmethodmenandai metode yang wajib diimplementasikan.@property+@abstractmethoduntuk atribut wajib.- ABC boleh memiliki implementasi default — subkelas bisa mewarisi atau meng-override. Pola ini disebut Template Method.
- Virtual subclass (
ABC.register()) memungkinkan kelas pihak ketiga dianggap sebagai implementasi ABC tanpa mengubah kodenya.Protocolmendefinisikan interface berdasarkan struktur, bukan pewarisan — kelas apapun yang memiliki metode yang diperlukan dianggap kompatibel.@runtime_checkablememungkinkanisinstance()bekerja pada Protocol — berguna untuk resource cleanup dan dispatch dinamis.- Pilih ABC jika butuh enforcement saat kelas didefinisikan atau implementasi default dishare. Pilih Protocol jika butuh interface yang lebih ringan dan tidak ingin coupling ke hirarki kelas.