Pendalaman enum dan typing

Modul enum dan typing, meliputi Enum, Optional, Union, Protocol, dan TypedDict.

Daftar isi

Modul enum mengubah kumpulan konstanta menjadi tipe dengan anggota bernama, sedangkan modul typing menyediakan kosakata anotasi untuk menggambarkan tipe data kepada pembaca dan alat pemeriksa statis. Keduanya tidak menambah kecepatan program, tetapi keduanya menaikkan kejelasan kode, karena maksud data tertulis secara eksplisit dan diverifikasi sebelum program dijalankan. Topik ini membahas Enum beserta auto, anotasi Optional dan Union dengan sintaks pipa, Protocol untuk pengetikan struktural, TypedDict untuk dictionary terstruktur, serta program kecil yang memadukannya.

Enum untuk konstanta bernama#

Kelas yang mewarisi Enum mendefinisikan anggota (member) dengan nama dan nilai. Setiap anggota berupa objek tunggal, sehingga perbandingan antar anggota berlangsung lewat identitas, dan anggota dapat dicari dari nilai maupun dari namanya.

python Contoh: enum Status dan akses anggotanya
from enum import Enum

class Status(Enum):
    MENUNGGU = 1
    BERJALAN = 2
    SELESAI = 3

tugas = Status.BERJALAN
print(tugas)
print(tugas.name, tugas.value)
print(tugas is Status.BERJALAN)
print(Status(2))
print(Status["SELESAI"])
print([s.name for s in Status])
text Keluaran
Status.BERJALAN
BERJALAN 2
True
Status.BERJALAN
Status.SELESAI
['MENUNGGU', 'BERJALAN', 'SELESAI']

Pencetakan tugas menampilkan Status.BERJALAN, yaitu repr yang menyebut kelas dan nama anggotanya, bukan nilai mentahnya. Pencarian Status(2) bekerja dari nilai ke anggota, sedangkan Status["SELESAI"] bekerja dari nama ke anggota. Iterasi atas kelas menghasilkan seluruh anggota menurut urutan definisinya, sehingga menu seperti daftar pilihan dapat dibangun langsung dari enum tersebut.

Nilai otomatis dengan auto#

Fungsi auto() menetapkan nilai anggota secara otomatis, dimulai dari 1 dan bertambah satu setiap pemanggilan. Nilai yang dihasilkan sengaja dianggap tidak penting bagi pemakai enum, sehingga definisi tetap ringkas ketika nilai mentah memang tidak dipakai untuk apa pun.

python Contoh: nilai berurutan dengan auto
from enum import Enum, auto

class Arah(Enum):
    UTARA = auto()
    SELATAN = auto()
    TIMUR = auto()
    BARAT = auto()

print([a.value for a in Arah])
print(Arah(2).name)
text Keluaran
[1, 2, 3, 4]
SELATAN

Pencarian Arah(2) tetap berfungsi karena nilai otomatis itu tetap berupa angka berurutan. Apabila nilai enum perlu dibandingkan dengan teks pada data luar, pustaka enum menyediakan turunan seperti StrEnum yang anggotanya berupa string. Untuk penggunaan di dalam program, nilai otomatis sudah cukup dan menghindari angka ajaib, yaitu angka yang ditulis berulang tanpa penjelasan.

Optional, Union, dan sintaks pipa#

Anotasi Optional[X] menyatakan nilai bertipe X atau None, sedangkan Union[X, Y] menyatakan pilihan di antara beberapa tipe. Sejak Python 3.10, keduanya dapat ditulis dengan operator pipa, yaitu X | None dan X | Y, dan bentuk pipa inilah yang lazim pada kode modern.

python Contoh: anotasi Optional dan Union
def panjang_atau_nol(teks: str | None) -> int:
    if teks is None:
        return 0
    return len(teks)

def ringkas(nilai: int | str) -> str:
    if isinstance(nilai, int):
        return "angka " + str(nilai)
    return "teks " + repr(nilai)

