Closure

Closure sebagai fungsi yang menangkap variabel sekitarnya dan pola fungsi pabrik.

Daftar isi

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.

python Contoh: fungsi yang menangkap variabel luarnya
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)
text Keluaran
Halo, Ali
Halo, Citra
function
Ali

Pemanggilan 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.

python Contoh: penghitung dengan nonlocal
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)
text Keluaran
1 2 3
1
1

Penghitung 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().

python Contoh: pabrik pengali
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)
text Keluaran
20 50
2 5

Fungsi 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.

Lingkup buat_sapasel: nama = "Ali"tetap hidup selama dirujukmenangkapObjek fungsi sapakode: return "Halo, " + nama__closure__: (sel nama,)sapa() membaca sel saat dipanggil
Closure menyimpan rujukan ke sel variabel milik lingkup pabriknya

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.

python Contoh: penangkapan terlambat dalam perulangan
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]())
text Keluaran
2 2 2
0 1 2

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

python Contoh: penugasan tanpa nonlocal
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))
text Keluaran
UnboundLocalError cannot access local variable 'total' where it is not associated with a value

Pesan 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 nonlocal mengarahkan 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#