Setelah memahami sintaks dasar, struktur kontrol, fungsi, dan pemrograman berorientasi objek, tantangan Python biasanya bergeser. Masalahnya bukan lagi bagaimana membuat kode berjalan, melainkan bagaimana menjaga kode tetap mudah diuji, dirawat, dan dikembangkan ketika ukurannya bertambah.
Pada tahap ini, fitur Python seperti decorator, context manager, generator, type hint, dan pemrograman asynchronous mulai relevan. Namun, fitur-fitur tersebut tidak seharusnya dipakai hanya karena terlihat lebih canggih. Masing-masing menyelesaikan jenis masalah tertentu, dengan konsekuensi terhadap alur eksekusi, keterbacaan, dan cara pengujian.
Mulai dari Batas Modul yang Jelas
Kode Python yang berkembang sering mengalami masalah ketika satu berkas menangani terlalu banyak hal: membaca konfigurasi, mengakses database, memvalidasi input, memproses aturan bisnis, dan mencetak hasil. Setiap perubahan kemudian berisiko memengaruhi bagian lain yang tidak berkaitan langsung.
Pemisahan modul membantu membatasi tanggung jawab. Sebagai contoh, modul repository dapat berfokus pada akses data, modul service menangani aturan bisnis, sedangkan modul antarmuka menerima input dan menyusun respons. Nama dan pembagian ini bukan aturan wajib, tetapi prinsipnya sama: detail teknis sebaiknya tidak tersebar ke seluruh aplikasi.
Contoh ilustratif berikut menunjukkan pemisahan sederhana antara validasi dan perhitungan:
def validate_items(items):
if not items:
raise ValueError("Daftar item tidak boleh kosong")
for item in items:
if item < 0:
raise ValueError("Nilai item tidak boleh negatif")
def calculate_total(items):
validate_items(items)
return sum(items)
Dalam aplikasi nyata, fungsi ini mungkin perlu dipisahkan lagi sesuai konteks. Yang perlu diperhatikan adalah adanya batas yang dapat dikenali: validasi memiliki tanggung jawab sendiri, sementara perhitungan tidak perlu mengetahui bagaimana input diterima dari pengguna.
Decorator: Menambahkan Perilaku Tanpa Mengubah Fungsi
Decorator adalah fungsi yang menerima fungsi lain lalu mengembalikan fungsi baru. Pola ini berguna ketika beberapa fungsi membutuhkan perilaku tambahan yang sama, misalnya pencatatan sederhana, pemeriksaan hak akses, pengukuran durasi, atau percobaan ulang dengan aturan yang jelas.
Decorator yang baik tidak menyembunyikan perilaku utama secara berlebihan. Pembaca tetap perlu dapat memahami bahwa fungsi tertentu memiliki pemeriksaan atau efek tambahan.
from functools import wraps
def log_call(function):
@wraps(function)
def wrapper(*args, **kwargs):
print(f"Memanggil {function.__name__}")
result = function(*args, **kwargs)
print(f"Selesai: {function.__name__}")
return result
return wrapper
@log_call
def add(a, b):
return a + b
Penggunaan functools.wraps mempertahankan metadata fungsi seperti nama dan dokumentasinya. Tanpa itu, alat debugging dan introspeksi dapat melihat nama fungsi pembungkus, bukan fungsi asli.
Decorator juga dapat menerima parameter, tetapi strukturnya menjadi satu tingkat lebih dalam. Kompleksitas tersebut sebaiknya dibenarkan oleh kebutuhan yang berulang. Jika hanya satu fungsi yang memerlukan perilaku tambahan, pemanggilan fungsi biasa sering lebih mudah dibaca.
Risiko Decorator yang Terlalu Tersembunyi
Decorator dapat mengubah argumen, menelan exception, mengubah nilai balik, atau menambahkan efek samping. Perubahan semacam ini membuat kontrak fungsi tidak lagi terlihat dari definisinya. Karena itu, decorator sebaiknya memiliki tujuan sempit dan dokumentasi yang menjelaskan perilakunya.
Decorator untuk pengukuran waktu, misalnya, seharusnya tidak sekaligus mengubah format hasil dan menangani semua jenis exception. Memisahkan tanggung jawab akan membuat pengujian lebih terarah.
Context Manager untuk Resource yang Harus Ditutup
File, koneksi, lock, dan resource serupa perlu dilepaskan setelah digunakan. Context manager menyediakan cara untuk mengekspresikan pola tersebut melalui blok with. Python akan menjalankan proses masuk dan keluar context sesuai aturan yang ditentukan.
with open("data.txt", encoding="utf-8") as file:
content = file.read()
Pada contoh ini, penutupan file ditangani oleh context manager, termasuk ketika terjadi exception di dalam blok. Hal tersebut mengurangi kemungkinan resource tertinggal terbuka karena jalur keluar fungsi tidak hanya satu.
Context manager juga dapat dibuat dengan class yang menyediakan __enter__ dan __exit__, atau dengan decorator contextlib.contextmanager.
from contextlib import contextmanager
@contextmanager
def temporary_message(message):
print(f"Mulai: {message}")
try:
yield
finally:
print(f"Selesai: {message}")
with temporary_message("proses data"):
print("Menjalankan proses")
Blok finally tetap dijalankan ketika terjadi exception. Untuk resource yang benar-benar harus dikembalikan ke keadaan aman, penanganan di bagian keluar context harus dibuat hati-hati. Jangan menelan exception tanpa alasan, karena hal itu dapat menyamarkan kegagalan.
Abstraksi yang baik bukan yang menyembunyikan semua detail, melainkan yang menyembunyikan detail yang tidak perlu diketahui pemanggilnya.
Generator dan Iterator untuk Pemrosesan Bertahap
Fungsi yang menggunakan yield menghasilkan generator. Generator tidak langsung membentuk seluruh hasil dalam satu struktur data. Nilai diberikan ketika diminta melalui proses iterasi.
def read_positive_numbers(numbers):
for number in numbers:
if number > 0:
yield number
numbers = read_positive_numbers([3, -1, 4, 0, 8])
for number in numbers:
print(number)
Pola ini berguna ketika data diproses bertahap atau ketika seluruh hasil tidak perlu disimpan sekaligus. Namun, generator bersifat konsumtif: setelah iterasi selesai, generator yang sama tidak dapat dipakai untuk mengulang hasil tanpa membuatnya kembali.
Generator juga dapat dirangkai menjadi pipeline. Contoh ilustratif berikut memisahkan pembacaan, penyaringan, dan transformasi:
def non_empty(lines):
for line in lines:
cleaned = line.strip()
if cleaned:
yield cleaned
def uppercase(lines):
for line in lines:
yield line.upper()
pipeline = uppercase(non_empty([" python ", "", "generator"]))
Setiap tahap memiliki satu tanggung jawab. Jika data perlu digunakan berkali-kali, atau jika ukurannya kecil dan keterbacaan lebih penting daripada pemrosesan bertahap, list comprehension mungkin lebih sesuai.
Type Hint sebagai Kontrak Kode
Type hint membantu menjelaskan jenis input dan output yang diharapkan. Python tetap bersifat dynamically typed, sehingga type hint tidak secara otomatis memaksa semua nilai mengikuti anotasi saat program berjalan. Nilai utamanya terletak pada dokumentasi, pemeriksaan statis, dan bantuan dari editor.
def calculate_total(prices: list[float]) -> float:
return sum(prices)
Anotasi menjadi lebih berguna ketika struktur data memiliki aturan yang lebih spesifik. TypedDict, misalnya, dapat mendeskripsikan dictionary yang memiliki kunci tertentu, sedangkan Protocol dapat menjelaskan perilaku yang dibutuhkan tanpa mensyaratkan pewarisan dari class tertentu.
from typing import Protocol
class Writable(Protocol):
def write(self, text: str) -> None:
...
def save_message(target: Writable, message: str) -> None:
target.write(message)
Fungsi save_message tidak memerlukan class tertentu. Ia hanya membutuhkan objek yang menyediakan metode write dengan bentuk yang sesuai. Pendekatan ini dapat mengurangi keterikatan antar-modul, terutama ketika implementasi nyata dan implementasi untuk pengujian berbeda.
Kapan Anotasi Menjadi Berlebihan?
Tidak semua variabel perlu diberi anotasi. Tipe yang sudah jelas dari nilai awal biasanya tidak memerlukan penjelasan tambahan. Anotasi sebaiknya diprioritaskan pada batas modul, fungsi publik, struktur data kompleks, dan bagian yang sulit dipahami tanpa konteks.
Type hint juga bukan pengganti validasi runtime. Data dari jaringan, file, atau pengguna tetap perlu diperiksa ketika masuk ke sistem. Anotasi menjelaskan ekspektasi kode; validasi memastikan data nyata memenuhi ekspektasi tersebut.
Exception: Bedakan Kegagalan yang Dapat Ditangani
Exception sebaiknya ditangkap pada lapisan yang mengetahui cara menanganinya. Menangkap Exception secara luas di setiap fungsi dapat menyembunyikan bug yang seharusnya terlihat saat pengembangan.
try:
value = int(user_input)
except ValueError:
print("Input harus berupa bilangan bulat")
Contoh tersebut menangkap kesalahan konversi yang memang dapat diprediksi. Sebaliknya, exception akibat bug pemrograman tidak semestinya diubah menjadi pesan seolah-olah pengguna memasukkan data yang salah.
Exception juga dapat dibuat untuk menyatakan kegagalan domain yang spesifik:
class InsufficientBalanceError(Exception):
pass
def withdraw(balance: int, amount: int) -> int:
if amount > balance:
raise InsufficientBalanceError("Saldo tidak mencukupi")
return balance - amount
Exception khusus membuat pemanggil dapat mengambil keputusan berdasarkan jenis kegagalan, bukan dengan mencocokkan teks pesan. Pesan tetap berguna untuk manusia, tetapi sebaiknya tidak dijadikan dasar logika program.
Asyncio: Memahami Pekerjaan yang Menunggu
Pemrograman asynchronous sesuai untuk alur yang sering menunggu operasi seperti jaringan atau resource eksternal. Saat satu operasi menunggu, event loop dapat menjalankan coroutine lain yang siap berjalan. Ini berbeda dari menjalankan pekerjaan CPU berat secara otomatis di banyak inti.
import asyncio
async def fetch_value(name: str, delay: float) -> str:
await asyncio.sleep(delay)
return name
async def main():
results = await asyncio.gather(
fetch_value("A", 1),
fetch_value("B", 1),
)
print(results)
asyncio.run(main())
Contoh tersebut menggunakan asyncio.sleep sebagai ilustrasi operasi yang menunggu. Dalam aplikasi nyata, coroutine biasanya berinteraksi dengan library yang memang menyediakan API asynchronous. Memanggil fungsi blocking biasa di dalam coroutine dapat menahan event loop dan mengurangi manfaat desain asynchronous.
Asynchronous juga menambah kompleksitas: alur kontrol lebih sulit dilacak, resource perlu dikelola dengan benar, dan sebagian dependency mungkin tidak mendukung model yang sama. Gunakan pendekatan ini ketika karakter pekerjaan memang sesuai, bukan sebagai pengganti otomatis untuk kode synchronous.
Pengujian dan Desain yang Dapat Diamati
Kode yang mudah diuji biasanya memiliki fungsi kecil, dependensi yang dapat diganti, dan efek samping yang berada di batas sistem. Fungsi yang sekaligus membaca file, memproses data, dan mengirim permintaan jaringan akan lebih sulit diuji daripada tiga komponen yang memiliki tanggung jawab terpisah.
Contoh ilustratif, fungsi perhitungan harga dapat menerima data dan parameter secara langsung. Pengujian tidak perlu membuat file atau koneksi jaringan hanya untuk memeriksa aturan perhitungan.
def apply_discount(price: int, percentage: int) -> int:
if not 0 <= percentage <= 100:
raise ValueError("Persentase tidak valid")
return price - (price * percentage // 100)
Kasus pengujian sebaiknya mencakup input normal, batas nilai, serta input yang seharusnya ditolak. Detail framework pengujian dapat berbeda, tetapi prinsipnya tetap: setiap pengujian perlu memeriksa perilaku yang bermakna, bukan sekadar mengejar jumlah baris yang dieksekusi.
Checklist untuk Kode Python yang Mulai Membesar
Apakah setiap modul memiliki tanggung jawab yang dapat dijelaskan dalam satu atau dua kalimat?
Apakah fungsi publik memiliki kontrak input dan output yang jelas?
Apakah resource seperti file dan koneksi dikelola melalui context manager ketika sesuai?
Apakah generator digunakan karena pemrosesan bertahap memang dibutuhkan, bukan sekadar mengganti list?
Apakah exception ditangkap pada lapisan yang benar-benar dapat mengambil tindakan?
Apakah type hint dipakai untuk memperjelas batas kode tanpa menambahkan anotasi yang tidak membantu?
Apakah kode asynchronous berinteraksi dengan operasi yang dapat menunggu dan tidak memblokir event loop?
Apakah aturan bisnis dapat diuji tanpa menyiapkan seluruh sistem eksternal?
Untuk melanjutkan latihan, ambil satu program Python kecil yang pernah dibuat. Pisahkan input-output dari logika inti, tambahkan type hint pada fungsi utama, ubah pengelolaan file menjadi context manager, lalu tulis pengujian untuk kasus normal dan batas. Setelah itu, tinjau apakah decorator, generator, atau async benar-benar mengurangi masalah yang ada. Jika tidak, pertahankan solusi yang lebih sederhana.




