Python & AI Tutorials Logo
Pemrograman Python

38. Dekorator: Menambahkan Perilaku ke Fungsi

Dekorator (decorators) adalah salah satu fitur Python yang paling kuat untuk menulis kode yang bersih dan dapat dipakai ulang. Dekorator memungkinkan kamu memodifikasi atau menambahkan perilaku tambahan ke fungsi tanpa mengubah kode aslinya. Di bab ini, kita akan membangun pemahamanmu tentang fungsi sebagai objek kelas satu (first-class functions) dan closure dari Bab 23 untuk mengeksplorasi bagaimana dekorator bekerja dan bagaimana memakainya secara efektif.

38.1) Apa Itu Dekorator dan Kenapa Berguna

Sebuah dekorator (decorator) adalah fungsi yang menerima fungsi lain sebagai input dan mengembalikan versi yang sudah dimodifikasi dari fungsi tersebut. Ini mungkin karena, seperti yang kita pelajari di Bab 23, fungsi di Python adalah objek kelas satu (first-class objects)—mereka bisa dikirim sebagai argumen dan dikembalikan dari fungsi lain. Dekorator memungkinkan kamu “membungkus (wrap)” perilaku tambahan di sekitar fungsi yang sudah ada, sehingga mudah menambahkan fungsionalitas umum seperti logging, timing, validasi, atau kontrol akses tanpa mengotori logika inti.

Kenapa Dekorator Penting

Bayangkan kamu punya beberapa fungsi di programmu, dan kamu ingin mencatat (log) kapan masing-masing dipanggil. Tanpa dekorator, kamu mungkin menulis seperti ini:

python
# Tanpa dekorator - kode logging yang terduplikasi
def calculate_total(prices):
    print("Calling calculate_total")
    result = sum(prices)
    print(f"calculate_total returned: {result}")
    return result
 
def find_average(numbers):
    print("Calling find_average")
    result = sum(numbers) / len(numbers)
    print(f"find_average returned: {result}")
    return result
 
def process_order(order_id):
    print("Calling process_order")
    result = f"Order {order_id} processed"
    print(f"process_order returned: {result}")
    return result
 
# Menggunakan fungsi-fungsinya
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60

Pendekatan ini punya beberapa masalah:

  1. Duplikasi kode: Baris logging diulang di setiap fungsi
  2. Mencampur concern: Kode logging dicampur dengan logika bisnis
  3. Sulit dirawat: Jika kamu ingin mengubah format logging, kamu harus memperbarui setiap fungsi
  4. Mudah terlupa: Fungsi baru mungkin tidak menyertakan logging

Dekorator menyelesaikan masalah ini dengan memungkinkan kamu memisahkan perilaku logging dari fungsi inti kamu:

python
# Dengan dekorator - bersih dan mudah dirawat
# (Kita akan belajar cara membuat @log_calls di bab ini)
 
@log_calls
def calculate_total(prices):
    return sum(prices)
 
@log_calls
def find_average(numbers):
    return sum(numbers) / len(numbers)
 
@log_calls
def process_order(order_id):
    return f"Order {order_id} processed"
 
# Menggunakan fungsi-fungsinya menghasilkan output yang sama
calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60

Bedanya? Perilaku logging didefinisikan sekali di dekorator @log_calls dan dipakai ulang di mana-mana. Fungsi inti kamu tetap bersih dan fokus pada tujuan utamanya.

Kasus Penggunaan Umum untuk Dekorator

Dekorator sangat berguna untuk:

  • Logging: Mencatat kapan fungsi dipanggil dan apa yang dikembalikannya
  • Timing: Mengukur berapa lama fungsi dieksekusi
  • Validasi: Mengecek bahwa argumen fungsi memenuhi persyaratan tertentu
  • Caching: Menyimpan hasil pemanggilan fungsi yang mahal untuk dipakai ulang
  • Kontrol akses: Mengecek izin sebelum mengizinkan eksekusi fungsi
  • Retry logic: Mengulang otomatis operasi yang gagal
  • Pemeriksaan tipe: Memvalidasi tipe argumen dan tipe return

