Saat menulis program, kita sering ingin menambahkan perilaku yang sama pada banyak fungsi: mencatat setiap pemanggilan, mengukur durasi, atau memvalidasi input. Jika perilaku tersebut ditulis langsung di dalam setiap fungsi, kode yang sama berulang di banyak tempat dan sulit dirawat. Decorator menyelesaikan masalah ini dengan cara membungkus fungsi asli di dalam fungsi lain, tanpa menyentuh isi fungsi aslinya.
Ada dua modal dari chapter sebelumnya yang perlu kita bawa ke sini: fungsi adalah objek yang bisa disimpan di variabel dan dikirim sebagai argument (Chapter First-Class Functions), serta fungsi dalam fungsi bisa mengakses variabel sekitarnya melalui closure (sudah kita cicipi pada Chapter Scope dan Namespace).
Membungkus Fungsi Secara Manual #
Sebelum ada tanda @ apa pun, kita bisa membangun decorator secara manual. Perhatikan fungsi tampilkan_garis() berikut — fungsi ini menerima satu fungsi lain, lalu mengembalikan fungsi pembungkus yang menambahkan garis di atas dan bawah keluaran:
def tampilkan_garis(func):
def pembungkus():
print("=" * 30)
func()
print("=" * 30)
return pembungkus
def harga_tiket():
print("Tiket konser: Rp750.000")
harga_tiket = tampilkan_garis(harga_tiket)
harga_tiket()Output yang Dihasilkan:
==============================
Tiket konser: Rp750.000
==============================Mari kita bedah proses pembungkusannya baris per baris:
- Baris 1 (
def tampilkan_garis(func):): mendefinisikan decorator — sebuah fungsi yang menerimafunc, yaitu objek fungsi yang akan dibungkus. - Baris 2-5 (
def pembungkus():): mendefinisikan fungsi baru di dalam decorator.pembungkus()memanggilfunc()di antara dua cetakan garis. Closure membuatfunctetap bisa diakses dari dalampembungkus()meskipunfuncbukan variabel lokalnya. - Baris 6 (
return pembungkus): decorator mengembalikan objekpembungkus— bukan hasil pemanggilannya, melainkan fungsi itu sendiri. - Baris 11 (
harga_tiket = tampilkan_garis(harga_tiket)): objek fungsiharga_tiketyang lama dikirim ke decorator, dan namaharga_tiketdiikat ulang ke fungsipembungkusyang dikembalikan. - Baris 12 (
harga_tiket()): pemanggilan sekarang menjalankanpembungkus(), yang di dalamnya memanggil fungsi asli.
Fungsi asli harga_tiket() tidak pernah diubah isinya. Yang berubah hanyalah objek yang dirujuk oleh nama harga_tiket.
Sintaks @ Hanyalah Pintasan #
Penulisan harga_tiket = tampilkan_garis(harga_tiket) bekerja, tetapi terasa ribet. Python menyediakan sintaks @ sebagai bentuk pendeknya. Kedua program berikut menghasilkan perilaku yang persis sama:
def tampilkan_garis(func):
def pembungkus():
print("=" * 30)
func()
print("=" * 30)
return pembungkus
@tampilkan_garis
def harga_tiket():
print("Tiket konser: Rp750.000")
harga_tiket()Output yang Dihasilkan:
==============================
Tiket konser: Rp750.000
==============================Baris @tampilkan_garis tepat di atas def harga_tiket memberi instruksi kepada interpreter Python: setelah fungsi harga_tiket selesai dibuat, segera jalankan harga_tiket = tampilkan_garis(harga_tiket). Tidak ada mekanisme tersembunyi lain di balik tanda @ — hanya pemanggilan fungsi dan pengikatan ulang nama.
Kita bisa membuktikan kesetaraan kedua bentuk tersebut secara langsung:
def tampilkan_garis(func):
def pembungkus():
print("=" * 30)
func()
print("=" * 30)
return pembungkus
def harga_tiket():
print("Tiket konser: Rp750.000")
# bentuk panjang: bungkus secara manual
harga_tiket_manual = tampilkan_garis(harga_tiket)
# bentuk pendek: @ menjalankan pembungkusan yang sama
@tampilkan_garis
def harga_tiket_sugar():
print("Tiket bioskop: Rp50.000")
print("hasil bentuk manual:")
harga_tiket_manual()
print("hasil bentuk @:")
harga_tiket_sugar()Output yang Dihasilkan:
hasil bentuk manual:
==============================
Tiket konser: Rp750.000
==============================
hasil bentuk @:
==============================
Tiket bioskop: Rp50.000
==============================Perhatikan bahwa harga_tiket_manual tetap bisa dibuat tanpa @ sama sekali — bukti bahwa @ adalah murni gula sintaksis (syntactic sugar).
Alur Pembungkusan #
Gambar berikut merangkum perjalanan objek dari def hingga pemanggilan:
Alur pada diagram di atas terbagi dua tahap: tahap pembungkusan yang terjadi saat def dieksekusi, dan tahap pemanggilan yang terjadi setiap kali kita memanggil nama fungsi tersebut.
Dengan bekal ini, kita siap menulis decorator yang benar-benar berguna — pengukur waktu, penghitung pemanggilan, dan decorator yang ditumpuk bertingkat — pada chapter berikutnya.