%%{init: {"flowchart": {"diagramPadding": 40}}}%%
flowchart TD
A["Tulis ke laporan.txt.tmp"] --> B{"Berhasil sampai akhir?"}
B -- "ya" --> C["os.replace: tukar dengan laporan.txt"]
B -- "tidak (galat)" --> D["Buang berkas sementara,<br/>laporan lama tetap utuh"]
Bab 32 — Context Manager: Membuka, Memakai, dan Pasti Menutup
Seri I4 — Bahasa Python Tingkat Lanjut
Semua skrip bab ini ada di folder kode/bab32/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Pernahkah Anda lupa mematikan pompa kolam setelah selesai mengisi air? Pompa terus menyala, listrik terbuang, atau kolam meluap. Program punya masalah serupa: berkas yang dibuka dan lupa ditutup, koneksi yang dibiarkan menggantung. Lebih parah lagi bila di tengah pekerjaan terjadi galat: baris close() di ujung tidak pernah tercapai.
Python punya solusi: pernyataan with. Ia berjanji bahwa apa pun yang terjadi di dalam blok, urusan “tutup dan bersihkan” pasti dikerjakan.

with pada berkas
Anda sudah memakai with open(...) sejak Bab 13. Bandingkan dengan cara lama:
01_with_berkas.py
# Bab 32 - with menutup berkas otomatis
from pathlib import Path
folder = Path(__file__).parent / "_tmp"
folder.mkdir(exist_ok=True)
jalur = folder / "catatan.txt"
# Cara lama: harus ingat close()
f = open(jalur, "w", encoding="utf-8")
f.write("panen kebun A: 245.5 kg\n")
print("sebelum close, tertutup?", f.closed)
f.close()
print("sesudah close, tertutup?", f.closed)
# Cara with: tertutup sendiri, bahkan bila terjadi galat
with open(jalur, encoding="utf-8") as f:
isi = f.read()
print("di dalam with, tertutup?", f.closed)
print("di luar with, tertutup?", f.closed)
print(isi.strip())sebelum close, tertutup? False
sesudah close, tertutup? True
di dalam with, tertutup? False
di luar with, tertutup? True
panen kebun A: 245.5 kg
- Cara lama:
f.closedbernilaiFalsesampai Anda sendiri memanggilf.close(). Kalau ada galat sebelumclose(), berkasnya tetap terbuka. - Cara
with: di dalam blok, berkas terbuka (False). Begitu blok selesai, ia tertutup (True), tanpa Anda menulisclose(). - Perhatikan bahwa variabel
fmasih ada setelah blok, tetapi berkasnya sudah tertutup.
Di balik layar: __enter__ dan __exit__
with bisa dipakai pada objek apa pun yang punya dua metode khusus: __enter__ (dijalankan saat masuk blok; nilainya masuk ke variabel setelah as) dan __exit__ (dijalankan saat keluar blok, selalu, termasuk bila terjadi galat). Objek seperti itu disebut context manager.
02_kelas_enter_exit.py
# Bab 32 - Context manager dengan kelas: __enter__ dan __exit__
class Pintu:
def __init__(self, nama):
self.nama = nama
def __enter__(self):
print(f"buka pintu {self.nama}")
return self # nilai setelah kata "as"
def __exit__(self, jenis_galat, galat, jejak):
print(f"tutup pintu {self.nama}")
return False # False: galat (bila ada) tetap diteruskan
with Pintu("kandang") as p:
print("bekerja di dalam", p.nama)
print("--- sekarang dengan galat ---")
try:
with Pintu("kolam"):
print("bekerja...")
raise ValueError("pompa mati")
except ValueError as e:
print("galat ditangkap di luar:", e)buka pintu kandang
bekerja di dalam kandang
tutup pintu kandang
--- sekarang dengan galat ---
buka pintu kolam
bekerja...
tutup pintu kolam
galat ditangkap di luar: pompa mati
- Pada percobaan pertama urutannya: buka, bekerja, tutup.
- Pada percobaan kedua, galat
ValueError("pompa mati")terjadi di dalam blok. Pintu tetap ditutup (“tutup pintu kolam” tercetak) sebelum galatnya muncul di luar dan ditangkapexcept. __exit__menerima tiga informasi galat (jenis, isi, jejak; nilainyaNonebila tidak ada galat). MengembalikanFalseberarti “galatnya silakan diteruskan”.
Cara singkat: @contextmanager
Menulis kelas dengan dua metode itu panjang. contextlib.contextmanager mengubah fungsi generator (Bab 30) menjadi context manager: kode sebelum yield menjadi __enter__, kode sesudahnya menjadi __exit__.
03_contextmanager.py
# Bab 32 - @contextmanager: context manager dari fungsi generator
from contextlib import contextmanager
@contextmanager
def sesi_kerja(nama):
print(f"[{nama}] mulai")
try:
yield nama.upper() # isi blok with dijalankan di sini
finally:
print(f"[{nama}] selesai dan dibersihkan")
with sesi_kerja("pemupukan") as kode:
print("bekerja dengan kode", kode)
try:
with sesi_kerja("pemanenan"):
print("bekerja...")
1 / 0
except ZeroDivisionError:
print("galat tertangkap, tetapi pembersihan tetap jalan")[pemupukan] mulai
bekerja dengan kode PEMUPUKAN
[pemupukan] selesai dan dibersihkan
[pemanenan] mulai
bekerja...
[pemanenan] selesai dan dibersihkan
galat tertangkap, tetapi pembersihan tetap jalan
yield nama.upper()melepaskan kendali ke blokwithdan memberikan nilaiPEMUPUKANke variabelkode.try ... finallydi sekelilingyieldmenjamin pembersihan. Pada percobaan kedua terjadi1 / 0, tetapi “selesai dan dibersihkan” tetap tercetak, lalu galatnya tertangkap di luar.
Selalu bungkus yield di @contextmanager dengan try ... finally bila ada pembersihan. Tanpa finally, bila blok with terjadi galat, baris pembersihan sesudah yield tidak dijalankan.
__exit__ boleh menelan galat
Bila __exit__ mengembalikan True, galatnya dianggap sudah ditangani dan program lanjut seolah tidak ada apa-apa. Ini kuat tetapi berbahaya.
04_menelan_galat.py
# Bab 32 - __exit__ boleh menelan galat (dengan hati-hati)
from contextlib import suppress
class AbaikanKosong:
def __enter__(self):
return self
def __exit__(self, jenis_galat, galat, jejak):
if jenis_galat is ZeroDivisionError:
print("dibagi nol diabaikan")
return True # True: galat dianggap sudah ditangani
return False
with AbaikanKosong():
print(10 / 0)
print("program lanjut")
# Pustaka standar sudah menyediakan yang serupa
with suppress(FileNotFoundError):
open("tidak_ada.txt", encoding="utf-8")
print("lanjut lagi")dibagi nol diabaikan
program lanjut
lanjut lagi
Pada blok pertama, 10 / 0 melempar ZeroDivisionError, tetapi __exit__ mengembalikan True, jadi “program lanjut” tetap tampil. Pustaka standar menyediakan versi yang lebih rapi: contextlib.suppress(FileNotFoundError), yang menelan hanya jenis galat yang Anda sebut.
Menelan galat bisa menyembunyikan bug. Telan hanya jenis galat yang Anda harapkan dan memang tidak berbahaya (misalnya berkas yang memang boleh tidak ada). Jangan menelan semua galat.
Alat siap pakai di contextlib
05_alat_contextlib.py
# Bab 32 - Alat siap pakai di contextlib dan modul lain
import io
from contextlib import ExitStack, redirect_stdout
from pathlib import Path
# redirect_stdout: tangkap apa yang dicetak
tangkap = io.StringIO()
with redirect_stdout(tangkap):
print("ini tidak tampil di layar")
print("tertangkap:", repr(tangkap.getvalue()))
# ExitStack: jumlah berkas baru diketahui saat program jalan
folder = Path(__file__).parent / "_tmp"
folder.mkdir(exist_ok=True)
nama = ["kolam_a.txt", "kolam_b.txt", "kolam_c.txt"]
with ExitStack() as tumpukan:
berkas = [tumpukan.enter_context(open(folder / n, "w", encoding="utf-8")) for n in nama]
for i, f in enumerate(berkas):
f.write(f"data kolam nomor {i + 1}\n")
print("terbuka bersamaan:", len(berkas))
print("semua tertutup:", all(f.closed for f in berkas))tertangkap: 'ini tidak tampil di layar\n'
terbuka bersamaan: 3
semua tertutup: True
redirect_stdoutmengalihkan apa yang dicetakprintke penampungio.StringIO. Layar tidak menampilkan apa-apa, tetapi isinya bisa dibaca lagi ('ini tidak tampil di layar\n'). Berguna untuk menguji fungsi yang mencetak.ExitStackdipakai bila jumlah sumber daya baru diketahui saat program berjalan. Di sini tiga berkas dibuka lewatenter_context, dan ketiganya ditutup bersama saat blok selesai.
Jebakan: with pada objek yang bukan context manager
06_bukan_context_manager.py
# Bab 32 - Salah: with pada objek yang bukan context manager
class Gudang:
pass
with Gudang() as g:
print("tidak akan sampai sini")Traceback (most recent call last):
File "06_bukan_context_manager.py", line 6, in <module>
with Gudang() as g:
~~~~~~^^
TypeError: 'Gudang' object does not support the context manager protocol (missed __exit__ method)
Pesan galatnya jelas: kelas Gudang tidak punya __exit__ (dan __enter__), jadi tidak mendukung “protokol context manager”. Solusinya: tambahkan kedua metode itu, atau pakai @contextmanager.
Latihan
- Tulis context manager kelas
Pencatatyang mencetak “mulai” di__enter__dan “selesai” di__exit__. Pakai denganwithdan tambahkan satuprintdi dalamnya. - Ubah
03_contextmanager.py: tambahkan context managersesi_kerja("penyiraman")ketiga yang mengalamiValueError, dan tangkap galat itu di luar. Apakah “dibersihkan” tetap tercetak? - Gunakan
contextlib.suppress(FileNotFoundError)untuk menghapus berkas_tmp/tidak_ada.txtdenganPath.unlink()tanpa error bila berkasnya memang tidak ada. - Tantangan: tulis context manager
catat_langkah(nama)memakai@contextmanageryang mencetak “[nama] mulai” dan “[nama] selesai”; bila terjadi galat, cetak “[nama] GAGAL” lalu lempar ulang galatnya. Uji dengan satu blok sukses dan satu blok gagal.
Ringkasan Bab
withmenjamin pembersihan (menutup berkas, melepas sumber daya) walau terjadi galat.- Context manager = objek dengan
__enter__dan__exit__;__exit__selalu jalan. @contextmanagermengubah fungsi generator menjadi context manager; bungkusyielddengantry ... finally.__exit__yang mengembalikanTruemenelan galat; pakaisuppresshanya untuk jenis galat yang diharapkan.contextlibmenyediakansuppress,redirect_stdout, danExitStack(jumlah sumber daya dinamis).- Proyek I4: laporan ditulis lewat berkas sementara, sehingga laporan lama tetap utuh bila penulisan gagal.
Selanjutnya, di Bab 33: type hint, cara memberi tahu manusia dan alat pemeriksa tentang jenis data yang diharapkan.