BAB 25: Closure
Pahami closure sebagai teknik mendefinisikan fungsi di dalam fungsi yang mampu mengingat dan mengakses variabel dari scope luar meskipun fungsi luar sudah selesai dijalankan.
Di bab sebelumnya, kita membangun fungsi yang sangat fleksibel dengan *args dan **kwargs — fungsi yang tidak peduli berapa banyak argumen yang dilempar kepadanya. Fleksibilitas itu bekerja di level antarmuka fungsi: berapa argumen yang diterima dan dalam format apa.
Sekarang kita masuk ke wilayah yang berbeda. Bagaimana kalau sebuah fungsi bisa membawa “memori” dari lingkungan tempat ia diciptakan — variabel, status, konteks — bahkan setelah fungsi yang menciptakannya sudah lama selesai? Teknik inilah yang disebut closure.
Fungsi di Dalam Fungsi
Python membolehkan kita mendefinisikan fungsi di dalam fungsi. Ini bukan fitur eksotis — kita sudah melihat pola ini saat membahas scope variabel di beberapa bab sebelumnya. Tapi ada sesuatu yang lebih menarik yang terjadi ketika fungsi dalam itu dikembalikan ke pemanggil.
Bayangkan sistem kuis kita perlu fitur cetak nilai dengan format yang berbeda-beda: ada yang mau format ringkas, ada yang mau format tabel lengkap. Daripada menulis dua fungsi terpisah dengan banyak logika duplikat, kita bisa membuat sebuah fungsi pabrik yang menghasilkan fungsi cetak sesuai format yang diminta.
# formatter.py
def buat_formatter(format_tipe: str):
label_header = f"[ LAPORAN {format_tipe.upper()} ]"
def cetak_nilai(nama: str, nilai: float):
if format_tipe == "ringkas":
print(f"{nama}: {nilai:.1f}")
else:
print(label_header)
print(f" Nama : {nama}")
print(f" Nilai : {nilai:.1f}")
print(f" Status: {'LULUS' if nilai >= 60 else 'TIDAK LULUS'}")
return cetak_nilai
Sekarang panggil buat_formatter dua kali dengan tipe berbeda:
cetak_ringkas = buat_formatter("ringkas")
cetak_lengkap = buat_formatter("lengkap")
cetak_ringkas("Hana", 88.5)
cetak_ringkas("Dika", 55.0)
print()
cetak_lengkap("Hana", 88.5)
Hana: 88.5
Dika: 55.0
[ LAPORAN LENGKAP ]
Nama : Hana
Nilai : 88.5
Status: LULUS
Yang terjadi di sini: cetak_ringkas dan cetak_lengkap adalah dua fungsi berbeda yang masing-masing “mengingat” nilai format_tipe dan label_header dari saat buat_formatter dipanggil. Inilah closure — fungsi cetak_nilai menutup (closing over) variabel dari scope luarnya.
Bagaimana Closure Bekerja
Ketika Python mengembalikan sebuah fungsi dalam, ia ikut membawa referensi ke variabel-variabel dari scope luarnya. Variabel-variabel itu tidak hilang meski fungsi luar sudah selesai — mereka “terkunci” di dalam fungsi dalam.
Setiap variabel yang “ditangkap” oleh closure bisa diperiksa lewat atribut __closure__:
# melihat isi closure
print(cetak_ringkas.__closure__)
for cell in cetak_ringkas.__closure__:
print(cell.cell_contents)
(<cell at 0x...>, <cell at 0x...>)
[ LAPORAN RINGKAS ]
ringkas
Keyword nonlocal
Sejauh ini closure hanya membaca variabel dari scope luar. Tapi bagaimana kalau fungsi dalam perlu mengubah nilai variabel tersebut?
Coba tambahkan penghitung ke sistem kuis: sebuah fungsi yang mencatat berapa kali ia sudah dipanggil.
# tracker.py
def buat_tracker_panggilan(nama_fungsi: str):
jumlah_panggilan = 0
def catat_dan_jalankan(nilai: float) -> str:
nonlocal jumlah_panggilan
jumlah_panggilan += 1
status = "LULUS" if nilai >= 60 else "TIDAK LULUS"
return f"[{nama_fungsi}] Panggilan ke-{jumlah_panggilan}: {nilai:.1f} -> {status}"
return catat_dan_jalankan
evaluasi = buat_tracker_panggilan("evaluasi_nilai")
print(evaluasi(88.5))
print(evaluasi(55.0))
print(evaluasi(72.3))
[evaluasi_nilai] Panggilan ke-1: 88.5 -> LULUS
[evaluasi_nilai] Panggilan ke-2: 55.0 -> TIDAK LULUS
[evaluasi_nilai] Panggilan ke-3: 72.3 -> LULUS
Tanpa nonlocal jumlah_panggilan, baris jumlah_panggilan += 1 akan membuat Python bingung: ia mengira kita sedang mendefinisikan variabel lokal baru bernama jumlah_panggilan, tapi di sisi kanan (+= 1) variabel itu belum punya nilai. Hasilnya: UnboundLocalError.
nonlocal dibutuhkan hanya ketika fungsi dalam ingin mengubah binding variabel dari scope luar — misalnya jumlah += 1 atau teks = "baru". Kalau cuma membaca atau memodifikasi isi objek mutable (list, dict), nonlocal tidak diperlukan.
Perbedaan ini penting:
def contoh():
daftar = [] # mutable — bisa dimodifikasi tanpa nonlocal
angka = 0 # immutable binding — butuh nonlocal untuk diubah
def dalam():
nonlocal angka
angka += 1 # mengubah binding -> nonlocal wajib
daftar.append(angka) # memodifikasi isi list -> nonlocal tidak perlu
dalam()
dalam()
return daftar, angka
print(contoh()) # ([1, 2], 2)
Closure sebagai Factory Function
Pola yang paling sering ditemui di dunia nyata adalah menggunakan closure sebagai factory — fungsi yang menghasilkan fungsi lain dengan konfigurasi tertentu. Ini lebih ringan daripada membuat class hanya untuk menyimpan satu atau dua atribut konfigurasi.
Sistem kuis kita butuh berbagai format validasi nilai: ada yang menggunakan skala 0–100, ada yang menggunakan 0–4.0 (GPA), ada yang hanya pass/fail. Dengan factory function:
# validator.py
def buat_validator(skala_maks: float, batas_lulus: float):
def validasi(nilai: float) -> dict:
if nilai < 0 or nilai > skala_maks:
return {"valid": False, "pesan": f"Nilai harus antara 0 dan {skala_maks}"}
lulus = nilai >= batas_lulus
persentase = (nilai / skala_maks) * 100
return {
"valid": True,
"lulus": lulus,
"persentase": round(persentase, 1),
"pesan": "LULUS" if lulus else "TIDAK LULUS",
}
return validasi
# buat tiga validator dengan skala berbeda
validasi_standar = buat_validator(skala_maks=100, batas_lulus=60)
validasi_gpa = buat_validator(skala_maks=4.0, batas_lulus=2.0)
validasi_ketat = buat_validator(skala_maks=100, batas_lulus=75)
print(validasi_standar(88))
print(validasi_gpa(3.2))
print(validasi_ketat(70))
{'valid': True, 'lulus': True, 'persentase': 88.0, 'pesan': 'LULUS'}
{'valid': True, 'lulus': True, 'persentase': 80.0, 'pesan': 'LULUS'}
{'valid': True, 'lulus': False, 'persentase': 70.0, 'pesan': 'TIDAK LULUS'}
Tiga fungsi validator berbeda, semuanya berasal dari satu template validasi. Setiap fungsi mengingat konfigurasi skala_maks dan batas_lulus yang diberikan saat factory dipanggil.
Menyimpan Fungsi dalam Variabel
Closure, seperti semua fungsi Python, adalah objek. Artinya mereka bisa disimpan dalam variabel, list, dictionary, bahkan dilempar sebagai argumen ke fungsi lain.
# kumpulan validator dalam dictionary
daftar_validator = {
"standar": buat_validator(100, 60),
"ketat": buat_validator(100, 75),
"gpa": buat_validator(4.0, 2.0),
}
nilai_peserta = [("Hana", 88), ("Dika", 55), ("Reza", 72)]
for nama, nilai in nilai_peserta:
hasil = daftar_validator["standar"](nilai)
print(f"{nama}: {hasil['pesan']}")
Hana: LULUS
Dika: TIDAK LULUS
Reza: LULUS
Atau lempar fungsi sebagai argumen — ini disebut higher-order function, dan closure sangat cocok untuk pola ini:
def proses_batch(daftar_nilai: list, fungsi_proses):
return [fungsi_proses(v) for v in daftar_nilai]
formatter_persen = buat_formatter("ringkas")
# gunakan closure standar
hasil = proses_batch([88, 55, 72], lambda v: f"{v:.1f}%")
for h in hasil:
print(h)
88.0%
55.0%
72.0%
Ketika kamu butuh fungsi dengan konfigurasi yang sedikit berbeda-beda tapi logikanya sama, factory function dengan closure hampir selalu lebih bersih daripada membuat class dengan __init__ dan __call__ — kecuali kalau state-nya kompleks dan butuh banyak method.
Latihan
Tiga latihan untuk mengeksplorasi closure lebih jauh:
-
Tulis factory function
buat_penghitung(mulai_dari: int)yang mengembalikan dua fungsi sekaligus:tambah()yang menaikkan counter danreset()yang mengembalikannya ke nilaimulai_dari. Kembalikan keduanya sebagai tuple. Uji dengan membuat dua instance counter yang independen. -
Modifikasi
buat_validatordi atas agar menyimpan riwayat validasi — setiap kalivalidasi()dipanggil, simpan hasil validasinya ke dalam list. Tambahkan fungsiriwayat()yang mengembalikan list tersebut. Kembalikan kedua fungsi sebagai dictionary{"validasi": ..., "riwayat": ...}. -
Tulis factory
buat_filter_nilai(operator: str, ambang: float)yang mengembalikan fungsifilter. Fungsifilterini menerima list nilai dan mengembalikan hanya nilai yang memenuhi kondisi (operatorbisa berupa">","<",">=","<="). Gunakan closure untuk menyimpanoperatordanambang.
Closure membuat Python terasa lebih ekspresif — kita bisa membawa perilaku dan konfigurasi bersama-sama dalam satu objek fungsi yang ringkas. Pola ini menjadi fondasi dari fitur Python yang lebih canggih lagi: decorator, yakni teknik untuk membungkus sebuah fungsi dengan perilaku tambahan tanpa mengubah kode aslinya. Itu yang akan kita dalami di bab selanjutnya.