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