Keuntungan utamanya adalah kamu menulis dekorator sekali dan bisa menerapkannya ke banyak fungsi dengan satu baris kode.

38.2) Fungsi sebagai Objek: Fondasi Dekorator

Sebelum kita bisa memahami dekorator, kita perlu meninjau dan memperluas konsep bahwa fungsi adalah objek kelas satu (first-class objects) di Python. Seperti yang kita pelajari di Bab 23, ini berarti fungsi bisa ditetapkan ke variabel, dikirim sebagai argumen, dan dikembalikan dari fungsi lain.

Fungsi Bisa Di-assign ke Variabel

Saat kamu mendefinisikan sebuah fungsi, Python membuat objek fungsi dan mengikatnya ke sebuah nama:

python
def greet(name):
    return f"Hello, {name}!"
 
# Objek fungsi bisa ditugaskan ke variabel lain
say_hello = greet
 
# Kedua nama merujuk ke objek fungsi yang sama
print(greet("Alice"))      # Output: Hello, Alice!
print(say_hello("Bob"))    # Output: Hello, Bob!

Nama greet dan say_hello sama-sama merujuk ke objek fungsi yang sama. Ini fundamental untuk cara dekorator bekerja.

Fungsi Bisa Dikirim sebagai Argumen

Kamu bisa mengirim fungsi ke fungsi lain sama seperti nilai lainnya:

python
def apply_twice(func, value):
    """Terapkan sebuah fungsi ke sebuah nilai dua kali."""
    result = func(value)
    result = func(result)
    return result
 
def add_five(x):
    return x + 5
 
result = apply_twice(add_five, 10)
print(result)  # Output: 20 (10 + 5 = 15, lalu 15 + 5 = 20)

Di sini, apply_twice menerima fungsi add_five sebagai argumen dan memanggilnya dua kali.

Fungsi Bisa Mengembalikan Fungsi Lain

Sebuah fungsi bisa membuat dan mengembalikan fungsi baru:

python
def make_multiplier(factor):
    """Buat fungsi yang mengalikan dengan faktor tertentu."""
    def multiply(x):
        return x * factor
    return multiply
 
times_three = make_multiplier(3)
times_five = make_multiplier(5)
 
print(times_three(10))  # Output: 30
print(times_five(10))   # Output: 50

Fungsi make_multiplier mengembalikan fungsi baru yang “mengingat” nilai factor melalui closure (seperti yang kita pelajari di Bab 23).

Membungkus Fungsi: Pola Inti Dekorator

Pola dekorator menggabungkan konsep-konsep ini: sebuah fungsi yang menerima fungsi sebagai input, membuat fungsi wrapper yang menambahkan perilaku, dan mengembalikan wrapper:

python
def simple_wrapper(original_func):
    """Bungkus sebuah fungsi dengan perilaku tambahan."""
    def wrapper():
        print("Before calling the function")
        result = original_func()
        print("After calling the function")
        return result
    return wrapper
 
def say_hello():
    print("Hello!")
    return "greeting"
 
# Membungkus fungsi secara manual
wrapped_hello = simple_wrapper(say_hello)
return_value = wrapped_hello()
# Output:
# Before calling the function
# Hello!
# After calling the function
 
print(f"Returned: {return_value}")
# Output: Returned: greeting

Mari kita telusuri apa yang terjadi:

  1. simple_wrapper menerima say_hello sebagai original_func
  2. Ia membuat fungsi baru wrapper yang:
    • Mencetak "Before calling the function"
    • Memanggil original_func() (yang adalah say_hello)
    • Mencetak "After calling the function"
    • Mengembalikan hasilnya
  3. simple_wrapper mengembalikan fungsi wrapper
  4. Saat kita memanggil wrapped_hello(), kita sebenarnya memanggil wrapper, yang memanggil say_hello asli di dalamnya

