Decorator

Decorator untuk membungkus fungsi, functools.wraps, dan decorator dengan argumen.

Daftar isi

Decorator adalah callable yang menerima sebuah fungsi dan mengembalikan fungsi pengganti, sehingga perilaku tambahan dapat diselipkan tanpa mengubah badan fungsi aslinya. Penulisan dengan tanda @ membuat pola ini ringkas dan seragam, dan pola ini dipakai luas pada pustaka standar maupun framework untuk pencatatan, pengukuran, pendaftaran, dan caching. Topik ini membahas pembungkusan manual, sintaks @, pemulihan metadata dengan functools.wraps, decorator yang menerima argumen, urutan penumpukan, beserta dua kesalahan yang lazim.

Membungkus fungsi dengan decorator#

Decorator tidak lebih dari fungsi biasa yang polanya berupa tiga langkah, yaitu menerima fungsi, mendefinisikan fungsi pembungkus (wrapper) yang memanggil fungsi aslinya, lalu mengembalikan pembungkus itu. Tanda @nama di atas sebuah def hanya gula sintaks untuk penugasan f = nama(f). Contoh berikut mendefinisikan decorator pencatat pemanggilan, lalu memasangnya pada dua fungsi.

python Contoh: decorator pencatat pemanggilan
def cetak_pemanggilan(fungsi):
    def pembungkus(*args, **kwargs):
        print("->", fungsi.__name__)
        hasil = fungsi(*args, **kwargs)
        print("<-", fungsi.__name__)
        return hasil
    return pembungkus

@cetak_pemanggilan
def jumlah(a, b):
    return a + b

@cetak_pemanggilan
def sapa(nama):
    return "Hai " + nama

print(jumlah(2, 3))
print(sapa("Dewi"))
text Keluaran
-> jumlah
<- jumlah
5
-> sapa
<- sapa
Hai Dewi

Nama jumlah setelah dekorasi tidak lagi menunjuk fungsi aslinya, melainkan objek pembungkus yang meneruskan seluruh argumen melalui *args dan **kwargs sebelum mengembalikan hasilnya. Karena pembungkus bersifat umum, satu decorator dapat dipasang pada fungsi dengan tanda tangan apa pun. Kesetaraan dengan penugasan biasa juga dapat ditunjukkan tanpa tanda @ sama sekali. Fungsi apa pun yang menerima fungsi dan mengembalikan fungsi dapat dipakai sebagai decorator melalui penugasan manual.

python Contoh: dekorasi melalui penugasan manual
def cetak_pemanggilan(fungsi):
    def pembungkus(*args, **kwargs):
        print("->", fungsi.__name__)
        hasil = fungsi(*args, **kwargs)
        print("<-", fungsi.__name__)
        return hasil
    return pembungkus

def ganda(n):
    return n * 2

ganda = cetak_pemanggilan(ganda)
print(ganda(4))
print(type(ganda).__name__)
text Keluaran
-> ganda
<- ganda
8
function

Keluarannya sama dengan versi ber-@, sedangkan cetakan function menegaskan bahwa nama ganda kini menunjuk objek fungsi baru, yaitu pembungkusnya. Tanda @ dengan demikian tidak menambah kemampuan apa pun, melainkan memangkas satu baris penugasan dan menaruh decorator pada tempat yang paling dekat dengan definisinya. Diagram berikut menggambarkan perjalanan pemanggilan dari pemanggil sampai kembali ke pemanggil.

pemanggiljumlah(2, 3)pembungkusmencatat lalu meneruskanfungsi aslijumlah(a, b)argumenterusanhasil dikembalikan lewat pembungkus@cetak_pemanggilan mengganti nama jumlah dengan pembungkusnya
Pemanggilan fungsi terdekorasi melewati pembungkus sebelum mencapai fungsi asli

Mempertahankan metadata dengan functools.wraps#

Pembungkus adalah fungsi berbeda dari fungsi aslinya, sehingga metadata seperti __name__ dan __doc__ ikut tertukar. Kondisi ini menyulitkan alat introspeksi, mulai dari dokumentasi otomatis sampai debugger, karena semuanya membaca nama pembungkus.

python Contoh: nama fungsi tertukar tanpa wraps
def tanpa_wraps(fungsi):
    def pembungkus(*args, **kwargs):
        return fungsi(*args, **kwargs)
    return pembungkus

@tanpa_wraps
def bagi(a, b):
    """Membagi dua angka."""
    return a / b

print(bagi.__name__)
text Keluaran
pembungkus

Modul functools menyediakan decorator wraps untuk menyalin metadata fungsi asli ke pembungkusnya. Decorator ini dipasang pada definisi pembungkus dengan fungsi asli sebagai argumennya, dan hanya itu perbedaannya dari versi tanpa wraps.

python Contoh: metadata pulih dengan @wraps
from functools import wraps

def cetak_pemanggilan(fungsi):
    @wraps(fungsi)
    def pembungkus(*args, **kwargs):
        return fungsi(*args, **kwargs)
    return pembungkus

@cetak_pemanggilan
def bagi(a, b):
    """Membagi dua angka."""
    return a / b

print(bagi.__name__)
print(bagi.__doc__)
print(bagi(10, 2))
text Keluaran
bagi
Membagi dua angka.
5.0

