Chapter sebelumnya menunjukkan bahwa decorator adalah fungsi yang menerima fungsi dan mengembalikan fungsi pembungkus. Sekarang kita gunakan pola tersebut untuk masalah nyata: mengukur berapa lama sebuah fungsi berjalan, menghitung berapa kali sebuah fungsi dipanggil, dan memahami apa yang terjadi ketika beberapa decorator dipasang sekaligus. Ketiganya adalah pola yang muncul terus-menerus di library dan framework Python modern.
Decorator Pengukur Waktu #
Module time menyediakan fungsi time.perf_counter() — jam monotonik yang cocok untuk mengukur selisih waktu antara dua titik eksekusi. Kita bungkus pengukuran tersebut menjadi decorator @ukur_waktu:
import time
def ukur_waktu(func):
def pembungkus(*args, **kwargs):
mulai = time.perf_counter()
hasil = func(*args, **kwargs)
durasi = time.perf_counter() - mulai
print(f"{func.__name__} selesai dalam {durasi:.2f} detik")
return hasil
return pembungkus
@ukur_waktu
def unduh_laporan():
time.sleep(0.2)
return "laporan-penjualan.csv"
hasil = unduh_laporan()
print("berkas diterima:", hasil)Output yang Dihasilkan:
unduh_laporan selesai dalam 0.20 detik
berkas diterima: laporan-penjualan.csvMari kita bedah bagian-bagian pentingnya:
- Baris 4 (
def pembungkus(*args, **kwargs):): tanda bintang ganda menampung positional argument dan keyword argument apa pun. Pola ini membuat decorator kompatibel dengan fungsi bertanda tangan apa pun — teknik*argsdan**kwargssudah kita pelajari pada Chapter Arbitrary Arguments. - Baris 5-7 (
mulai = time.perf_counter()): pembungkus mencatat waktu mulai, memanggil fungsi asli di baris 6, lalu menghitung selisihnya sebagaidurasi. - Baris 9 (
return hasil): nilai kembalian fungsi asli diteruskan ke pemanggil. Tanpa baris ini,unduh_laporan()selalu tampak mengembalikanNone. - Baris 13-15 (
def unduh_laporan():): fungsi ini tidak tahu apa pun tentang pengukuran waktu — seluruh logika pengukuran hidup di decorator.
Decorator Penghitung Panggilan #
Pola kedua menyimpan jumlah pemanggilan di dalam closure. Dictionary dipakai sebagai wadah karena dictionary bisa diubah (mutable) sehingga isinya bertahan di antara pemanggilan:
def hitung_panggilan(func):
total = {"jumlah": 0}
def pembungkus(*args, **kwargs):
total["jumlah"] += 1
print(f"panggilan ke-{total['jumlah']} untuk {func.__name__}")
return func(*args, **kwargs)
return pembungkus
@hitung_panggilan
def validasi_email(email):
return "@" in email
print(validasi_email("[email protected]"))
print(validasi_email("[email protected]"))
print(validasi_email("budi.example.com"))Output yang Dihasilkan:
panggilan ke-1 untuk validasi_email
True
panggilan ke-2 untuk validasi_email
True
panggilan ke-3 untuk validasi_email
FalseSetiap kali validasi_email() dipanggil, pembungkus() menaikkan nilai total["jumlah"] sebelum meneruskan argument ke fungsi asli. Dictionary total dibuat satu kali saat decorator berjalan, sehingga hitungannya tidak pernah kembali ke nol selama program hidup.
Menumpuk Beberapa Decorator #
Satu fungsi bisa dipasangi lebih dari satu decorator. Urutannya penting — decorator paling bawah dipasang lebih dulu:
def hiasan_atas(func):
def pembungkus():
print("[atas] sebelum fungsi")
func()
print("[atas] sesudah fungsi")
return pembungkus
def hiasan_bawah(func):
def pembungkus():
print("[bawah] sebelum fungsi")
func()
print("[bawah] sesudah fungsi")
return pembungkus
@hiasan_atas
@hiasan_bawah
def sapa():
print("Halo, Budi!")
sapa()Output yang Dihasilkan:
[atas] sebelum fungsi
[bawah] sebelum fungsi
Halo, Budi!
[bawah] sesudah fungsi
[atas] sesudah fungsiInterpreter Python membaca tumpukan tersebut dari bawah ke atas:
@hiasan_bawahdipasang lebih dulu:sapamenjadi pembungkus darihiasan_bawah.@hiasan_atasmembebani hasilnya:sapamenjadi pembungkushiasan_atasyang membungkus pembungkushiasan_bawah.- Pemanggilan
sapa()mengalir dari lapisan terluar (atas) ke fungsi asli, lalu kembali keluar — itulah mengapa[bawah] sesudah fungsitercetak sebelum[atas] sesudah fungsi.
Decorator Berjalan Saat def, Bukan Saat Pemanggilan #
Fakta yang sering mengejutkan: badan decorator (bukan badan pembungkusnya) berjalan pada saat def dieksekusi. Kita buktikan dengan mencetak langsung di badan decorator:
def catat_pembungkusan(func):
print(f"membungkus {func.__name__} sekarang")
def pembungkus():
print(f"memanggil {func.__name__}")
func()
return pembungkus
@catat_pembungkusan
def siapkan_data():
print("data siap")
print("--- belum ada satu pun pemanggilan ---")
siapkan_data()Output yang Dihasilkan:
membungkus siapkan_data sekarang
--- belum ada satu pun pemanggilan ---
memanggil siapkan_data
data siapBaris membungkus siapkan_data sekarang tercetak sebelum garis pemisah, padahal siapkan_data() belum pernah dipanggil. Inilah bukti nyata bahwa dekorasi terjadi sekali — saat definisi fungsi dibaca interpreter — sedangkan pembungkus() berjalan pada setiap pemanggilan.
Dengan tiga pola ini, kita sudah bisa menulis decorator untuk kebutuhan sehari-hari. Bagian yang belum beres: decorator seperti @ukur_waktu tidak bisa menerima pengaturan sendiri, misalnya jumlah pengulangan atau teks prefix log. Chapter Decorators dengan Arguments menuntaskan kebutuhan tersebut.