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.
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)berkas ditutup otomatis: Truewith open("catatan.txt", encoding="utf-8") as f:
isi = f.read()
print(isi, end="")baris pertama
baris keduaSetelah 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.
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)__enter__: sumber daya disiapkan
badan with dijalankan
__exit__: badan selesai tanpa error
__enter__: sumber daya disiapkan
__exit__: terjadi ValueError
ditangkap di luar: sesuatu gagalKetika 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__().
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")error ditelan: KeyError
program berlanjut tanpa errorKeyError 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.
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)sebelum badan: tugas-A
di dalam with, nilai = 42
sesudah badan: tugas-ANilai 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.
from contextlib import suppress
with suppress(FileNotFoundError):
open("berkas-tidak-ada.txt", encoding="utf-8")
print("berkas memang tidak ada, program berlanjut")berkas memang tidak ada, program berlanjutsuppress(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.
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)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.
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="")salinan dibuat
BARIS PERTAMA
BARIS KEDUASetiap 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
withmemanggil__enter__()saat masuk dan__exit__()saat keluar, sehingga pembersihan sumber daya berjalan pasti, termasuk ketika exception terjadi. - Nilai kembalian
__enter__()diikat denganas, sedangkan nilai kembalian__exit__()menentukan apakah exception diteruskan atau ditelan. - Decorator
@contextmanagermengubah fungsi generator menjadi context manager, dengan kode sebelumyieldsebagai pembuka dan kode sesudahnya sebagai pembersih. contextlib.suppress()menelan jenis exception yang disebutkan secara eksplisit, dan satu pernyataanwithdapat membuka beberapa context manager sekaligus.
Referensi#
- The with statement — Python Software Foundation — Referensi bahasa untuk semantik pernyataan with dan protokol context managernya.
- With Statement Context Managers — Python Software Foundation — Bagian model data Python yang menjelaskan enter dan exit.
- contextlib — Python Software Foundation — Dokumentasi modul contextlib beserta @contextmanager dan suppress.