Nama dan docstring bagi kembali seperti aslinya, padahal objek yang dipanggil tetap pembungkus. Selain __name__ dan __doc__, wraps juga memulihkan atribut lain seperti __module__ dan __wrapped__, sehingga fungsi terdekorasi tetap dapat diperiksa sebagaimana fungsi biasa.

Decorator dengan argumen#

Decorator yang menerima argumen sendiri membutuhkan satu lapis fungsi lagi, karena argumennya harus ditangkap lebih dahulu sebelum fungsi yang didekorasi diterima. Bentuknya berupa tiga tingkat, yaitu pabrik decorator, decorator, dan pembungkus.

python Contoh: decorator pengulang dengan argumen
from functools import wraps

def ulang(jumlah):
    def dekorator(fungsi):
        @wraps(fungsi)
        def pembungkus(*args, **kwargs):
            hasil = None
            for _ in range(jumlah):
                hasil = fungsi(*args, **kwargs)
            return hasil
        return pembungkus
    return dekorator

@ulang(3)
def sapa(nama):
    print("Halo", nama)
    return nama

sapa("Budi")
text Keluaran
Halo Budi
Halo Budi
Halo Budi

Ekspresi @ulang(3) dievaluasi lebih dahulu, sehingga Python memperoleh decorator sesungguhnya dari pemanggilan ulang(3) dan baru kemudian menerapkannya pada sapa. Nilai 3 tersimpan di lingkup ulang dan tetap terjangkau oleh pembungkus melalui aturan closure, sehingga tiga tingkat fungsi tersebut saling terhubung tanpa variabel global.

Urutan penumpukan decorator#

Beberapa decorator dapat dipasang pada satu fungsi, dan penerapannya bergerak dari bawah ke atas. Decorator yang paling dekat dengan def diterapkan lebih dahulu, sedangkan decorator di atasnya membungkus hasilnya.

python Contoh: dua decorator bertumpuk
from functools import wraps

def tandai(awalan):
    def dekorator(fungsi):
        @wraps(fungsi)
        def pembungkus(*args, **kwargs):
            return f"[{awalan}] " + str(fungsi(*args, **kwargs))
        return pembungkus
    return dekorator

@tandai("atas")
@tandai("bawah")
def data():
    return "isi"

print(data())
text Keluaran
[atas] [bawah] isi

Keluaran [atas] [bawah] isi memperlihatkan urutan itu secara langsung, karena tandai("bawah") menyentuh data lebih dahulu, lalu tandai("atas") membungkus hasilnya. Pada saat pemanggilan, kontrol mengalir dari pembungkus terluar ke dalam, sehingga urutan penulisan dan urutan eksekusi saling berkebalikan. Pemahaman ini penting ketika decorator menyimpan state atau mengubah urutan argumen.

Kesalahan umum#

Kesalahan pertama adalah lupa mengembalikan hasil fungsi asli dari pembungkus. Pembungkus yang hanya memanggil fungsi tanpa return mengembalikan None, dan pemanggil menerima None tanpa pesan error apa pun.

python Contoh: pembungkus tanpa return
def catat(fungsi):
    def pembungkus(*args, **kwargs):
        fungsi(*args, **kwargs)
    return pembungkus

@catat
def jumlah(a, b):
    return a + b

print(jumlah(2, 3))
text Keluaran
None

Perbaikannya adalah menugaskan hasil pemanggilan ke variabel lalu mengembalikannya, sebagaimana pola hasil = fungsi(*args, **kwargs) yang dipakai sepanjang topik ini.

Kesalahan kedua adalah mendefinisikan pembungkus tanpa parameter variadik seperti *args, **kwargs, sehingga fungsi yang memiliki argumen gagal dipanggil melalui pembungkusnya.

python Contoh: pembungkus tanpa penerusan argumen
def catat(fungsi):
    def pembungkus():
        print("memanggil", fungsi.__name__)
        return fungsi()
    return pembungkus

@catat
def jumlah(a, b):
    return a + b

try:
    jumlah(2, 3)
except TypeError as e:
    print(type(e).__name__, str(e))
text Keluaran
TypeError catat.<locals>.pembungkus() takes 0 positional arguments but 2 were given

Pesan errornya menunjuk pembungkus, bukan jumlah, karena pemanggilan benar-benar masuk ke pembungkus lebih dahulu. Penambahan *args, **kwargs pada definisi dan pemanggilan pembungkus menjadikan decorator ini aman dipasang pada fungsi apa pun.

Ringkasan#

  • Decorator menerima fungsi dan mengembalikan penggantinya, dengan tanda @ sebagai singkatan penugasan ulang nama fungsi.
  • Pembungkus meneruskan argumen melalui *args dan **kwargs, mengembalikan hasil fungsi asli, dan sebaiknya dilengkapi @functools.wraps agar metadata fungsi asli pulih.
  • Decorator dengan argumen berbentuk tiga tingkat fungsi, dan dekorasi berlaku sekali pada saat def dieksekusi.
  • Penumpukan decorator diterapkan dari bawah ke atas, sedangkan alur eksekusinya bergerak dari pembungkus terluar ke dalam.

Referensi#