1. Pengenalan Decorators
Decorator adalah fungsi yang menerima fungsi lain sebagai argumen dan mengembalikan fungsi baru yang biasanya diperluas atau dimodifikasi perilakunya. Dengan decorator, Anda bisa menambahkan fungsionalitas ekstra pada fungsi tanpa mengubah kode aslinya.
Konsep decorator sejajar dengan prinsip Open/Closed Principle dalam pemrograman — kode terbuka untuk diperluas, tetapi tertutup untuk dimodifikasi. Decorator menggunakan sintaks @decorator_name yang ditempatkan di atas fungsi.
Mengapa Decorator Penting?
| Fitur | Penjelasan |
|---|---|
| Code Reusability | Tulis logika sekali, gunakan di banyak fungsi |
| Separation of Concerns | Pisahkan logika bisnis dari logika tambahan (logging, caching, dll) |
| DRY Principle | Hindari duplikasi kode yang sama |
| Clean Code | Kode utama tetap bersih dan fokus pada logika inti |
| Framework Patterns | Digunakan luas di Flask, Django, FastAPI, pytest, dll |
┌─────────────────────────────────────────────────────┐
│ CARA KERJA DECORATOR │
│ │
│ @my_decorator ┌───────────────────┐ │
│ def say_hello(): │ my_decorator │ │
│ print("Hello") │ ┌─────────────┐ │ │
│ │ │ say_hello() │ │ │
│ # Sama dengan: │ │ (original) │ │ │
│ # say_hello = │ └──────┬──────┘ │ │
│ # my_decorator( │ │ │ │
│ # say_hello │ ┌──────▼──────┐ │ │
│ # ) │ │ wrapper() │──┼──► │
│ │ │ (enhanced) │ │ output│
│ │ └─────────────┘ │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────┘
2. Fungsi sebagai First-Class Citizen
Sebelum memahami decorator, kita perlu memahami bahwa di Python, fungsi adalah first-class citizen — fungsi bisa disimpan dalam variabel, diteruskan sebagai argumen, dan dikembalikan dari fungsi lain.
# Fungsi bisa disimpan dalam variabel
def sapa():
return "Halo, dunia!"
fungsi_saya = sapa # TANPA tanda kurung — menyimpan referensi
print(fungsi_saya()) # Halo, dunia!
# Fungsi bisa diteruskan sebagai argumen
def jalankan(fungsi):
print("Menjalankan fungsi...")
hasil = fungsi()
print(f"Hasil: {hasil}")
jalankan(sapa)
# Menjalankan fungsi...
# Hasil: Halo, dunia!
# Fungsi bisa mengembalikan fungsi lain
def buat_pengali(n):
def pengali(x):
return x * n
return pengali
kali_dua = buat_pengali(2)
kali_tiga = buat_pengali(3)
print(kali_dua(5)) # 10
print(kali_tiga(5)) # 15
print(kali_dua(10)) # 20
# Fungsi bisa disimpan dalam data structure
def tambah(a, b): return a + b
def kurang(a, b): return a - b
def kali(a, b): return a * b
operasi = {
"+": tambah,
"-": kurang,
"*": kali
}
print(operasi["+"](10, 5)) # 15
print(operasi["*"](10, 5)) # 50
# Inner function (fungsi bersarang)
def luar():
pesan = "Ini dari fungsi luar"
def dalam():
print(pesan) # Bisa akses variabel dari fungsi luar
return dalam()
luar() # Ini dari fungsi luar
Closures
# Closure: inner function yang mengingat variabel dari enclosing scope
def buat_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
hitung = buat_counter()
print(hitung()) # 1
print(hitung()) # 2
print(hitung()) # 3
# Closure untuk membuat decorator nanti
def logger(func):
def wrapper(*args, **kwargs):
print(f"Memanggil {func.__name__}...")
hasil = func(*args, **kwargs)
print(f"Selesai.")
return hasil
return wrapper
def tambah(a, b):
return a + b
tambah_dengan_log = logger(tambah)
print(tambah_dengan_log(3, 5))
# Memanggil tambah...
# Selesai.
# 8
3. Function Decorator
Function decorator adalah fungsi yang membungkus fungsi lain untuk menambah atau memodifikasi perilakunya. Ini adalah jenis decorator yang paling umum digunakan.
Membuat Decorator Pertama
# === Membuat decorator sederhana ===
def dekorator_sederhana(func):
def wrapper():
print("=== Sebelum fungsi dipanggil ===")
func()
print("=== Setelah fungsi dipanggil ===")
return wrapper
# Menggunakan decorator dengan sintaks @
@dekorator_sederhana
def sapa():
print("Halo, dunia!")
sapa()
# Output:
# === Sebelum fungsi dipanggil ===
# Halo, dunia!
# === Setelah fungsi dipanggil ===
# Tanpa sintaks @, sama dengan:
# def sapa():
# print("Halo, dunia!")
# sapa = dekorator_sederhana(sapa)
Decorator dengan *args dan **kwargs
# Decorator yang bisa membungkus fungsi dengan argumen apapun
def universal_decorator(func):
def wrapper(*args, **kwargs):
print(f"Memanggil {func.__name__} dengan args={args}, kwargs={kwargs}")
hasil = func(*args, **kwargs)
print(f"{func.__name__} mengembalikan: {hasil}")
return hasil
return wrapper
@universal_decorator
def tambah(a, b):
return a + b
@universal_decorator
def sapa(nama, pesan="Halo"):
return f"{pesan}, {nama}!"
print(tambah(3, 5))
# Memanggil tambah dengan args=(3, 5), kwargs={}
# tambah mengembalikan: 8
# 8
print(sapa("Budi", pesan="Selamat pagi"))
# Memanggil sapa dengan args=('Budi',), kwargs={'pesan': 'Selamat pagi'}
# sapa mengembalikan: Selamat pagi, Budi!
# Selamat pagi, Budi!
Preserving Metadata dengan @functools.wraps
import functools
# TANPA functools.wraps — metadata hilang!
def decorator_tanpa_wraps(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator_tanpa_wraps
def fungsi_contoh():
"""Ini docstring fungsi contoh."""
pass
print(fungsi_contoh.__name__) # wrapper ← salah!
print(fungsi_contoh.__doc__) # None ← hilang!
# DENGAN functools.wraps — metadata terjaga!
def decorator_dengan_wraps(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@decorator_dengan_wraps
def fungsi_contoh2():
"""Ini docstring fungsi contoh."""
pass
print(fungsi_contoh2.__name__) # fungsi_contoh2 ← benar!
print(fungsi_contoh2.__doc__) # Ini docstring fungsi contoh. ← terjaga!
# SELALU gunakan @functools.wraps(func) di decorator Anda!
Selalu gunakan @functools.wraps(func) di dalam decorator Anda. Ini mempertahankan metadata asli fungsi seperti __name__, __doc__, dan __module__ yang penting untuk debugging dan introspeksi.
4. Built-in Decorators
Python menyediakan beberapa decorator bawaan yang sangat berguna untuk penggunaan sehari-hari.
@staticmethod, @classmethod, @property
# === @staticmethod ===
# Metode yang tidak membutuhkan akses ke instance atau class
class Kalkulator:
@staticmethod
def tambah(a, b):
return a + b
@staticmethod
def kali(a, b):
return a * b
# Bisa dipanggil tanpa membuat instance
print(Kalkulator.tambah(5, 3)) # 8
print(Kalkulator.kali(4, 6)) # 24
# === @classmethod ===
# Metode yang menerima class sebagai argumen pertama (cls)
class Mahasiswa:
jumlah = 0
def __init__(self, nama, nim):
self.nama = nama
self.nim = nim
Mahasiswa.jumlah += 1
@classmethod
def dari_string(cls, data_str):
"""Factory method: buat Mahasiswa dari string"""
nama, nim = data_str.split(",")
return cls(nama.strip(), nim.strip())
@classmethod
def get_jumlah(cls):
return cls.jumlah
# Membuat instance dengan factory method
mhs1 = Mahasiswa("Budi", "2024001")
mhs2 = Mahasiswa.dari_string("Ani, 2024002")
print(Mahasiswa.get_jumlah()) # 2
# === @property ===
# Mengubah method menjadi attribute yang bisa diakses seperti variabel
class Lingkaran:
def __init__(self, jari_jari):
self._jari_jari = jari_jari
@property
def jari_jari(self):
"""Getter untuk jari_jari"""
return self._jari_jari
@jari_jari.setter
def jari_jari(self, nilai):
"""Setter dengan validasi"""
if nilai <= 0:
raise ValueError("Jari-jari harus positif!")
self._jari_jari = nilai
@property
def luas(self):
"""Hitung luas — computed property"""
import math
return math.pi * self._jari_jari ** 2
@property
def keliling(self):
"""Hitung keliling — computed property"""
import math
return 2 * math.pi * self._jari_jari
lingkaran = Lingkaran(5)
print(f"Jari-jari: {lingkaran.jari_jari}") # 5
print(f"Luas: {lingkaran.luas:.2f}") # 78.54
print(f"Keliling: {lingkaran.keliling:.2f}") # 31.42
lingkaran.jari_jari = 10 # Menggunakan setter
print(f"Luas baru: {lingkaran.luas:.2f}") # 314.16
@abstractmethod dan Decorator Lainnya
# === @abstractmethod ===
from abc import ABC, abstractmethod
class Hewan(ABC):
@abstractmethod
def suara(self):
"""Setiap hewan harus punya suara"""
pass
@abstractmethod
def bergerak(self):
pass
class Kucing(Hewan):
def suara(self):
return "Meow!"
def bergerak(self):
return "Kucing berjalan dengan 4 kaki"
class Burung(Hewan):
def suara(self):
return "Cuit!"
def bergerak(self):
return "Burung terbang di udara"
kucing = Kucing()
print(kucing.suara()) # Meow!
print(kucing.bergerak()) # Kucing berjalan dengan 4 kaki
# hewan = Hewan() ← Error! Tidak bisa instantiate abstract class
# === @functools.lru_cache ===
import functools
@functools.lru_cache(maxsize=128)
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(50)) # 12586269025 — sangat cepat berkat caching!
print(fibonacci.cache_info())
# CacheInfo(hits=48, misses=51, maxsize=128, currsize=51)
# === @dataclasses.dataclass ===
from dataclasses import dataclass
@dataclass
class Titik:
x: float
y: float
def jarak_ke(self, lain):
return ((self.x - lain.x)**2 + (self.y - lain.y)**2) ** 0.5
t1 = Titik(3, 4)
t2 = Titik(0, 0)
print(t1) # Titik(x=3, y=4)
print(t1.jarak_ke(t2)) # 5.0
5. Custom Decorators
Timer Decorator
import functools
import time
def timer(func):
"""Decorator untuk mengukur waktu eksekusi fungsi"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
mulai = time.perf_counter()
hasil = func(*args, **kwargs)
selesai = time.perf_counter()
print(f"⏱️ {func.__name__} selesai dalam {selesai - mulai:.4f} detik")
return hasil
return wrapper
@timer
def proses_data(n):
"""Fungsi simulasi pemrosesan data"""
total = sum(i ** 2 for i in range(n))
return total
hasil = proses_data(1000000)
print(f"Hasil: {hasil}")
# ⏱️ proses_data selesai dalam 0.0823 detik
# Hasil: 333332833333500000
Logger Decorator
import functools
from datetime import datetime
def logger(func):
"""Decorator untuk logging pemanggilan fungsi"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
waktu = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{waktu}] ▶️ Memanggil {func.__name__}()")
print(f" args: {args}")
print(f" kwargs: {kwargs}")
try:
hasil = func(*args, **kwargs)
print(f"[{waktu}] ✅ {func.__name__}() → {hasil}")
return hasil
except Exception as e:
print(f"[{waktu}] ❌ {func.__name__}() → Error: {e}")
raise
return wrapper
@logger
def bagi(a, b):
return a / b
bagi(10, 3)
# [2026-06-26 10:30:00] ▶️ Memanggil bagi()
# args: (10, 3)
# kwargs: {}
# [2026-06-26 10:30:00] ✅ bagi() → 3.3333333333333335
# bagi(10, 0) ← Akan log error juga!
Retry Decorator
import functools
import time
def retry(max_attempts=3, delay=1):
"""Decorator untuk mencoba ulang fungsi jika gagal"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"⚠️ Percobaan {attempt}/{max_attempts} gagal: {e}")
if attempt < max_attempts:
print(f" Menunggu {delay} detik...")
time.sleep(delay)
raise Exception(f"Gagal setelah {max_attempts} percobaan")
return wrapper
return decorator
# Contoh penggunaan
import random
@retry(max_attempts=3, delay=0.5)
def ambil_data_api():
"""Simulasi API call yang kadang gagal"""
if random.random() < 0.7: # 70% kemungkinan gagal
raise ConnectionError("Server tidak merespons")
return {"status": "ok", "data": [1, 2, 3]}
try:
hasil = ambil_data_api()
print(f"Data: {hasil}")
except Exception as e:
print(f"Error final: {e}")
6. Parameterized Decorators
Parameterized decorator adalah decorator yang bisa menerima argumen. Bentuknya adalah decorator factory — fungsi yang mengembalikan decorator.
import functools
# Parameterized decorator = decorator factory
# 3 tingkat nested function!
def repeat(n):
"""Menjalankan fungsi sebanyak n kali"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
hasil = None
for _ in range(n):
hasil = func(*args, **kwargs)
return hasil
return wrapper
return decorator
@repeat(3)
def sapa(nama):
print(f"Halo, {nama}!")
sapa("Budi")
# Halo, Budi!
# Halo, Budi!
# Halo, Budi!
# === Access Control Decorator ===
def require_role(role):
"""Membatasi akses berdasarkan role"""
def decorator(func):
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if user.get("role") != role:
print(f"❌ Akses ditolak! Butuh role: {role}")
return None
return func(user, *args, **kwargs)
return wrapper
return decorator
@require_role("admin")
def hapus_user(user, target):
print(f"✅ {user['nama']} menghapus user: {target}")
return True
@require_role("admin")
def lihat_laporan(user):
print(f"✅ {user['nama']} melihat laporan")
return {"total": 100}
admin = {"nama": "Budi", "role": "admin"}
viewer = {"nama": "Ani", "role": "viewer"}
hapus_user(admin, "Dimas") # ✅ Budi menghapus user: Dimas
hapus_user(viewer, "Dimas") # ❌ Akses ditolak! Butuh role: admin
lihat_laporan(admin) # ✅ Budi melihat laporan
Rate Limiter Decorator
import functools
import time
from collections import defaultdict
def rate_limit(max_calls, period):
"""Membatasi jumlah pemanggilan fungsi dalam periode waktu"""
def decorator(func):
calls = []
@functools.wraps(func)
def wrapper(*args, **kwargs):
sekarang = time.time()
# Hapus panggilan lama
calls[:] = [t for t in calls if sekarang - t < period]
if len(calls) >= max_calls:
tunggu = period - (sekarang - calls[0])
print(f"⚠️ Rate limit! Tunggu {tunggu:.1f} detik")
return None
calls.append(sekarang)
return func(*args, **kwargs)
return wrapper
return decorator
@rate_limit(max_calls=3, period=5)
def kirim_pesan(pesan):
print(f"📨 Mengirim: {pesan}")
return True
# 3 panggilan pertama berhasil
kirim_pesan("Pesan 1") # 📨 Mengirim: Pesan 1
kirim_pesan("Pesan 2") # 📨 Mengirim: Pesan 2
kirim_pesan("Pesan 3") # 📨 Mengirim: Pesan 3
kirim_pesan("Pesan 4") # ⚠️ Rate limit! Tunggu X.X detik
# === Max Length Decorator ===
def max_length(limit):
"""Membatasi panjang input string"""
def decorator(func):
@functools.wraps(func)
def wrapper(text, *args, **kwargs):
if len(text) > limit:
text = text[:limit] + "..."
return func(text, *args, **kwargs)
return wrapper
return decorator
@max_length(20)
def tampilkan_judul(judul):
print(f"📰 {judul}")
tampilkan_judul("Judul yang sangat panjang sekali dan melebihi batas")
# 📰 Judul yang sangat panja...
7. Class Decorator
Class bisa berperan sebagai decorator. Ini berguna ketika Anda membutuhkan state (status) dalam decorator, yang sulit dilakukan dengan fungsi closure biasa.
import functools
import time
# Class sebagai decorator (menggunakan __call__)
class Timer:
"""Class-based decorator untuk mengukur waktu"""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.total_time = 0
self.call_count = 0
def __call__(self, *args, **kwargs):
mulai = time.perf_counter()
hasil = self.func(*args, **kwargs)
durasi = time.perf_counter() - mulai
self.total_time += durasi
self.call_count += 1
print(f"⏱️ {self.func.__name__}: {durasi:.4f}s (call #{self.call_count})")
return hasil
def statistik(self):
rata = self.total_time / self.call_count if self.call_count else 0
print(f"📊 {self.func.__name__}: {self.call_count} calls, "
f"total={self.total_time:.4f}s, avg={rata:.4f}s")
@Timer
def proses_a(n):
return sum(range(n))
@Timer
def proses_b(n):
return [i**2 for i in range(n)]
proses_a(1000000) # ⏱️ proses_a: 0.0312s (call #1)
proses_a(500000) # ⏱️ proses_a: 0.0156s (call #2)
proses_b(1000000) # ⏱️ proses_b: 0.0891s (call #1)
proses_a.statistik() # 📊 proses_a: 2 calls, total=0.0468s, avg=0.0234s
# === Call Counter ===
class CallCounter:
"""Menghitung berapa kali fungsi dipanggil"""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
self.history = []
def __call__(self, *args, **kwargs):
self.count += 1
self.history.append(args)
return self.func(*args, **kwargs)
def reset(self):
self.count = 0
self.history = []
@CallCounter
def tambah(a, b):
return a + b
print(tambah(1, 2)) # 3
print(tambah(3, 4)) # 7
print(tambah(5, 6)) # 11
print(f"Dipanggil {tambah.count} kali") # Dipanggil 3 kali
print(f"History: {tambah.history}") # [(1,2), (3,4), (5,6)]
# === Memoization Class ===
class Memoize:
"""Caching hasil fungsi"""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.cache = {}
def __call__(self, *args):
if args not in self.cache:
self.cache[args] = self.func(*args)
print(f"💾 Cache MISS: {args} → {self.cache[args]}")
else:
print(f"⚡ Cache HIT: {args} → {self.cache[args]}")
return self.cache[args]
@Memoize
def fibonacci(n):
if n < 2:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10)) # 55 — dengan caching!
8. Decorator Chaining
Anda bisa menerapkan beberapa decorator pada satu fungsi sekaligus. Decorator diterapkan dari bawah ke atas (bottom-up).
import functools
def bold(func):
"""Membungkus output dengan tag bold"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
"""Membungkus output dengan tag italic"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
def uppercase(func):
"""Mengubah output menjadi huruf besar"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
# Chaining: diterapkan dari bawah ke atas
@bold
@italic
@uppercase
def sapa(nama):
return f"halo, {nama}"
print(sapa("budi"))
# Eksekusi: sapa → uppercase → italic → bold
# <b><i>HALO, BUDI</i></b>
# Sama dengan:
# sapa = bold(italic(uppercase(sapa)))
# Contoh chaining yang lebih praktis
def validate_positive(func):
@functools.wraps(func)
def wrapper(x, *args, **kwargs):
if x < 0:
raise ValueError(f"Input harus positif, dapat: {x}")
return func(x, *args, **kwargs)
return wrapper
def log_call(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"📞 Memanggil {func.__name__}({args})")
return func(*args, **kwargs)
return wrapper
@log_call
@validate_positive
def akar_kuadrat(x):
return x ** 0.5
print(akar_kuadrat(16)) # 4.0
# 📞 Memanggil akar_kuadrat((16,))
# 4.0
# akar_kuadrat(-5) ← ValueError: Input harus positif
9. Studi Kasus Praktis
Sistem Autentikasi Sederhana
import functools
# === Decorator Collection ===
def login_required(func):
"""Memastikan user sudah login"""
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("is_authenticated"):
print("❌ Silakan login terlebih dahulu!")
return None
return func(user, *args, **kwargs)
return wrapper
def admin_only(func):
"""Memastikan user adalah admin"""
@functools.wraps(func)
def wrapper(user, *args, **kwargs):
if user.get("role") != "admin":
print("❌ Hanya admin yang bisa mengakses!")
return None
return func(user, *args, **kwargs)
return wrapper
def cache_result(func):
"""Cache hasil fungsi berdasarkan argumen"""
cache = {}
@functools.wraps(func)
def wrapper(*args, **kwargs):
key = str(args) + str(kwargs)
if key not in cache:
cache[key] = func(*args, **kwargs)
print(f"💾 Cache disimpan untuk key: {key[:30]}...")
else:
print(f"⚡ Menggunakan cache untuk key: {key[:30]}...")
return cache[key]
return wrapper
# === Menggunakan decorator ===
@login_required
def lihat_profil(user):
return f"Profil: {user['nama']} ({user['email']})"
@login_required
@admin_only
def hapus_artikel(user, artikel_id):
print(f"🗑️ Artikel #{artikel_id} dihapus oleh {user['nama']}")
return True
@cache_result
def ambil_data_berat(query):
"""Simulasi query database yang lambat"""
import time
time.sleep(1) # Simulasi delay
return {"query": query, "results": [1, 2, 3]}
# Test
user_guest = {"nama": "Guest"}
user_admin = {
"nama": "Budi",
"email": "budi@email.com",
"role": "admin",
"is_authenticated": True
}
lihat_profil(user_guest) # ❌ Silakan login terlebih dahulu!
lihat_profil(user_admin) # Profil: Budi (budi@email.com)
hapus_artikel(user_admin, 42) # 🗑️ Artikel #42 dihapus oleh Budi
ambil_data_berat("Python") # 💾 Cache disimpan...
ambil_data_berat("Python") # ⚡ Menggunakan cache...
10. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Decorators: