Ada pola aneh yang mungkin pernah kita jumpai: sebuah function tetap mengingat nilai dari function lain yang sudah selesai dieksekusi. Pada Chapter First-Class Functions, buat_pangkat(2) menghasilkan function hitung yang selalu tahu pangkatnya — padahal buat_pangkat sudah kembali. Mekanisme di baliknya bernama closure: inner function yang membawa serta variabel dari lingkungan pembuatnya. Memahami closure memperlengkap kita menulis function factory, dan yang lebih penting — menghindari jebakan klasiknya.
Closure: Function yang Membawa Data #
Ketika sebuah function kembali, namespace lokalnya secara normal ikut musnah. Tetapi apabila inner function masih mereferensikan variabel lokal pembungkusnya, interpreter Python menyimpan variabel tersebut dalam sebuah cell dan mengikat cell itu ke inner function. Kita bisa memeriksanya langsung lewat attribute __closure__:
def buat_pengali(faktor):
def kalikan(nilai):
return nilai * faktor
return kalikan
tiga_kali = buat_pengali(3)
lima_kali = buat_pengali(5)
print(tiga_kali(10))
print(lima_kali(10))
sel = tiga_kali.__closure__[0]
print(type(sel).__name__, sel.cell_contents)
print(lima_kali.__closure__[0].cell_contents)Output yang Dihasilkan:
30
50
cell 3
5Mari kita telaah apa yang terjadi pada baris-baris kode di atas:
- Baris 6 (
tiga_kali = buat_pengali(3)): pemanggilan menghasilkan object functionkalikanyang membawa cell berisifaktor = 3. Pemanggilan kedua menghasilkan object berbeda dengan cellfaktor = 5— dua closure independen. - Baris 12 (
sel = tiga_kali.__closure__[0]): attribute__closure__berisi tuple of cell; indeks[0]mengambil cell pertama. - Baris 13 (
print(type(sel).__name__, ...)): tipe cell adalahcell, dancell_contentsmemperlihatkan nilai yang ditangkap —3. - Baris 14: closure kedua menyimpan nilai
5di cell miliknya sendiri, membuktikan keduanya tidak berbagi state.
Counter Factory Tanpa global #
Kebutuhan klasik closure adalah state kecil yang tidak mau diekspos sebagai variabel global. Kombinasi closure dengan keyword nonlocal — yang sudah kita bahas pada Chapter local, global, dan nonlocal — menghasilkan counter yang aman:
def buat_counter():
hitungan = 0
def naik():
nonlocal hitungan
hitungan += 1
return hitungan
return naik
klik_banner = buat_counter()
klik_tombol = buat_counter()
print(klik_banner(), klik_banner(), klik_banner())
print(klik_tombol())Output yang Dihasilkan:
1 2 3
1Setiap pemanggilan buat_counter() menciptakan hitungan yang segar. Counter klik_banner naik sampai 3 tanpa mengganggu klik_tombol yang tetap mulai dari 1 — sesuatu yang tidak mungkin dilakukan variabel global tunggal. Nilai hitungan juga tidak bisa disentuh dari luar; satu-satunya pintu aksesnya adalah function naik.
Jebakan Late Binding di Loop #
Closure tidak mengambil nilai variabel saat dibuat — closure menyimpan referensi ke variabel, dan nilai dibaca saat pemanggilan terjadi. Perilaku ini disebut late binding, dan loop adalah tempat ia paling sering menggigit:
salaman_buggy = []
for nomor in range(3):
def sapa():
return f"Salam dari tombol {nomor}"
salaman_buggy.append(sapa)
for sapa in salaman_buggy:
print(sapa())Output yang Dihasilkan:
Salam dari tombol 2
Salam dari tombol 2
Salam dari tombol 2Ketiga function menghasilkan tombol 2, bukan tombol 0, 1, 2. Penyebabnya: setiap sapa menangkap variabel nomor yang sama. Saat ketiganya akhirnya dipanggil, loop sudah selesai dan nilai nomor tinggal 2 — nilai terakhir setelah range(3) habis.
Solusinya: buat setiap iterasi menghasilkan scope sendiri lewat function factory:
def buat_sapa(nomor):
def sapa():
return f"Salam dari tombol {nomor}"
return sapa
salaman = [buat_sapa(n) for n in range(3)]
for sapa in salaman:
print(sapa())Output yang Dihasilkan:
Salam dari tombol 0
Salam dari tombol 1
Salam dari tombol 2Setiap pemanggilan buat_sapa(n) menciptakan parameter nomor yang merupakan variabel berbeda, sehingga setiap closure membawa cell berisi nilainya sendiri.
Closure atau Class Kecil? #
Kebutuhan “state kecil + satu perilaku” bisa diselesaikan closure maupun class. Bandingkan dua pencatat kejadian berikut:
class PenghitungKejadian:
def __init__(self, judul):
self.judul = judul
self.hitungan = 0
def catat(self):
self.hitungan += 1
return f"[{self.judul}] kejadian ke-{self.hitungan}"
def buat_pencatat(judul):
hitungan = 0
def catat():
nonlocal hitungan
hitungan += 1
return f"[{judul}] kejadian ke-{hitungan}"
return catat
versi_class = PenghitungKejadian("login")
versi_closure = buat_pencatat("login")
print(versi_class.catat())
print(versi_class.catat())
print(versi_closure())
print(versi_closure())Output yang Dihasilkan:
[login] kejadian ke-1
[login] kejadian ke-2
[login] kejadian ke-1
[login] kejadian ke-2Keduanya berperilaku identik. Pilihan akhirnya soal selera dan arah pengembangan:
| Aspek | Closure | Class kecil |
|---|---|---|
| Tempat state | Variabel lokal yang ditangkap cell | Instance attributes (self.hitungan) |
| Akses dari luar | Tertutup — tidak bisa diutak-atik pemanggil | Terbuka — versi_class.hitungan bisa dibaca dan diubah |
| Menambah perilaku baru | Kurang cocok — harus mengubah factory | Tambah method baru tanpa mengetuk yang lama |
| Paling pas untuk | Satu perilaku + sedikit state privat | Beberapa perilaku + state yang berkembang |
Penutup satu babak: closure bukan sekadar kuriositas — closure adalah mesin di balik seluruh decorator yang akan kita bahas tuntas pada Chapter Memahami Decorators.