kamus = {"a": "apel"}
print(panjang_atau_nol(kamus.get("a")))
print(panjang_atau_nol(kamus.get("z")))
print(panjang_atau_nol(None))
print(ringkas(7), ringkas("tujuh"))
text Keluaran
4
0
0
angka 7 teks 'tujuh'

Kunci z yang tidak ada membuat kamus.get("z") mengembalikan None, dan fungsi yang dianotasi str | None menangani kemungkinan itu secara eksplisit alih-alih menimbulkan TypeError. Anotasi int | str pada ringkas menggambarkan pola yang sama untuk pilihan tipe, dan cabang isinstance di dalamnya menjadi penentu jalannya.

Protocol dan TypedDict#

Kelas Protocol menetapkan kontrak metode tanpa pewarisan, sehingga objek apa pun yang memiliki metode yang sesuai dianggap memenuhi protokolnya. Pendekatan ini memformalkan duck typing menjadi sesuatu yang dapat diperiksa alat statis. Sementara itu, TypedDict menggambarkan dictionary dengan kunci dan tipe nilai yang ditetapkan, tetapi tetap berupa dictionary biasa saat dijalankan.

python Contoh: Protocol dan TypedDict
from typing import Protocol, TypedDict, runtime_checkable

@runtime_checkable
class Penyimpan(Protocol):
    def simpan(self, kunci: str, nilai: str) -> None: ...

class Memori:
    def simpan(self, kunci: str, nilai: str) -> None:
        print("menyimpan", kunci, "=", nilai, "di memori")

class Berkas:
    def simpan(self, kunci: str, nilai: str) -> None:
        print("menyimpan", kunci, "=", nilai, "di berkas")

def tulis(wadah: Penyimpan) -> None:
    wadah.simpan("status", "aktif")

tulis(Memori())
tulis(Berkas())
print(isinstance(Memori(), Penyimpan))
text Keluaran
menyimpan status = aktif di memori
menyimpan status = aktif di berkas
True

Kedua kelas tidak mewarisi apa pun, tetapi keduanya memenuhi Penyimpan karena memiliki metode simpan dengan tanda tangan yang sesuai. Decorator runtime_checkable membuat protokol dapat dipakai pada isinstance, yang hanya memeriksa keberadaan metodenya. Untuk data, TypedDict mendefinisikan bentuk dictionary secara bernama.

python Contoh: TypedDict dan anotasinya
from typing import TypedDict

class Pengguna(TypedDict):
    nama: str
    umur: int

data: Pengguna = {"nama": "Dewi", "umur": 24}
print(data["nama"], data["umur"])
print(Pengguna.__annotations__)
text Keluaran
Dewi 24
{'nama': <class 'str'>, 'umur': <class 'int'>}

Nilai data tetap berupa dict biasa dan tidak ada pemeriksaan saat dijalankan, tetapi alat statis akan menolak kunci yang tidak dikenal atau nilai yang bertipe keliru. Kunci opsional dinyatakan melalui warisan total=False, sehingga bentuk dictionary yang longgar tetap terdokumentasi.

Program utuh: daftar tugas bertipe#

Contoh berikut memadukan Enum, Optional, dan TypedDict pada satu program kecil, yaitu daftar tugas dengan status bernama dan catatan yang boleh kosong.

python status_tugas.py
from enum import Enum, auto
from typing import Optional, TypedDict


class Status(Enum):
    MENUNGGU = auto()
    BERJALAN = auto()
    SELESAI = auto()


class Tugas(TypedDict):
    judul: str
    status: Status
    catatan: Optional[str]


def ringkas(tugas: Tugas) -> str:
    baris = f"{tugas['judul']}: {tugas['status'].name}"
    if tugas["catatan"] is not None:
        baris += f" ({tugas['catatan']})"
    return baris