Ini adalah pola inti di balik semua dekorator.

Menangani Fungsi dengan Argumen

Wrapper di atas hanya bekerja untuk fungsi yang tidak menerima argumen. Agar bisa bekerja untuk fungsi apa pun, kita butuh *args dan **kwargs:

python
def flexible_wrapper(original_func):
    """Bungkus sebuah fungsi yang bisa menerima argumen apa pun."""
    def wrapper(*args, **kwargs):
        # *args menangkap argumen posisi
        # **kwargs menangkap argumen keyword
        print("Before calling the function")
        result = original_func(*args, **kwargs)
        print("After calling the function")
        return result
    return wrapper
 
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"
 
# Membungkus fungsi secara manual
greet = flexible_wrapper(greet)
 
result = greet("Alice")
# Output:
# Before calling the function
# After calling the function
 
print(result)
# Output: Hello, Alice!
 
result = greet("Bob", greeting="Hi")
# Output:
# Before calling the function
# After calling the function
 
print(result)
# Output: Hi, Bob!

Cara kerja *args dan **kwargs:

Seperti yang kita pelajari di Bab 20, *args dan **kwargs memungkinkan fungsi menerima jumlah argumen yang bervariasi:

  • *args mengumpulkan semua argumen posisi ke dalam tuple
  • **kwargs mengumpulkan semua argumen keyword ke dalam dictionary
  • Saat kita memanggil original_func(*args, **kwargs), kita membongkarnya kembali sebagai argumen untuk fungsi asli

Pola ini membuat wrapper kita bisa bekerja dengan fungsi apa pun, terlepas dari berapa banyak argumen yang diterimanya.

Beralih ke Sintaks yang Lebih Bersih

Pola ini adalah fondasi dekorator. Sintaks dekorator yang akan kita pelajari berikutnya hanyalah cara yang lebih bersih untuk menerapkan pola ini. Alih-alih menulis:

python
greet = flexible_wrapper(greet)

Kita akan memakai sintaks @:

python
@flexible_wrapper
def greet(name, greeting="Hello"):
    return f"{greeting}, {name}!"

Keduanya melakukan hal yang persis sama—sintaks @ hanyalah syntactic sugar yang membuat kode lebih bersih dan lebih mudah dibaca.

38.3) Sintaks @decorator: Penerapan yang Lebih Rapi

Menulis function_name = decorator(function_name) memang bekerja, tapi verbose dan mudah terlupakan. Python menyediakan sintaks @decorator sebagai cara yang lebih bersih untuk menerapkan dekorator.

Menggunakan Simbol @

Alih-alih membungkus fungsi secara manual, kamu bisa menaruh @decorator_name pada baris tepat sebelum definisi fungsi:

python
def log_call(func):
    """Dekorator yang mencatat pemanggilan fungsi."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
@log_call
def calculate_total(prices):
    return sum(prices)
 
@log_call
def find_average(numbers):
    return sum(numbers) / len(numbers)
 
# Gunakan fungsi yang sudah didekorasi
total = calculate_total([10, 20, 30])
# Output:
# Calling calculate_total
# calculate_total returned: 60
 
print(f"Total: {total}")
# Output: Total: 60
 
average = find_average([10, 20, 30])
# Output:
# Calling find_average
# find_average returned: 20.0
 
print(f"Average: {average}")
# Output: Average: 20.0

Sintaks @log_call persis setara dengan menulis:

python
def calculate_total(prices):
    return sum(prices)
 
calculate_total = log_call(calculate_total)

Namun sintaks @ jauh lebih bersih dan membuatnya langsung jelas bahwa fungsi tersebut didekorasi.

Menumpuk Beberapa Dekorator

Kamu bisa menerapkan beberapa dekorator ke fungsi yang sama dengan menumpuknya:

python
import time
 
def log_call(func):
    """Dekorator yang mencatat pemanggilan fungsi."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
