Context manager dan with

Context manager dan pernyataan with untuk pembersihan sumber daya secara deterministik.

Daftar isi

Sumber daya seperti berkas dan koneksi jaringan harus dilepaskan kembali setelah dipakai, termasuk ketika exception terjadi di tengah pemakaiannya. Pernyataan with menjalankan pembersihan tersebut secara otomatis melalui protokol context manager. Topik ini membahas pemakaian with pada berkas, protokol __enter__ dan __exit__ di baliknya, serta penulisan context manager sendiri dengan bantuan modul contextlib.

Pernyataan with pada berkas#

Bentuk with yang paling sering ditemui adalah pembukaan berkas. Fungsi open() mengembalikan objek berkas, kata kunci as mengikat objek tersebut ke sebuah nama, dan penutupan berkas dijamin terjadi ketika blok selesai, apa pun yang terjadi di dalamnya. Atribut closed pada objek berkas menyatakan keadaan tersebut.

python Contoh: menulis berkas dengan with
with open("catatan.txt", "w", encoding="utf-8") as f:
    f.write("baris pertama\n")
    f.write("baris kedua\n")
print("berkas ditutup otomatis:", f.closed)
text Keluaran
berkas ditutup otomatis: True
python Contoh: membaca berkas dengan with
with open("catatan.txt", encoding="utf-8") as f:
    isi = f.read()
print(isi, end="")
text Keluaran
baris pertama
baris kedua

Setelah blok with selesai, berkas sudah tertutup meskipun tidak ada satu pun pemanggilan close() yang ditulis. Membaca isinya memerlukan pernyataan with yang baru. Bandingkan dengan pola tanpa with, yang menuntut pasangan try dan finally agar close() tetap dipanggil ketika exception terjadi di tengah penulisan. Pernyataan with memadatkan seluruh pola itu menjadi satu baris.

Protokol context manager#

Objek yang sah dipakai dengan with disebut context manager. Protokolnya terdiri atas dua metode khusus. Metode __enter__() dijalankan saat pernyataan with dimulai, dan nilai kembaliannya diikat ke nama sesudah as. Metode __exit__() dijalankan saat blok selesai dengan tiga argumen, yaitu jenis, nilai, dan objek traceback dari exception yang sedang berlangsung, atau tiga nilai None apabila tidak ada exception. Diagram berikut merangkum siklusnya.

with ekspresi as nama__enter__() dijalankanbadan with dijalankan__exit__(None, None, None)__exit__(tipe, nilai, tb)tanpa exceptionada exceptionprogram berlanjutreturn True menelan exceptionreturn False meneruskannya
Siklus pernyataan with pada protokol context manager
python Contoh: kelas context manager sederhana
class Sesi:
    def __enter__(self):
        print("__enter__: sumber daya disiapkan")
        return self

    def __exit__(self, tipe, nilai, tb):
        if tipe is None:
            print("__exit__: badan selesai tanpa error")
        else:
            print("__exit__: terjadi", type(nilai).__name__)
        return False


with Sesi() as sesi:
    print("badan with dijalankan")

try:
    with Sesi() as sesi:
        raise ValueError("sesuatu gagal")
except ValueError as e:
    print("ditangkap di luar:", e)
text Keluaran
__enter__: sumber daya disiapkan
badan with dijalankan
__exit__: badan selesai tanpa error
__enter__: sumber daya disiapkan
__exit__: terjadi ValueError
ditangkap di luar: sesuatu gagal

Ketika badan with selesai dengan normal, __exit__() menerima tiga nilai None. Ketika exception terjadi, ketiga argumennya terisi, tetapi nilai kembalian False meneruskan exception itu ke luar sehingga except pemanggil dapat menanganinya. Dengan kata lain, with menjamin pembersihan dijalankan, sedangkan nasib exception tetap ditentukan oleh nilai kembalian __exit__().

python Contoh: return True menelan exception
class PenelanError:
    def __enter__(self):
        return self

    def __exit__(self, tipe, nilai, tb):
        if tipe is not None:
            print("error ditelan:", type(nilai).__name__)
            return True
        return False


with PenelanError():
    raise KeyError("detail yang dihilangkan")
print("program berlanjut tanpa error")
text Keluaran
error ditelan: KeyError
program berlanjut tanpa error

KeyError yang dibangkitkan tidak pernah keluar dari blok karena __exit__() mengembalikan True. Kemampuan ini berguna untuk kegagalan yang memang diharapkan, tetapi menelan exception tanpa sengaja akan menyembunyikan bug. Karena itu, True sebaiknya dikembalikan hanya bila penanganan yang menyeluruh sudah dilakukan di dalam metode tersebut.

Modul contextlib#

