Closure adalah fungsi yang tetap mengingat variabel dari lingkup luarnya meskipun fungsi luar tersebut sudah selesai dieksekusi. Mekanisme ini lahir dari kombinasi dua sifat Python, yaitu fungsi sebagai objek kelas satu dan aturan lingkup yang mencari nama ke luar secara bertingkat. Topik ini membahas penangkapan variabel pada fungsi bertingkat, pernyataan nonlocal untuk mengubahnya, pola fungsi pabrik sebagai penerapan paling umum, serta dua kesalahan yang sering muncul saat closure dipraktikkan.
Fungsi bertingkat dan penangkapan variabel#
Fungsi dapat didefinisikan di dalam fungsi lain. Fungsi bagian dalam boleh membaca variabel milik fungsi luarnya, dan apabila fungsi dalam itu dikembalikan sebagai nilai kembalian, variabel yang dirujuknya ikut terbawa. Kombinasi fungsi beserta variabel yang ditangkapnya itulah yang disebut closure. Contoh berikut membuat dua fungsi sapa dari pabrik yang sama, lalu memeriksa isi tangkapannya.
def buat_sapa(nama):
def sapa():
return "Halo, " + nama
return sapa
s1 = buat_sapa("Ali")
s2 = buat_sapa("Citra")
print(s1())
print(s2())
print(type(s1).__name__)
print(s1.__closure__[0].cell_contents)Halo, Ali
Halo, Citra
function
AliPemanggilan buat_sapa("Ali") selesai jauh sebelum s1() dijalankan, tetapi pesan "Halo, " tetap berhasil digabungkan dengan nama yang benar. Hal itu terjadi karena s1 menyimpan rujukan ke variabel nama, bukan menyalin hasilnya. Atribut __closure__ memperlihatkan mekanismenya, yaitu daftar cell yang setiap isinya dibaca melalui cell_contents. Setiap pemanggilan buat_sapa menghasilkan cell baru, sehingga s1 dan s2 membawa rujukan ke variabel yang berbeda meskipun kodenya sama.
Status independen dengan nonlocal#
Membaca variabel luar tidak memerlukan pernyataan apa pun, sedangkan mengubahnya perlu nonlocal. Tanpa pernyataan itu, penugasan di dalam fungsi dalam selalu membuat variabel lokal baru sesuai aturan LEGB, sehingga variabel luar tidak tersentuh. Dengan nonlocal, penugasan diarahkan ke variabel yang sama, dan pola ini menjadi cara membuat status privat tanpa kelas.
def buat_penghitung():
hitung = 0
def naik():
nonlocal hitung
hitung += 1
return hitung
return naik
c = buat_penghitung()
print(c(), c(), c())
d = buat_penghitung()
print(d())
print(d.__closure__[0].cell_contents)1 2 3
1
1Penghitung c menghitung sampai tiga karena setiap pemanggilan naik() menambah variabel hitung yang sama. Penghitung d dibuat dari pabrik yang sama tetapi berangkat dari nol lagi, karena pemanggilan buat_penghitung() kedua membentuk variabel dan cell yang berbeda. Status milik d pun terbaca 1 dari introspeksi closure di baris terakhir. Kedua penghitung tidak saling mengganggu, dan tidak ada cara lain untuk mengubah hitung selain melalui naik(), sehingga statusnya terlindungi secara alami.
Pola fungsi pabrik#
Fungsi yang mengembalikan fungsi seperti pada dua contoh di atas disebut fungsi pabrik (factory). Parameter pabrik berperan sebagai konfigurasi yang dibekukan pada saat pembuatan, sedangkan fungsi hasilnya tinggal dipanggil dengan data operasional. Pola ini cocok untuk fungsi kecil berperilaku seragam, misalnya pengali dengan faktor tetap, pemformat dengan temuan tetap, atau pembanding untuk argumen key pada sorted().
def buat_pengali(faktor):
def kali(angka):
return angka * faktor
return kali
dua = buat_pengali(2)
lima = buat_pengali(5)
print(dua(10), lima(10))
print(dua.__closure__[0].cell_contents, lima.__closure__[0].cell_contents)20 50
2 5Fungsi dua dan lima berbagi kode kali yang sama, tetapi faktor yang ditangkap masing-masing menjadikan perilakunya berbeda. Diagram berikut merangkum hubungan antara lingkup pabrik, cell yang ditangkap, dan objek fungsi hasilnya.
Demo interaktif berikut memperagakan pola yang sama pada bahasa JavaScript, yang juga mendukung closure. Jalankan demo berikut, tekan tombol Naik pada penghitung pertama beberapa kali, lalu amati bahwa penghitung kedua tetap berhenti pada angkanya sendiri. Setiap tombol Naik memanggil fungsi hasil pabrik yang berbeda dengan variabel internal yang berbeda pula.
Kesalahan umum#
Kesalahan pertama berasal dari waktu penangkapan. Closure menyimpan rujukan ke variabel, sehingga nilai yang terbaca adalah nilai terakhir saat fungsi dipanggil, bukan nilai saat fungsi didefinisikan. Di dalam perulangan, seluruh lambda yang dibuat berbagi variabel pengulang yang sama.
fungsi = []
for i in range(3):
fungsi.append(lambda: i)
print(fungsi[0](), fungsi[1](), fungsi[2]())
tertambat = []
for i in range(3):
tertambat.append(lambda i=i: i)
print(tertambat[0](), tertambat[1](), tertambat[2]())2 2 2
0 1 2Ketiga lambda pada baris pertama mengembalikan 2 karena perulangan telah selesai dan i berhenti pada nilai itu. Perbaikannya pada baris kedua mengikat nilai per iterasi melalui nilai default i=i, yang dievaluasi tepat saat lambda didefinisikan. Pola yang sama berlaku untuk callback yang dibuat di dalam perulangan apa pun.
Kesalahan kedua adalah menugaskan variabel luar tanpa nonlocal. Python kemudian menganggap variabel tersebut sebagai variabel lokal baru, sehingga error timbul karena variabel itu dibaca sebelum diisi.
def buat_penambah():
total = 0
def tambah(n):
total = total + n
return total
return tambah
t = buat_penambah()
try:
t(5)
except UnboundLocalError as e:
print(type(e).__name__, str(e))UnboundLocalError cannot access local variable 'total' where it is not associated with a valuePesan errornya menyebut total yang belum terhubung dengan nilai, padahal maksud penulisnya adalah variabel milik buat_penambah. Perbaikannya adalah menambahkan nonlocal total sebagai pernyataan pertama di dalam tambah, sehingga penugasan diarahkan ke variabel luar yang dimaksud.
Ringkasan#
- Closure adalah fungsi dalam yang menyimpan rujukan ke variabel lingkup luarnya, dan isi tangkapannya dapat diperiksa melalui
__closure__. - Pernyataan
nonlocalmengarahkan penugasan ke variabel luar, sedangkan tanpa pernyataan itu penugasan selalu membuat variabel lokal baru. - Setiap pemanggilan fungsi pabrik membentuk variabel dan cell yang berbeda, sehingga fungsi-fungsi hasilnya membawa status independen.
- Nilai yang dibaca closure adalah nilai terakhir variabelnya saat fungsi dipanggil, sehingga lambda di dalam perulangan perlu mengikat nilai melalui nilai default.
Referensi#
- Python Scopes and Namespaces — Python Software Foundation — Aturan lingkup bertingkat yang menjadi dasar penangkapan variabel.
- closure (Glosarium) — Python Software Foundation — Definisi resmi closure beserta sel yang menyimpan variabel bebasnya.
- Why do lambdas defined in a loop with different values all return the same result? — Python Software Foundation — Penjelasan resmi perangkap penangkapan terlambat dan solusi nilai defaultnya.