def timer(func):
    """Dekorator yang mengukur waktu eksekusi fungsi."""
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start_time
        print(f"{func.__name__} took {elapsed:.4f} seconds")
        return result
    return wrapper
 
@timer
@log_call
def process_data(items):
    total = sum(items)
    return total * 2
 
result = process_data([1, 2, 3, 4, 5])
# Output:
# Calling process_data
# process_data returned: 30
# process_data took 0.0001 seconds
 
print(f"Final result: {result}")
# Output: Final result: 30

Saat dekorator ditumpuk, dekorator diterapkan dari bawah ke atas (yang paling dekat dengan fungsi terlebih dulu):

python
@timer          # Diterapkan kedua (lapisan terluar)
@log_call       # Diterapkan pertama (paling dekat ke fungsi)
def process_data(items):
    pass

Ini setara dengan:

python
process_data = timer(log_call(process_data))

Urutan penerapan (bawah ke atas):

  1. @log_call membungkus fungsi asli terlebih dulu
  2. @timer membungkus hasilnya (membungkus fungsi yang sudah dibungkus)

Urutan eksekusi (atas ke bawah, terluar ke terdalam):

  1. wrapper timer mulai (terluar, dieksekusi pertama)
  2. wrapper log_call mulai (wrapper bagian dalam)
  3. Fungsi asli dieksekusi
  4. wrapper log_call selesai
  5. wrapper timer selesai (terluar, selesai terakhir)

Anggap dekorator seperti lapisan kertas kado—kamu menerapkannya dari dalam ke luar, tetapi saat membukanya (eksekusi), kamu bergerak dari luar ke dalam.

Penerapan Dekorator:

Fungsi Asli
process_data

Langkah 1: @log_call(dekorator bawah)

log_call membungkus fungsi asli

Langkah 2: @timer(dekorator atas)

timer membungkus wrapper log_call

Final: timer membungkus log_call membungkus fungsi asli

Alur Eksekusi:

Memanggil process_data

1. wrapper timer mulai
2. wrapper log_call mulai
3. Fungsi asli dieksekusi
4. wrapper log_call selesai
5. wrapper timer selesai

Mengembalikan hasil

38.4) Contoh Dekorator Praktis (Logging, Timing, Validasi)

Sekarang mari kita eksplorasi beberapa dekorator praktis yang mungkin kamu pakai di program nyata. Contoh-contoh ini menunjukkan pola yang umum dan memperlihatkan bagaimana dekorator menyelesaikan masalah dunia nyata.

Contoh 1: Dekorator Logging yang Ditingkatkan

Dekorator logging yang lebih canggih yang menyertakan timestamp dan menangani exception:

python
import time
 
def log_with_timestamp(func):
    """Dekorator yang mencatat pemanggilan fungsi dengan timestamp."""
    def wrapper(*args, **kwargs):
        timestamp = time.strftime("%Y-%m-%d %H:%M:%S")
        print(f"[{timestamp}] Calling {func.__name__}")
        
        try:
            result = func(*args, **kwargs)
            print(f"[{timestamp}] {func.__name__} completed successfully")
            return result
        except Exception as e:
            print(f"[{timestamp}] {func.__name__} raised {type(e).__name__}: {e}")
            raise
    
    return wrapper
 
@log_with_timestamp
def divide(a, b):
    return a / b
 
@log_with_timestamp
def process_user(user_id):
    # Simulasikan pemrosesan
    if user_id < 0:
        raise ValueError("User ID must be positive")
    return f"Processed user {user_id}"
 
# Uji eksekusi yang berhasil
result = divide(10, 2)
# Output:
# [2025-12-31 10:30:45] Calling divide
# [2025-12-31 10:30:45] divide completed successfully
 
print(f"Result: {result}")
# Output: Result: 5.0
 