daftar = [
    Tugas(judul="cadangkan arsip", status=Status.SELESAI, catatan=None),
    Tugas(judul="perbarui laporan", status=Status.BERJALAN, catatan="menunggu data"),
]

for tugas in daftar:
    print(ringkas(tugas))
    print("nilai enum:", tugas["status"].value)
text Keluaran
cadangkan arsip: SELESAI
nilai enum: 3
perbarui laporan: BERJALAN (menunggu data)
nilai enum: 2

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

bash
# menjalankan program daftar tugas bertipe
python3 status_tugas.py
text Keluaran
cadangkan arsip: SELESAI
nilai enum: 3
perbarui laporan: BERJALAN (menunggu data)
nilai enum: 2

Tipe Tugas menggambarkan bentuk datanya, sedangkan Status menggambarkan himpunan nilai yang sah untuk kolom statusnya. Nilai catatan yang dianotasi Optional[str] dibedakan dengan None sebelum dipakai, sehingga baris ringkasan menyesuaikan diri tanpa mencetak None mentah.

Kesalahan umum#

Kesalahan pertama adalah membandingkan anggota enum dengan nilai mentahnya. Anggota Enum bukan angka maupun string, sehingga perbandingan langsung dengan nilai mentah menghasilkan False meskipun nilainya sama.

python Contoh: enum tidak sama dengan nilai mentahnya
from enum import Enum

class Status(Enum):
    BERJALAN = 2

print(Status.BERJALAN == 2)
print(Status.BERJALAN == Status(2))
text Keluaran
False
True

Perbandingan yang benar dilakukan antar anggota, baik langsung seperti Status.BERJALAN maupun melalui konversi Status(2) dari nilai mentah yang datang dari luar program.

Kesalahan kedua adalah menganggap anotasi tipe sebagai penjaga runtime. Fungsi yang dianotasi int tetap menerima argumen lain dan menjalankannya seadanya.

python Contoh: anotasi tidak diperiksa saat berjalan
def dobel(x: int) -> int:
    return x * 2

print(dobel("ab"))
text Keluaran
abab

Fungsi itu menggandakan string tanpa error karena * sah untuk str, dan error hanya muncul bila operasinya memang tidak didukung tipe itu. Pemeriksaan tipe menempati ranah alat statis, sehingga penemuan dobel("ab") yang tidak disengaja menjadi tugas mypy atau pyright, bukan interpreter.

Kesalahan ketiga adalah memakai isinstance pada Protocol yang tidak didekorasi runtime_checkable. Pemeriksaan itu langsung menimbulkan error, sehingga objeknya tidak turut diperiksa.

python Contoh: isinstance pada protokol tanpa runtime_checkable
from typing import Protocol

class Diurut(Protocol):
    def __lt__(self, lain): ...

try:
    isinstance(3, Diurut)
except TypeError as e:
    print(type(e).__name__, str(e))
text Keluaran
TypeError Instance and class checks can only be used with @runtime_checkable protocols

Perbaikannya adalah menambahkan decorator @runtime_checkable pada protokol yang memang perlu diuji saat berjalan, dengan catatan pemeriksaannya hanya menilai keberadaan nama metode, bukan tanda tangannya.

Ringkasan#

  • Kelas Enum mendefinisikan anggota bernama yang dibandingkan lewat identitas, dapat dicari dari nilai dan dari nama, serta mendukung nilai otomatis melalui auto().
  • Anotasi Optional[X] dan Union ditulis modern sebagai X | None dan X | Y, dan seluruh anotasi diperiksa oleh alat statis, bukan interpreter.
  • Protocol menetapkan kontrak metode tanpa pewarisan, dengan @runtime_checkable mengizinkan pemeriksaan isinstance yang bersifat nama-metode saja.
  • TypedDict menggambarkan dictionary dengan kunci dan tipe tetap, tetapi objeknya tetap berupa dictionary biasa saat program berjalan.

Referensi#