Pada Chapter Dynamic Typing kita mengenal duck typing: interpreter Python tidak peduli asal-usul tipe — yang penting objek punya method yang dipanggil. Namun ABC pada chapter sebelumnya masih menuntut hubungan nominal: subclass harus mewarisi kontraknya. Bagaimana bila kita ingin kompatibilitas tipe tanpa memaksa semua class mewarisi satu induk? Di situlah typing.Protocol berperan: kompatibilitas ditentukan oleh bentuk — method apa saja yang dimiliki — bukan oleh silsilah. Prinsipnya kerap diringkas sebagai “if it quacks like a duck”: jika class berkata dia bisa berkwik, dia adalah bebek.
Nominal vs Structural Typing #
Dua filosofi penentuan kompatibilitas tipe:
| Aspek | Nominal (ABC/inheritance) | Structural (Protocol) |
|---|---|---|
| Dasar kompatibilitas | hubungan pewarisan yang dinyatakan | kecocokan bentuk method |
| Syarat pada class | harus mewarisi ABC | tidak ada — cukup punya methodnya |
| Ditegakkan oleh | interpreter saat instansiasi | static checker, opsional isinstance |
| Cocok untuk | hierarki yang kita kendalikan penuh | integrasi class luar/pihak ketiga |
Pada dunia nominal, class Kucing(BisaDibunyikan) adalah pernyataan eksplisit. Pada dunia struktural, Kucing kompatibel dengan BisaDibunyikan begitu class tersebut memiliki method bunyi() — tanpa perlu tahu Protocol itu ada.
Mendefinisikan Protocol #
Protocol didefinisikan dengan mewarisi typing.Protocol, dan setiap method di dalamnya hanya ditulis tanda tangannya:
from typing import Protocol
class BisaDibunyikan(Protocol):
def bunyi(self) -> str: ...
class Kucing:
def bunyi(self) -> str:
return "meong"
class MesinAlarm:
def bunyi(self) -> str:
return "beep beep"
def bunyikan(alat: BisaDibunyikan) -> str:
return alat.bunyi()
print(bunyikan(Kucing()))
print(bunyikan(MesinAlarm()))
# tanpa @runtime_checkable, isinstance justru error
try:
isinstance(Kucing(), BisaDibunyikan)
except TypeError as e:
print("TypeError:", e)Output yang Dihasilkan:
meong
beep beep
TypeError: Instance and class checks can only be used with @runtime_checkable protocolsMari kita bedah elemen kuncinya:
- Baris 4 (
class BisaDibunyikan(Protocol)): definisi Protocol. Methodbunyidiisi...karena hanya tanda tangan yang relevan — tidak ada implementasi yang diwariskan. - Baris 8 (
class Kucing) dan Baris 13 (class MesinAlarm): keduanya tidak mewarisiBisaDibunyikan, tidak mengimpor, bahkan tidak mengetahui keberadaan Protocol ini. Keduanya hanya kebetulan punya methodbunyi(). - Baris 18 (
def bunyikan(alat: BisaDibunyikan)): annotasi parameter memakai Protocol. Kucing dan MesinAlarm sama-sama diterima karena bentuknya cocok. - Baris 27 (
isinstance(...)): Protocol polos menolak pemeriksaan runtime denganTypeError— penegakan utamanya memang bukan di runtime.
Kontras dengan ABC: tidak ada TypeError tentang method abstrak, tidak ada instansiasi yang ditolak. Protocol sepenuhnya adalah alat deklaratif untuk static checker dan pembaca kode.
@runtime_checkable dan isinstance #
Bila pemeriksaan runtime memang diperlukan, decorator @runtime_checkable membukanya:
from typing import Protocol, runtime_checkable
@runtime_checkable
class BisaDibunyikan(Protocol):
def bunyi(self) -> str: ...
class Kucing:
def bunyi(self) -> str:
return "meong"
class Meja:
pass
class Palsu:
def bunyi(self, volume, nada): # tanda tangan berbeda
return "akui"
print(isinstance(Kucing(), BisaDibunyikan))
print(isinstance(Meja(), BisaDibunyikan))
# isinstance hanya memeriksa KEBERADAAN method, bukan tanda tangannya
print(isinstance(Palsu(), BisaDibunyikan))Output yang Dihasilkan:
True
False
TrueKucing lolos, Meja ditolak — tetapi perhatikan class Palsu: method bunyi-nya menerima dua parameter wajib, tanda tangan yang tidak akan pernah bisa dipanggil sebagai alat.bunyi(). isinstance tetap melaporkan True.
Pemeriksaan Statis dengan mypy #
Kekuatan penuh Protocol muncul pada static checker seperti mypy: checker membandingkan bentuk class terhadap Protocol secara struktural, sebelum program dijalankan. Static checker menolak argumen yang method-nya tidak cocok — misalnya class Faktur yang hanya punya render() padahal Protocol menuntut tampil(). Pendalaman type hinding dan penggunaan mypy menanti pada section Type Hints dan Static Analysis.
Di runtime, tanpa pemeriksaan apa pun, ketidakcocokan baru terasa saat method yang hilang benar-benar dipanggil:
from typing import Protocol
class BisaDicetak(Protocol):
def tampil(self) -> str: ...
class Artikel:
def tampil(self) -> str:
return "isi artikel"
class Faktur:
def render(self) -> str: # method-nya bernama render, bukan tampil
return "isi faktur"
def cetak(dokumen: BisaDicetak) -> None:
print(dokumen.tampil())
cetak(Artikel())
# Python runtime TIDAK memeriksa Protocol: Faktur lolos sampai method dipanggil
try:
cetak(Faktur())
except AttributeError as e:
print("AttributeError:", e)Output yang Dihasilkan:
isi artikel
AttributeError: 'Faktur' object has no attribute 'tampil'Artikel diproses normal karena bentuknya cocok. Faktur tidak ditolak saat dilempar ke cetak() — galat AttributeError baru muncul pada baris dokumen.tampil(), tepat saat method yang dijanjikan Protocol ternyata tidak ada. Inilah pembagian kerja yang perlu diingat: @runtime_checkable memberi pemeriksaan dangkal, static checker memberi pemeriksaan penuh, dan interpreter menjalankan kode apa adanya.