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:

python
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:

text
unduh_laporan selesai dalam 0.20 detik
berkas diterima: laporan-penjualan.csv

Mari kita bedah bagian-bagian pentingnya:

  1. 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 *args dan **kwargs sudah kita pelajari pada Chapter Arbitrary Arguments.
  2. Baris 5-7 (mulai = time.perf_counter()): pembungkus mencatat waktu mulai, memanggil fungsi asli di baris 6, lalu menghitung selisihnya sebagai durasi.
  3. Baris 9 (return hasil): nilai kembalian fungsi asli diteruskan ke pemanggil. Tanpa baris ini, unduh_laporan() selalu tampak mengembalikan None.
  4. 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:

python
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:

text
panggilan ke-1 untuk validasi_email
True
panggilan ke-2 untuk validasi_email
True
panggilan ke-3 untuk validasi_email
False

Setiap 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.

Satu fungsi bisa dipasangi lebih dari satu decorator. Urutannya penting — decorator paling bawah dipasang lebih dulu:

python
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:

text
[atas] sebelum fungsi
[bawah] sebelum fungsi
Halo, Budi!
[bawah] sesudah fungsi
[atas] sesudah fungsi

Interpreter Python membaca tumpukan tersebut dari bawah ke atas:

  1. @hiasan_bawah dipasang lebih dulu: sapa menjadi pembungkus dari hiasan_bawah.
  2. @hiasan_atas membebani hasilnya: sapa menjadi pembungkus hiasan_atas yang membungkus pembungkus hiasan_bawah.
  3. Pemanggilan sapa() mengalir dari lapisan terluar (atas) ke fungsi asli, lalu kembali keluar — itulah mengapa [bawah] sesudah fungsi tercetak 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:

python
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:

text
membungkus siapkan_data sekarang
--- belum ada satu pun pemanggilan ---
memanggil siapkan_data
data siap

Baris 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.