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__:

python
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:

text
30
50
cell 3
5

Mari kita telaah apa yang terjadi pada baris-baris kode di atas:

  1. Baris 6 (tiga_kali = buat_pengali(3)): pemanggilan menghasilkan object function kalikan yang membawa cell berisi faktor = 3. Pemanggilan kedua menghasilkan object berbeda dengan cell faktor = 5 — dua closure independen.
  2. Baris 12 (sel = tiga_kali.__closure__[0]): attribute __closure__ berisi tuple of cell; indeks [0] mengambil cell pertama.
  3. Baris 13 (print(type(sel).__name__, ...)): tipe cell adalah cell, dan cell_contents memperlihatkan nilai yang ditangkap — 3.
  4. Baris 14: closure kedua menyimpan nilai 5 di 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:

python
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:

text
1 2 3
1

Setiap 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:

python
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:

text
Salam dari tombol 2
Salam dari tombol 2
Salam dari tombol 2

Ketiga 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:

python
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:

text
Salam dari tombol 0
Salam dari tombol 1
Salam dari tombol 2

Setiap 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:

python
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:

text
[login] kejadian ke-1
[login] kejadian ke-2
[login] kejadian ke-1
[login] kejadian ke-2

Keduanya berperilaku identik. Pilihan akhirnya soal selera dan arah pengembangan:

AspekClosureClass kecil
Tempat stateVariabel lokal yang ditangkap cellInstance attributes (self.hitungan)
Akses dari luarTertutup — tidak bisa diutak-atik pemanggilTerbuka — versi_class.hitungan bisa dibaca dan diubah
Menambah perilaku baruKurang cocok — harus mengubah factoryTambah method baru tanpa mengetuk yang lama
Paling pas untukSatu perilaku + sedikit state privatBeberapa 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.