Decorator #

Di banyak aplikasi, ada perilaku yang perlu diterapkan ke banyak fungsi sekaligus: mencatat log setiap kali fungsi dipanggil, mengukur waktu eksekusi, memeriksa autentikasi pengguna sebelum menjalankan logika utama, atau meng-cache hasil komputasi. Tanpa decorator, kamu harus menyalin kode yang sama ke setiap fungsi — repetitif dan rawan inkonsistensi. Decorator memungkinkan kamu membungkus fungsi dengan perilaku tambahan tanpa menyentuh implementasi aslinya, menjaga kode tetap DRY dan terstruktur.

Fungsi sebagai First-Class Object #

Decorator bisa ada karena Python memperlakukan fungsi sebagai first-class object — fungsi bisa disimpan dalam variabel, dikirim sebagai argumen, dan dikembalikan dari fungsi lain. Ini fondasi yang perlu dipahami sebelum menulis decorator.

def sapa():
    print("Halo!")

# Fungsi bisa disimpan dalam variabel
aksi = sapa
aksi()  # Halo!

# Fungsi bisa dikirim sebagai argumen
def jalankan(fungsi):
    fungsi()

jalankan(sapa)  # Halo!

# Fungsi bisa dikembalikan dari fungsi lain
def buat_sapa(nama):
    def sapa_nama():
        print(f"Halo, {nama}!")
    return sapa_nama  # kembalikan fungsi, bukan hasil panggilannya

sapa_budi = buat_sapa("Budi")
sapa_budi()  # Halo, Budi!

Decorator pada dasarnya adalah fungsi yang menerima fungsi sebagai input dan mengembalikan fungsi baru sebagai output.


Decorator Dasar #

Bentuk paling sederhana dari decorator: sebuah fungsi yang membungkus fungsi lain dengan perilaku tambahan.

def log_panggilan(func):
    def wrapper(*args, **kwargs):
        print(f"[LOG] Memanggil '{func.__name__}'")
        hasil = func(*args, **kwargs)
        print(f"[LOG] '{func.__name__}' selesai")
        return hasil
    return wrapper

@log_panggilan
def hitung_total(a, b):
    return a + b

total = hitung_total(3, 7)
print(f"Total: {total}")
# [LOG] Memanggil 'hitung_total'
# [LOG] 'hitung_total' selesai
# Total: 10

Sintaks @log_panggilan di atas setara dengan menulis:

def hitung_total(a, b):
    return a + b

hitung_total = log_panggilan(hitung_total)  # ekuivalen dengan @log_panggilan

Masalah Identity Fungsi dan Solusinya: @wraps #

Ada jebakan umum saat membuat decorator: fungsi asli kehilangan identitasnya.