# Uji eksekusi yang berhasil dengan validasi
user = process_user(42)
# Output:
# [2025-12-31 10:30:45] Calling process_user
# [2025-12-31 10:30:45] process_user completed successfully
 
print(user)
# Output: Processed user 42
 
# Uji penanganan exception
try:
    divide(10, 0)
    # Output:
    # [2025-12-31 10:30:45] Calling divide
    # [2025-12-31 10:30:45] divide raised ZeroDivisionError: division by zero
except ZeroDivisionError:
    print("Handled division by zero")
    # Output: Handled division by zero
 
try:
    process_user(-5)
    # Output:
    # [2025-12-31 10:30:45] Calling process_user
    # [2025-12-31 10:30:45] process_user raised ValueError: User ID must be positive
except ValueError:
    print("Handled invalid user ID")
    # Output: Handled invalid user ID

Dekorator ini:

  • Menambahkan timestamp ke semua pesan log
  • Mencatat baik penyelesaian yang sukses maupun exception
  • Me-raise ulang exception setelah mencatatnya (menggunakan raise tanpa argumen)
  • Menggunakan blok try/except untuk menangkap dan mencatat exception apa pun

Contoh 2: Dekorator Timing Kinerja

Dekorator yang mengukur dan melaporkan waktu eksekusi fungsi:

python
import time
 