Modul contextlib mengurangi kerangka kode ketika context manager ditulis berulang-ulang. Decorator @contextmanager mengubah fungsi generator, yaitu fungsi yang memuat pernyataan yield, menjadi context manager. Pada fungsi yang sudah dibungkus, kode sebelum yield berperan sebagai __enter__(), sedangkan kode sesudahnya sebagai __exit__(). Apabila protokol lengkapnya sudah dipahami, pilihannya sederhana, yaitu kelas untuk context manager yang kompleks dan dapat dipakai ulang, sedangkan @contextmanager untuk pembersihan yang ringkas dalam satu fungsi.

python Contoh: @contextmanager dari contextlib
from contextlib import contextmanager


@contextmanager
def penghitung(nama):
    print("sebelum badan:", nama)
    try:
        yield 42
    finally:
        print("sesudah badan:", nama)


with penghitung("tugas-A") as nilai:
    print("di dalam with, nilai =", nilai)
text Keluaran
sebelum badan: tugas-A
di dalam with, nilai = 42
sesudah badan: tugas-A

Nilai yang dilewatkan yield menjadi nilai yang diikat oleh as. Apabila badan with menimbulkan exception, exception itu tiba di titik yield, sehingga penanganannya dapat ditulis pada fungsi generator itu sendiri. Pemakaian try dan finally di sekitar yield memastikan pembersihan tetap berjalan, sama seperti kontrak __exit__().

Fungsi contextlib.suppress() menyediakan cara eksplisit untuk menelan jenis exception tertentu.

python Contoh: contextlib.suppress
from contextlib import suppress

with suppress(FileNotFoundError):
    open("berkas-tidak-ada.txt", encoding="utf-8")
print("berkas memang tidak ada, program berlanjut")
text Keluaran
berkas memang tidak ada, program berlanjut

suppress(FileNotFoundError) menelan FileNotFoundError dan hanya itu, sedangkan jenis lain tetap diteruskan. Dibandingkan blok try dan except yang kosong, maksud penulisnya lebih terbaca karena nama fungsinya sudah menyatakan penanganan yang disengaja. Modul ini juga menyediakan contextlib.closing() untuk objek yang memiliki metode close() tetapi bukan context manager, sehingga objek semacam itu tidak perlu dibungkus kelas baru.

Kesalahan umum#

Kesalahan pertama adalah mengira with menangkap exception. Pernyataan with hanya menjamin __exit__() dijalankan, dan kecuali metode itu mengembalikan True, exception tetap diteruskan keluar blok. Kesalahan yang menyusul sering berupa pemakaian objek di luar masa hidupnya.

python Contoh: berkas sudah tertutup di luar with
with open("catatan.txt", encoding="utf-8") as f:
    print("di dalam:", f.readline().strip())
print("di luar with:", f.closed)
try:
    f.read()
except ValueError as e:
    print("error:", type(e).__name__, "-", e)
text Keluaran
di dalam: baris pertama
di luar with: True
error: ValueError - I/O operation on closed file.

Nama f masih terikat setelah blok selesai, tetapi berkasnya sudah tertutup, sehingga f.read() menimbulkan ValueError dengan pesan I/O operation on closed file. Pesan tersebut hampir selalu berarti objek berkas dipakai di luar blok with-nya. Perbaikannya adalah memindahkan seluruh pemakaian ke dalam blok.

Kesalahan kedua adalah menyusun dua pernyataan with bersarang padahal keduanya dapat ditulis berdampingan. Satu pernyataan with menerima beberapa context manager sekaligus, dan pembersihannya berjalan berkebalikan dengan urutan masuknya.

python Contoh: dua context manager dalam satu with
with open("catatan.txt", encoding="utf-8") as masuk, open("salinan.txt", "w", encoding="utf-8") as keluar:
    for baris in masuk:
        keluar.write(baris.upper())
print("salinan dibuat")
with open("salinan.txt", encoding="utf-8") as f:
    print(f.read(), end="")
text Keluaran
salinan dibuat
BARIS PERTAMA
BARIS KEDUA

Setiap baris sumber melewati upper() sebelum ditulis ke salinan. Ketika blok selesai, salinan.txt ditutup lebih dahulu, lalu catatan.txt, sesuai urutan berkebalikan dari pembukaannya. Apabila salah satu pembukaan gagal, sumber daya yang sudah terbuka tetap dibersihkan.

Ringkasan#

  • Pernyataan with memanggil __enter__() saat masuk dan __exit__() saat keluar, sehingga pembersihan sumber daya berjalan pasti, termasuk ketika exception terjadi.
  • Nilai kembalian __enter__() diikat dengan as, sedangkan nilai kembalian __exit__() menentukan apakah exception diteruskan atau ditelan.
  • Decorator @contextmanager mengubah fungsi generator menjadi context manager, dengan kode sebelum yield sebagai pembuka dan kode sesudahnya sebagai pembersih.
  • contextlib.suppress() menelan jenis exception yang disebutkan secara eksplisit, dan satu pernyataan with dapat membuka beberapa context manager sekaligus.

Referensi#