Context manager adalah objek yang menetapkan pekerjaan persiapan dan pembersihan di sekitar sebuah blok kode, dan pernyataan with memastikan keduanya dijalankan. Pustaka standar sudah memakai pola ini untuk berkas, kunci (lock), dan koneksi, tetapi Python juga menyediakan protokol untuk membuat context manager sendiri, baik berupa kelas dengan metode __enter__ dan __exit__ maupun fungsi generator dengan decorator @contextmanager. Topik ini membahas keduanya beserta aturan penanganan exception di dalamnya.
Kelas dengan enter dan exit#
Sebuah kelas menjadi context manager dengan menyediakan dua metode khusus. Metode __enter__ dipanggil saat masuk blok with, dan nilai kembaliannya ditugaskan ke variabel setelah kata as. Metode __exit__ dipanggil saat blok selesai, baik selesai normal maupun karena exception, sehingga pekerjaan pembersihan tidak pernah terlewat.
class Pelacak:
def __init__(self, nama):
self.nama = nama
def __enter__(self):
print("masuk", self.nama)
return self
def __exit__(self, tipe, nilai, traceback):
print("keluar", self.nama)
return False
with Pelacak("tahap-1") as p:
print("badan with berjalan")
print("selesai")masuk tahap-1
badan with berjalan
keluar tahap-1
selesaiKeluaran menegaskan urutannya, yaitu __enter__ terlebih dahulu, badan with, lalu __exit__ tanpa syarat. Parameter __exit__ berjumlah tiga dan berisi informasi exception apabila badan blok gagal, atau None untuk ketiganya bila blok selesai normal. Diagram berikut merangkum cabang keputusan yang terjadi di sekitar blok with.
Menelan atau meneruskan exception#
Nilai kembalian __exit__ menentukan nasib exception yang terjadi di badan blok. Nilai True menandakan exception sudah ditangani, sehingga with menelannya dan program berlanjut, sedangkan nilai False atau None membiarkan exception menyebar ke luar. Pembersihan di dalam __exit__ tetap berjalan pada kedua keadaan tersebut.
class TangkapSemua:
def __enter__(self):
return self
def __exit__(self, tipe, nilai, traceback):
if tipe is not None:
print("menangkap", tipe.__name__)
return True
with TangkapSemua():
raise ValueError("contoh")
print("program berlanjut")menangkap ValueError
program berlanjutPemanggilan raise di badan blok tidak menghentikan program karena __exit__ mengembalikan True setelah mencatat nama exceptionnya. Bandingkan dengan versi yang mengembalikan False pada contoh berikut, yang tetap membersihkan namun membiarkan exception naik.
class JanganTangkap:
def __enter__(self):
return self
def __exit__(self, tipe, nilai, traceback):
print("bersih tetap dijalankan meski error")
return False
try:
with JanganTangkap():
raise ValueError("contoh")
except ValueError as e:
print("tertangkap di luar:", e)bersih tetap dijalankan meski error
tertangkap di luar: contohProgram utuh: transaksi dictionary#
Contoh berikut merangkai protokol di atas menjadi context manager transaksi, yaitu context manager yang mencadangkan isi dictionary saat masuk dan memulihkannya bila badan blok berakhir dengan exception. Pola semacam ini berguna untuk pembaruan data yang harus bersifat utuh, berhasil semua atau tidak sama sekali.
class Transaksi:
"""Context manager yang memulihkan isi dictionary bila terjadi error."""
def __init__(self, data):
self.data = data
def __enter__(self):
self.cadangan = dict(self.data)
return self.data
def __exit__(self, tipe, nilai, traceback):
if tipe is not None:
self.data.clear()
self.data.update(self.cadangan)
print("transaksi dibatalkan, data dipulihkan")
return True
print("transaksi berhasil, data tersimpan")
return False
stok = {"apel": 10, "jeruk": 5}
with Transaksi(stok) as data:
data["apel"] = 8
data["mangga"] = 4
print(dict(sorted(stok.items())))
with Transaksi(stok) as data:
data["apel"] = 0
raise RuntimeError("gudang tutup")
print(dict(sorted(stok.items())))transaksi berhasil, data tersimpan
{'apel': 8, 'jeruk': 5, 'mangga': 4}
transaksi dibatalkan, data dipulihkan
{'apel': 8, 'jeruk': 5, 'mangga': 4}Simpan berkas di atas di direktori kerja, lalu jalankan dengan perintah berikut.
# menjalankan program context manager transaksi
python3 transaksi.pytransaksi berhasil, data tersimpan
{'apel': 8, 'jeruk': 5, 'mangga': 4}
transaksi dibatalkan, data dipulihkan
{'apel': 8, 'jeruk': 5, 'mangga': 4}Transaksi pertama selesai normal, sehingga perubahan apel dan penambahan mangga bertahan. Transaksi kedua mengubah apel menjadi 0 sebelum menimbulkan RuntimeError, dan __exit__ memulihkan isi dictionary dari cadangannya, sehingga cetakan terakhir kembali menampilkan apel bernilai 8. Metode __enter__ mengembalikan dictionary aslinya, sehingga badan blok bekerja langsung pada data yang dipantau, tanpa salinan tambahan di luar cadangan internal.
@contextmanager dari contextlib#
Untuk context manager sederhana, menulis kelas lengkap menjadi berlebihan. Modul contextlib menyediakan decorator @contextmanager yang mengubah fungsi generator menjadi context manager, dengan pekerjaan sebelum yield sebagai pengganti __enter__ dan pekerjaan sesudahnya sebagai pengganti __exit__.
from contextlib import contextmanager
@contextmanager
def indikator(nama):
print("mulai", nama)
try:
yield
finally:
print("selesai", nama)
with indikator("unduh"):
print("proses berjalan")mulai unduh
proses berjalan
selesai unduhKode sebelum yield berjalan saat masuk blok, sedangkan kode pada klausa finally berjalan saat keluar blok, termasuk ketika badan with menimbulkan exception. Dengan demikian, membungkus yield dengan try/finally merupakan padanan langsung dari logika pembersihan pada __exit__, dan bentuk generator ini memang dirancang untuk kasus persiapan beserta pembersihannya saja.
Satu pernyataan with juga dapat membuka beberapa context manager sekaligus yang dipisahkan koma, dan __enter__ dipanggil menurut urutan penulisannya. Bentuk ini menggantikan dua blok with yang bersarang ketika kedua sumber daya dibuka bersama, misalnya berkas masukan dan berkas keluaran pada penyalinan teks, sehingga urutan pembukaan dan penutupannya tetap rapi pada satu baris.
Kesalahan umum#
Kesalahan pertama adalah menaruh kode pembersihan setelah yield tanpa try/finally. Bila badan with menimbulkan exception, eksekusi generator berhenti di yield, sehingga baris pembersihan tidak pernah tercapai.
from contextlib import contextmanager
@contextmanager
def rapuh(nama):
print("mulai", nama)
yield
print("selesai", nama)
try:
with rapuh("unduh"):
raise RuntimeError("terputus")
except RuntimeError as e:
print("tertangkap:", e)mulai unduh
tertangkap: terputusCetakan selesai unduh yang semestinya muncul tidak ada, dan sumber daya yang seharusnya dilepas tetap tertahan. Perbaikannya mengikuti pola indikator, yaitu membungkus yield dengan try/finally agar pembersihan berjalan pada setiap jalur keluar.
Kesalahan kedua adalah mendefinisikan __exit__ tanpa tiga parameternya. Protokol memanggil __exit__ dengan tiga argumen, sehingga definisi yang lebih pendek menimbulkan error tepat saat blok selesai.
class Keliru:
def __enter__(self):
return self
def __exit__(self):
pass
try:
with Keliru():
pass
except TypeError as e:
print(type(e).__name__, str(e))TypeError Keliru.__exit__() takes 1 positional argument but 4 were givenPesan errornya menyebut empat argumen, yaitu objek self ditambah tiga informasi exception dari protokol. Perbaikannya adalah mendeklarasikan __exit__(self, tipe, nilai, traceback) meskipun ketiga parameternya tidak semuanya dipakai, karena kontraknya ditetapkan oleh protokol, bukan oleh kebutuhan badan metode.
Ringkasan#
- Kelas menjadi context manager dengan metode
__enter__, yang nilainya ditugaskan keas, dan metode__exit__yang selalu dipanggil saat blok berakhir. - Nilai kembalian
__exit__menentukan nasib exception, yaituTruemenelannya, sedangkanFalseatauNonemembiarkannya menyebar. - Decorator
@contextmanagermengubah generator ber-satu-yield menjadi context manager, dengantry/finallydi sekitaryieldsebagai pembersihnya. - Pembersihan pada
__exit__maupun pada klausafinallyberjalan baik blok selesai normal maupun gagal, sehingga perilakunya setara dan pemilihannya bergantung pada keringkasan yang dibutuhkan.
Referensi#
- The with statement — Python Software Foundation — Semantik pernyataan with dan urutan pemanggilan protokolnya.
- contextlib.contextmanager — Python Software Foundation — Decorator pembuat context manager dari fungsi generator.
- object.exit — Python Software Foundation — Kontrak tiga argumen exit dan arti nilai kembalinya.