Decorator @ukur_waktu pada chapter sebelumnya bekerja untuk semua fungsi, tetapi perilakunya seragam — pengaturannya terkunci di dalam kode. Bagaimana jika kita ingin decorator yang bisa diatur pemakainya: ulang pemanggilan sebanyak n kali, atau tampilkan log dengan prefix "INFO" atau "DEBUG"? Untuk itu, decorator perlu menerima argument sendiri, dan strukturnya bertambah satu lapisan closure.

Tiga Lapis Closure #

Decorator biasa punya dua lapis: badan decorator yang menerima func, dan badan pembungkus() yang berjalan pada tiap pemanggilan. Ketika decorator perlu argument, kita membutuhkan tiga lapis:

text
lapis 1: factory menerima pengaturan  ->  mengembalikan decorator
lapis 2: decorator menerima func       ->  mengembalikan pembungkus
lapis 3: pembungkus berjalan di setiap pemanggilan

Perbedaan mendasarnya: pada @ukur_waktu, interpreter Python mengirim objek fungsi langsung ke decorator ukur_waktu. Namun pada @ulangi(3), yang dikirim lebih dulu adalah angka 3. Ekspresi ulangi(3) berjalan seketika sebagai panggilan fungsi biasa, dan hasilnya — si decorator sejati — barulah menerima fungsi yang didekorasi.

Decorator repeat(n) #

Kita terjemahkan struktur tiga lapis tersebut menjadi decorator @ulangi(n):

python
def ulangi(n):
    def dekorator(func):
        def pembungkus(*args, **kwargs):
            for _ in range(n):
                hasil = func(*args, **kwargs)
            return hasil
        return pembungkus
    return dekorator

@ulangi(3)
def kirim_pesan(nama):
    print(f"pesan terkirim ke {nama}")
    return "semua terkirim"

print(kirim_pesan("Budi"))

Output yang Dihasilkan:

text
pesan terkirim ke Budi
pesan terkirim ke Budi
pesan terkirim ke Budi
semua terkirim

Mari kita bedah ketiga lapisnya:

  1. Baris 1 (def ulangi(n):): lapis factory. Saat interpreter membaca @ulangi(3), pemanggilan ulangi(3) berjalan seketika dan mengembalikan dekorator dengan nilai n = 3 tersimpan di closure.
  2. Baris 2 (def dekorator(func):): lapis decorator. Objek fungsi kirim_pesan dikirim ke lapis ini, sama seperti decorator biasa.
  3. Baris 3-7 (def pembungkus(*args, **kwargs):): lapis pembungkus. Loop for _ in range(n) memanggil fungsi asli sebanyak tiga kali; nilai kembalian pemanggilan terakhir diteruskan ke pemanggil.
  4. Baris 15 (print(kirim_pesan("Budi"))): satu pemanggilan dari luar memicu tiga pemanggilan fungsi asli di dalam.

Prefix Logger #

Contoh kedua memanfaatkan argument berupa string. Decorator @log_dengan_prefix(prefix) mencatat setiap pemanggilan dengan label yang bisa diatur:

python
def log_dengan_prefix(prefix):
    def dekorator(func):
        def pembungkus(*args, **kwargs):
            print(f"[{prefix}] memanggil {func.__name__}{args}")
            hasil = func(*args, **kwargs)
            print(f"[{prefix}] {func.__name__} selesai")
            return hasil
        return pembungkus
    return dekorator

@log_dengan_prefix("INFO")
def tambah_pesanan(a, b):
    return a + b

@log_dengan_prefix("DEBUG")
def cek_stok(nama_barang):
    return f"stok {nama_barang}: 12"

print(tambah_pesanan(3, 4))
print(cek_stok("kopi"))

Output yang Dihasilkan:

text
[INFO] memanggil tambah_pesanan(3, 4)
[INFO] tambah_pesanan selesai
7
[DEBUG] memanggil cek_stok('kopi',)
[DEBUG] cek_stok selesai
stok kopi: 12

Dua fungsi memakai decorator yang sama dengan pengaturan berbeda: tambah_pesanan() dicatat dengan prefix INFO, sedangkan cek_stok() dengan prefix DEBUG. Masing-masing prefix tersimpan terpisah di closure masing-masing pembungkus.

Perhatikan detail pada keluaran ('kopi',): {args} mencetak argument positional sebagai tuple, dan tuple berisi satu elemen selalu ditampilkan dengan koma di belakang. Koma tersebut bagian dari notasi tuple, bukan bagian dari data.

Bayangan Keterbacaan #

Struktur tiga lapis memang kuat, tetapi setiap lapis menambah beban membaca:

LapisMenerimaDijalankan saatNama di contoh
1pengaturan (n, prefix)@ulangi(3) dieksekusiulangi, log_dengan_prefix
2objek fungsisetelah lapis 1 selesaidekorator
3argument pemanggilansetiap kali fungsi dipanggilpembungkus

Ada satu masalah halus yang belum kita tangani: semua contoh di chapter ini mengganti nama fungsi dengan pembungkus, sehingga __name__ dan docstring fungsi asli menghilang. Chapter functools.wraps menunjukkan cara memulihkannya.