def measure_time(func):
    """Dekorator yang mengukur dan melaporkan waktu eksekusi."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        
        # Format waktu secara tepat
        if elapsed < 0.001:
            time_str = f"{elapsed * 1000000:.2f} microseconds"
        elif elapsed < 1:
            time_str = f"{elapsed * 1000:.2f} milliseconds"
        else:
            time_str = f"{elapsed:.2f} seconds"
        
        print(f"{func.__name__} executed in {time_str}")
        return result
    
    return wrapper
 
@measure_time
def find_primes(limit):
    """Temukan semua bilangan prima hingga limit."""
    primes = []
    for num in range(2, limit):
        is_prime = True
        for divisor in range(2, int(num ** 0.5) + 1):
            if num % divisor == 0:
                is_prime = False
                break
        if is_prime:
            primes.append(num)
    return primes
 
@measure_time
def calculate_factorial(n):
    """Hitung faktorial dari n."""
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result
 
# Uji fungsi-fungsi yang sudah didekorasi
primes = find_primes(1000)
# Output: find_primes executed in 15.23 milliseconds
 
print(f"Found {len(primes)} primes")
# Output: Found 168 primes
 
factorial = calculate_factorial(100)
# Output: calculate_factorial executed in 45.67 microseconds
 
print(f"Factorial has {len(str(factorial))} digits")
# Output: Factorial has 158 digits

Dekorator ini secara otomatis memformat pengukuran waktu dengan tepat (microseconds, milliseconds, atau seconds) berdasarkan durasinya.

Contoh 3: Dekorator Validasi Input

Dekorator yang memvalidasi argumen fungsi sebelum eksekusi:

python
def validate_positive(func):
    """Dekorator yang memastikan semua argumen numerik bernilai positif."""
    def wrapper(*args, **kwargs):
        # Periksa argumen posisi
        for i, arg in enumerate(args):
            if isinstance(arg, (int, float)) and arg <= 0:
                raise ValueError(
                    f"Argument {i} to {func.__name__} must be positive, got {arg}"
                )
        
        # Periksa argumen keyword
        for key, value in kwargs.items():
            if isinstance(value, (int, float)) and value <= 0:
                raise ValueError(
                    f"Argument '{key}' to {func.__name__} must be positive, got {value}"
                )
        
        return func(*args, **kwargs)
    
    return wrapper
 
@validate_positive
def calculate_area(width, height):
    """Hitung luas sebuah persegi panjang."""
    return width * height
 
@validate_positive
def calculate_discount(price, discount_percent):
    """Hitung harga setelah diskon."""
    discount = price * (discount_percent / 100)
    return price - discount
 
# Uji input yang valid
area = calculate_area(10, 5)
print(f"Area: {area}")
# Output: Area: 50
 
discounted = calculate_discount(100, 20)
print(f"Discounted price: ${discounted:.2f}")
# Output: Discounted price: $80.00
 
# Uji input yang tidak valid
try:
    calculate_area(-5, 10)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: Argument 0 to calculate_area must be positive, got -5
 
try:
    calculate_discount(100, discount_percent=-10)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: Argument 'discount_percent' to calculate_discount must be positive, got -10

Dekorator ini:

  • Mengecek semua argumen numerik (baik posisi maupun keyword)
  • Melempar error yang deskriptif jika ada yang tidak positif
  • Memberikan pesan error yang jelas yang menunjukkan argumen mana yang gagal validasi

38.5) (Opsional) Dekorator dengan Argumen

Sejauh ini, semua dekorator kita adalah fungsi sederhana yang menerima fungsi sebagai input. Tapi bagaimana kalau kamu ingin mengonfigurasi perilaku dekorator? Misalnya, kamu mungkin ingin dekorator retry di mana kamu bisa menentukan jumlah percobaan, atau dekorator logging di mana kamu bisa menentukan level log.

Dekorator dengan argumen membutuhkan satu tingkat tambahan nesting fungsi. Alih-alih dekorator berupa fungsi yang menerima sebuah fungsi, ia menjadi fungsi yang menerima argumen dan mengembalikan dekorator.

Polanya: Factory Dekorator

Dekorator dengan argumen sebenarnya adalah factory dekorator (decorator factory) - sebuah fungsi yang membuat dan mengembalikan dekorator. Kunci untuk memahami ini adalah mengetahui apa yang Python lakukan dengan simbol @.

Prinsip Kunci: Python Mengevaluasi @ Terlebih Dulu

Python selalu mengevaluasi apa pun yang ada setelah @ terlebih dulu, lalu menggunakan hasilnya untuk mendekorasi fungsi kamu.

Mari kita bandingkan:

A) Dekorator Dasar:

Berdasarkan contoh ini:

python
def log_call(func):
    """Dekorator yang mencatat pemanggilan fungsi."""
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper
 
@log_call
def greet(name):
    return f"Hello, {name}!"

Yang Python lakukan:

  1. Evaluasi @log_call → Hasil: log_call itu sendiri (objek fungsi)
  2. Terapkan ke greet: greet = log_call(greet)

B) Factory Dekorator:

Berdasarkan contoh ini:

python
def repeat(times):
    """Level 1: Factory - menerima konfigurasi"""
    def decorator(func):
        """Level 2: Dekorator - menerima fungsi yang akan didekorasi"""
        def wrapper(*args, **kwargs):
            """Level 3: Wrapper - dieksekusi ketika fungsi terdekorasi dipanggil"""
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator
 
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")
 
greet("Alice")
# Output:
# Hello, Alice!
# Hello, Alice!
# Hello, Alice!

Yang Python lakukan:

  1. Evaluasi @repeat(3) → Hasil: repeat(3) dipanggil, mengembalikan fungsi dekorator
  2. Terapkan dekorator itu ke greet: greet = decorator(greet)

Perbedaannya: @log_call memberimu fungsi itu sendiri, tetapi @repeat(3) memanggil sebuah fungsi (repeat) yang mengembalikan dekorator.

Memahami Tiga Level

Sebuah factory dekorator punya tiga fungsi yang saling bersarang, masing-masing dengan peran spesifik:

python
def repeat(times):                      # Level 1: Factory
    def decorator(func):                # Level 2: Dekorator  
        def wrapper(*args, **kwargs):   # Level 3: Wrapper
            for i in range(times):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

Level 1 - Factory (repeat):

  • Menerima: Konfigurasi (times)
  • Mengembalikan: Fungsi dekorator
  • Dipanggil: Saat Python mengevaluasi @repeat(3)

Level 2 - Dekorator (decorator):

  • Menerima: Fungsi yang akan didekorasi (func)
  • Mengembalikan: Fungsi wrapper
  • Dipanggil: Tepat setelah Level 1, sebagai bagian dari sintaks @

Level 3 - Wrapper (wrapper):

  • Menerima: Argumen fungsi saat dipanggil (*args, **kwargs)
  • Mengembalikan: Hasil
  • Dipanggil: Setiap kali kamu memanggil fungsi terdekorasi

Eksekusi Langkah demi Langkah

Mari telusuri apa yang terjadi dengan @repeat(3):

python
# Yang kamu tulis:
@repeat(3)
def greet(name):
    print(f"Hello, {name}!")

Langkah 1: Python mengevaluasi repeat(3)

python
decorator = repeat(3)  # Factory mengembalikan dekorator (times=3 ditangkap)

Langkah 2: Python menerapkan dekorator ke greet

python
def greet(name):
    print(f"Hello, {name}!")
 
greet = decorator(greet)  # Dekorator mengembalikan wrapper (func=greet ditangkap)

Catatan: Pada titik ini, greet sekarang merujuk ke fungsi wrapper. greet asli ditangkap di func.

Langkah 3: Saat kamu memanggil greet("Alice"), wrapper dieksekusi

python
greet("Alice")  # Sebenarnya memanggil wrapper("Alice")
# wrapper menggunakan 'times' dan 'func' yang ditangkap

Kenapa Tiga Level?

Setiap level menangkap informasi yang berbeda melalui closure:

python
def repeat(times):                      # Menangkap: times
    def decorator(func):                # Menangkap: func (dan mengingat times)
        def wrapper(*args, **kwargs):   # Menangkap: times, func, dan menerima args
            for i in range(times):      # Menggunakan 'times' yang ditangkap
                result = func(*args, **kwargs)  # Menggunakan 'func' dan 'args' yang ditangkap
            return result
        return wrapper
    return decorator
  • Level 1 menangkap konfigurasi (times)
  • Level 2 menangkap fungsi yang akan didekorasi (func)
  • Level 3 menerima argumen saat dipanggil (args, kwargs)

Tanpa ketiga level, kita tidak bisa punya dekorator yang dapat dikonfigurasi dan mengingat baik pengaturan maupun fungsi yang sedang didekorasi.

Contoh 1: Dekorator Logging yang Bisa Dikonfigurasi

Berikut contoh praktis dekorator logging yang menerima konfigurasi:

python
def log_with_prefix(prefix="LOG"):
    """Factory dekorator yang membuat dekorator logging dengan prefix kustom."""
    def decorator(func):
        def wrapper(*args, **kwargs):
            print(f"[{prefix}] Calling {func.__name__}")
            result = func(*args, **kwargs)
            print(f"[{prefix}] {func.__name__} returned: {result}")
            return result
        return wrapper
    return decorator
 
@log_with_prefix(prefix="INFO")
def calculate_total(prices):
    return sum(prices)
 
@log_with_prefix()  # Gunakan prefix default
def get_average(numbers):
    return sum(numbers) / len(numbers)
 
# Uji fungsi-fungsi yang sudah didekorasi
total = calculate_total([10, 20, 30])
# Output:
# [INFO] Calling calculate_total
# [INFO] calculate_total returned: 60
 
print(f"Total: {total}")
# Output: Total: 60
 
average = get_average([10, 20, 30])
# Output:
# [LOG] Calling get_average
# [LOG] get_average returned: 20.0
 
print(f"Average: {average}")
# Output: Average: 20.0

Perhatikan bahwa:

  • @log_with_prefix(prefix="INFO") memakai prefix kustom
  • @log_with_prefix() memakai prefix default "LOG"
  • Kamu harus menyertakan tanda kurung meskipun memakai default

Contoh 2: Dekorator dengan Banyak Argumen

Berikut dekorator yang memvalidasi rentang numerik:

python
def validate_range(min_value=None, max_value=None):
    """
    Factory dekorator yang memvalidasi argumen numerik berada dalam suatu rentang.
    
    Args:
        min_value: Nilai minimum yang diizinkan (inklusif)
        max_value: Nilai maksimum yang diizinkan (inklusif)
    """
    def decorator(func):
        def wrapper(*args, **kwargs):
            # Periksa semua argumen numerik
            all_args = list(args) + list(kwargs.values())
            
            for arg in all_args:
                if isinstance(arg, (int, float)):
                    if min_value is not None and arg < min_value:
                        raise ValueError(
                            f"{func.__name__} received {arg}, "
                            f"which is below minimum {min_value}"
                        )
                    if max_value is not None and arg > max_value:
                        raise ValueError(
                            f"{func.__name__} received {arg}, "
                            f"which is above maximum {max_value}"
                        )
            
            return func(*args, **kwargs)
        return wrapper
    return decorator
 
@validate_range(min_value=0, max_value=100)
def calculate_percentage(value, total):
    """Hitung persentase."""
    return (value / total) * 100
 
@validate_range(min_value=0)
def calculate_age(birth_year, current_year):
    """Hitung usia dari tahun lahir."""
    return current_year - birth_year
 
# Uji input yang valid
percentage = calculate_percentage(25, 100)
print(f"Percentage: {percentage}%")
# Output: Percentage: 25.0%
 
age = calculate_age(1990, 2025)
print(f"Age: {age}")
# Output: Age: 35
 
# Uji input yang tidak valid
try:
    calculate_percentage(150, 100)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: calculate_percentage received 150, which is above maximum 100
 
try:
    calculate_age(-5, 2025)
except ValueError as e:
    print(f"Validation error: {e}")
    # Output: Validation error: calculate_age received -5, which is below minimum 0

Kapan Menggunakan Dekorator dengan Argumen

Gunakan dekorator dengan argumen ketika:

  • Kamu perlu mengonfigurasi perilaku dekorator
  • Dekorator yang sama harus bekerja berbeda di konteks yang berbeda
  • Kamu ingin membuat dekorator lebih bisa dipakai ulang dan fleksibel

Contoh umum meliputi:

  • Dekorator retry dengan jumlah percobaan dan jeda yang dapat dikonfigurasi
  • Dekorator logging dengan level log atau format yang dapat dikonfigurasi
  • Dekorator validasi dengan aturan yang dapat dikonfigurasi
  • Dekorator caching dengan ukuran cache atau waktu kedaluwarsa yang dapat dikonfigurasi
  • Rate limiting decorator dengan batas yang dapat dikonfigurasi

Catatan tentang Kompleksitas

Dekorator dengan argumen menambahkan satu tingkat kompleksitas ekstra. Saat menulisnya:

  • Gunakan nama parameter yang jelas dan deskriptif
  • Sediakan nilai default yang masuk akal
  • Sertakan docstring yang menjelaskan parameternya
  • Pertimbangkan apakah fleksibilitas tambahan sepadan dengan kompleksitasnya

Untuk kasus sederhana, dekorator tanpa argumen sering kali lebih jelas dan lebih mudah dipahami.


Dekorator adalah alat yang kuat untuk menulis kode Python yang bersih dan mudah dirawat. Dekorator memungkinkan kamu memisahkan concern lintas-fungsi (cross-cutting concerns) seperti logging, timing, dan validasi dari logika bisnis inti, sehingga kode kamu lebih mudah dibaca, diuji, dan dimodifikasi. Saat kamu terus memprogram dengan Python, kamu akan menemukan dekorator digunakan secara luas di framework dan library, dan kamu akan menemukan banyak kesempatan untuk menulis dekoratormu sendiri untuk menyelesaikan masalah umum dengan elegan.


© 2025. Primesoft Co., Ltd.
support@primesoft.ai