# ANTI-PATTERN: decorator tanpa @wraps merusak metadata fungsi
def log_panggilan(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log_panggilan
def hitung_total(a, b):
    """Menghitung penjumlahan dua angka."""
    return a + b

print(hitung_total.__name__)  # 'wrapper' — bukan 'hitung_total'!
print(hitung_total.__doc__)   # None — docstring hilang!

# BENAR: gunakan @wraps untuk menjaga metadata fungsi asli
from functools import wraps

def log_panggilan(func):
    @wraps(func)  # salin metadata dari func ke wrapper
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@log_panggilan
def hitung_total(a, b):
    """Menghitung penjumlahan dua angka."""
    return a + b

print(hitung_total.__name__)  # 'hitung_total' ✓
print(hitung_total.__doc__)   # 'Menghitung penjumlahan dua angka.' ✓
Selalu gunakan @functools.wraps(func) di dalam setiap decorator yang kamu buat. Tanpanya, tools seperti debugger, dokumentasi otomatis, dan help() akan menampilkan informasi yang salah — bisa sangat membingungkan saat debugging.

Decorator Praktis #

Berikut beberapa decorator yang sering dibutuhkan dalam aplikasi nyata:

Timer — Mengukur Waktu Eksekusi #

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        mulai = time.perf_counter()
        hasil = func(*args, **kwargs)
        durasi = time.perf_counter() - mulai
        print(f"[TIMER] '{func.__name__}' selesai dalam {durasi:.4f} detik")
        return hasil
    return wrapper

@timer
def proses_data(n):
    return sum(range(n))

proses_data(10_000_000)
# [TIMER] 'proses_data' selesai dalam 0.3142 detik

Retry — Ulangi Saat Gagal #

import time
from functools import wraps

def retry(maks_coba=3, jeda=1.0):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for percobaan in range(1, maks_coba + 1):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if percobaan == maks_coba:
                        raise
                    print(f"[RETRY] Percobaan {percobaan} gagal: {e}. Coba lagi dalam {jeda}s...")
                    time.sleep(jeda)
        return wrapper
    return decorator

@retry(maks_coba=3, jeda=0.5)
def ambil_data_api(url):
    import random
    if random.random() < 0.7:  # simulasi 70% kemungkinan gagal
        raise ConnectionError("Koneksi terputus")
    return f"Data dari {url}"

try:
    hasil = ambil_data_api("https://api.example.com/data")
    print(hasil)
except ConnectionError:
    print("Gagal setelah 3 percobaan.")

Decorator Berargumen #

Saat decorator perlu dikonfigurasi, kamu butuh satu lapisan fungsi tambahan — fungsi pembuat decorator.

from functools import wraps

def batasi_akses(peran_diizinkan):
    """Decorator yang membatasi akses berdasarkan peran pengguna."""
    def decorator(func):
        @wraps(func)
        def wrapper(pengguna, *args, **kwargs):
            if pengguna.get("peran") not in peran_diizinkan:
                raise PermissionError(
                    f"Peran '{pengguna.get('peran')}' tidak diizinkan mengakses '{func.__name__}'"
                )
            return func(pengguna, *args, **kwargs)
        return wrapper
    return decorator

@batasi_akses(peran_diizinkan=["admin", "manajer"])
def hapus_pengguna(pengguna, target_id):
    print(f"Pengguna {target_id} dihapus oleh {pengguna['nama']}.")

# Berhasil
hapus_pengguna({"nama": "Budi", "peran": "admin"}, target_id=42)

# Gagal
try:
    hapus_pengguna({"nama": "Sari", "peran": "user"}, target_id=42)
except PermissionError as e:
    print(e)

Struktur decorator berargumen selalu tiga lapis:

def decorator_berargumen(argumen):     ← lapisan 1: terima konfigurasi
    def decorator(func):               ← lapisan 2: terima fungsi
        @wraps(func)
        def wrapper(*args, **kwargs):  ← lapisan 3: jalankan logika
            # sebelum
            hasil = func(*args, **kwargs)
            # sesudah
            return hasil
        return wrapper
    return decorator

Stacking Decorator #

Beberapa decorator bisa ditumpuk pada satu fungsi. Urutan eksekusinya dari dalam ke luar — decorator paling dekat ke fungsi dieksekusi pertama.

from functools import wraps

def decorator_a(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("A: sebelum")
        hasil = func(*args, **kwargs)
        print("A: sesudah")
        return hasil
    return wrapper

def decorator_b(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("B: sebelum")
        hasil = func(*args, **kwargs)
        print("B: sesudah")
        return hasil
    return wrapper

@decorator_a
@decorator_b
def fungsi_ku():
    print("Fungsi utama")

fungsi_ku()
# B: sebelum  ← decorator_b lebih dekat ke fungsi, dieksekusi lebih dulu
# A: sebelum
# Fungsi utama
# A: sesudah
# B: sesudah
Urutan stacking @decorator_a @decorator_b def f():
    setara dengan: decorator_a(decorator_b(f))

Urutan eksekusi:
    decorator_a.wrapper mulai
        decorator_b.wrapper mulai
            f() dijalankan
        decorator_b.wrapper selesai
    decorator_a.wrapper selesai

Decorator untuk Kelas #

Decorator pada Metode #

from functools import wraps

def validasi_positif(func):
    """Pastikan semua argumen numerik bernilai positif."""
    @wraps(func)
    def wrapper(self, *args, **kwargs):
        for arg in args:
            if isinstance(arg, (int, float)) and arg <= 0:
                raise ValueError(f"Argumen harus positif, dapat: {arg}")
        return func(self, *args, **kwargs)
    return wrapper

class Kalkulator:
    @validasi_positif
    def akar(self, n):
        import math
        return math.sqrt(n)

    @validasi_positif
    def log(self, n):
        import math
        return math.log(n)

kalk = Kalkulator()
print(kalk.akar(16))   # 4.0
print(kalk.log(100))   # 4.605...

try:
    kalk.akar(-5)
except ValueError as e:
    print(e)  # Argumen harus positif, dapat: -5

Decorator Bawaan Python #

Python memiliki tiga decorator built-in yang sering digunakan dalam definisi kelas:

class Lingkaran:
    PI = 3.14159

    def __init__(self, jari_jari):
        self._jari_jari = jari_jari

    @property
    def jari_jari(self):
        """Getter — akses seperti atribut biasa."""
        return self._jari_jari

    @jari_jari.setter
    def jari_jari(self, nilai):
        """Setter — validasi sebelum menyimpan."""
        if nilai <= 0:
            raise ValueError("Jari-jari harus positif")
        self._jari_jari = nilai

    @property
    def luas(self):
        """Computed property — tidak perlu setter."""
        return self.PI * self._jari_jari ** 2

    @classmethod
    def dari_diameter(cls, diameter):
        """Factory method — buat instance dari parameter alternatif."""
        return cls(diameter / 2)

    @staticmethod
    def adalah_valid(nilai):
        """Utility method — tidak butuh akses ke instance atau kelas."""
        return nilai > 0

# Penggunaan
lingkaran = Lingkaran(5)
print(lingkaran.luas)       # 78.53975

lingkaran.jari_jari = 10   # memanggil setter
print(lingkaran.luas)       # 314.159

l2 = Lingkaran.dari_diameter(20)  # classmethod
print(l2.jari_jari)         # 10.0

print(Lingkaran.adalah_valid(-1))  # False

Ringkasan #

  • Decorator adalah fungsi yang membungkus fungsi lain — menerima fungsi sebagai argumen dan mengembalikan fungsi baru dengan perilaku tambahan.
  • Selalu gunakan @functools.wraps(func) — tanpanya, metadata fungsi asli (__name__, __doc__) akan hilang dan menyulitkan debugging.
  • Gunakan *args, **kwargs di wrapper — agar decorator kompatibel dengan fungsi yang memiliki argumen apapun.
  • Decorator berargumen butuh tiga lapisan fungsi — lapisan terluar menerima konfigurasi, lapisan tengah menerima fungsi, lapisan dalam menjalankan logika.
  • Stacking decorator dieksekusi dari dalam ke luar — decorator paling dekat ke fungsi dieksekusi pertama.
  • @property untuk computed attribute dengan validasi di setter; @classmethod untuk factory method; @staticmethod untuk utility method yang tidak butuh akses ke self atau cls.
  • Gunakan decorator untuk cross-cutting concerns — logging, timing, retry, autentikasi, caching — agar logika bisnis tetap bersih dari kode infrastruktur.

← Sebelumnya: Context Manager   Berikutnya: Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact