Context Manager #
Setiap kali kamu membuka file, membuat koneksi database, atau mengakquire lock, ada tanggung jawab tersembunyi: memastikan sumber daya itu ditutup kembali — bahkan kalau terjadi error di tengah jalan. Tanpa penanganan yang tepat, resource leak adalah masalah yang sering tidak terlihat hingga sistem mulai berperilaku aneh. Context manager adalah mekanisme Python untuk menyelesaikan masalah ini secara elegan: kamu mendefinisikan apa yang terjadi saat masuk dan keluar dari sebuah konteks, dan Python menjamin keduanya selalu dieksekusi melalui pernyataan with.
Masalah yang Dipecahkan Context Manager #
Tanpa context manager, kode pengelolaan sumber daya penuh dengan boilerplate dan rawan lupa:
# ANTI-PATTERN: pengelolaan resource manual — rawan resource leak
file = open("data.txt", "r")
konten = file.read()
file.close() # bagaimana jika file.read() melempar eksepsi? close() tidak akan dipanggil!
# Versi "aman" manual yang benar tapi verbose:
file = open("data.txt", "r")
try:
konten = file.read()
finally:
file.close() # finally memastikan close() selalu dipanggil
# BENAR: gunakan context manager — ringkas dan aman secara otomatis
with open("data.txt", "r") as file:
konten = file.read()
# file.close() dipanggil otomatis di sini, termasuk saat terjadi eksepsi
Blok with menjamin bahwa proses cleanup selalu berjalan, apapun yang terjadi di dalam blok — termasuk saat terjadi eksepsi yang tidak tertangkap.
Cara Kerja: Protokol __enter__ dan __exit__
#
Sebuah objek bisa digunakan sebagai context manager jika mengimplementasikan dua metode:
with KonteksKu() as obj:
# lakukan sesuatu
Alur eksekusi:
1. KonteksKu().__enter__() dipanggil
→ nilai kembaliannya diikat ke variabel `obj`
2. Blok with dijalankan
3. KonteksKu().__exit__(exc_type, exc_val, exc_tb) dipanggil
→ dipanggil selalu, bahkan saat terjadi eksepsi
→ jika return True: eksepsi diserap (tidak dipropagasi)
→ jika return False/None: eksepsi dipropagasi ke atas
Variabel setelahasmenerima nilai kembalian dari__enter__(), bukan instance context manager itu sendiri. Untukopen(),__enter__()mengembalikan objek file — itulah mengapa kamu bisa menuliswith open(...) as fdan langsung menggunakanf.
Membuat Context Manager dengan Kelas #
Cara paling eksplisit adalah mendefinisikan kelas dengan metode __enter__ dan __exit__. Cocok untuk context manager yang kompleks atau perlu menyimpan state.
import time
class Timer:
"""Context manager untuk mengukur waktu eksekusi sebuah blok kode."""
def __enter__(self):
self.mulai = time.perf_counter()
return self # diikat ke variabel setelah `as`
def __exit__(self, exc_type, exc_val, exc_tb):
self.durasi = time.perf_counter() - self.mulai
print(f"Waktu eksekusi: {self.durasi:.4f} detik")
return False # propagasi eksepsi jika ada
with Timer() as t:
total = sum(range(10_000_000))
print(f"Total: {total}, durasi tersimpan: {t.durasi:.4f} detik")
Contoh lain — context manager untuk koneksi database:
import sqlite3
class KoneksiDB:
def __init__(self, path_db):
self.path_db = path_db
self.koneksi = None
def __enter__(self):
self.koneksi = sqlite3.connect(self.path_db)
return self.koneksi # kembalikan objek koneksi, bukan self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is None:
self.koneksi.commit() # sukses → commit
else:
self.koneksi.rollback() # error → rollback
self.koneksi.close()
return False # propagasi eksepsi
with KoneksiDB("aplikasi.db") as db:
cursor = db.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER, nama TEXT)")
cursor.execute("INSERT INTO users VALUES (1, 'Budi')")
# commit otomatis di sini — rollback otomatis jika ada error
Menangani Eksepsi di __exit__
#
Parameter exc_type, exc_val, exc_tb memberi informasi tentang eksepsi yang terjadi. Kamu bisa memutuskan untuk menyerap atau meneruskannya:
class KonteksAman:
def __enter__(self):
print("Masuk konteks.")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print(f"ValueError ditangkap dan diserap: {exc_val}")
return True # serap eksepsi — kode setelah `with` tetap berjalan
if exc_type is not None:
print(f"Eksepsi lain diteruskan: {exc_type.__name__}")
return False # propagasi eksepsi lainnya
with KonteksAman():
raise ValueError("Nilai tidak valid")
print("Baris ini tetap dieksekusi karena ValueError diserap.")
Berhati-hatilah saat menyerap eksepsi denganreturn Truedi__exit__. Ini bisa menyembunyikan error yang seharusnya ditangani oleh pemanggil. Serap eksepsi hanya jika kamu memang tahu eksepsi tersebut bisa diabaikan dengan aman dalam konteks tersebut.
Membuat Context Manager dengan @contextmanager
#
Untuk context manager yang lebih sederhana, modul contextlib menyediakan dekorator @contextmanager yang memungkinkan kamu menulis context manager sebagai fungsi generator — tanpa perlu mendefinisikan kelas.
from contextlib import contextmanager
@contextmanager
def timer(label="Operasi"):
import time
mulai = time.perf_counter()
try:
yield # eksekusi blok `with` terjadi di sini
finally:
durasi = time.perf_counter() - mulai
print(f"{label}: {durasi:.4f} detik")
with timer("Hitung total"):
total = sum(range(10_000_000))
Pola try/yield/finally adalah idiom standar untuk @contextmanager:
from contextlib import contextmanager
import os
@contextmanager
def direktori_sementara(path):
"""Buat direktori, jalankan blok, lalu hapus direktori."""
os.makedirs(path, exist_ok=True)
print(f"Direktori '{path}' dibuat.")
try:
yield path # nilai yang di-yield diikat ke variabel `as`
finally:
import shutil
shutil.rmtree(path)
print(f"Direktori '{path}' dihapus.")
with direktori_sementara("/tmp/kerja_sementara") as folder:
# tulis file sementara
with open(f"{folder}/output.txt", "w") as f:
f.write("data sementara")
print(f"File dibuat di: {folder}/output.txt")
# direktori otomatis dihapus setelah blok selesai
Perbandingan kelas vs @contextmanager:
Gunakan kelas jika:
✓ Context manager perlu menyimpan state yang kompleks
✓ Perlu diwarisi atau diextend
✓ Ada banyak metode helper selain __enter__/__exit__
Gunakan @contextmanager jika:
✓ Logikanya sederhana dan linear
✓ Ingin kode lebih ringkas
✓ Tidak perlu inheritance
Mengelola Banyak Context Manager #
Beberapa with dalam Satu Baris
#
Python memungkinkan membuka beberapa context manager sekaligus dalam satu pernyataan with:
# ANTI-PATTERN: nested with yang tidak perlu
with open("input.txt", "r") as masuk:
with open("output.txt", "w") as keluar:
keluar.write(masuk.read())
# BENAR: gabungkan dalam satu with
with open("input.txt", "r") as masuk, open("output.txt", "w") as keluar:
keluar.write(masuk.read())
ExitStack untuk Jumlah Context Manager yang Dinamis
#
Jika jumlah context manager tidak diketahui saat kode ditulis — misalnya ditentukan dari konfigurasi atau input pengguna — gunakan contextlib.ExitStack:
from contextlib import ExitStack
def proses_banyak_file(daftar_file):
with ExitStack() as stack:
# buka semua file secara dinamis
file_handles = [
stack.enter_context(open(f, "r"))
for f in daftar_file
]
# semua file terbuka di sini
for fh in file_handles:
print(fh.readline())
# semua file otomatis ditutup setelah blok selesai
proses_banyak_file(["a.txt", "b.txt", "c.txt"])
ExitStack juga bisa digunakan untuk menambahkan callback cleanup secara dinamis:
from contextlib import ExitStack
with ExitStack() as stack:
stack.callback(print, "Cleanup 1 dijalankan")
stack.callback(print, "Cleanup 2 dijalankan")
print("Melakukan pekerjaan...")
# Output (urutan terbalik — LIFO):
# Melakukan pekerjaan...
# Cleanup 2 dijalankan
# Cleanup 1 dijalankan
Ringkasan #
- Context manager menjamin cleanup selalu berjalan — termasuk saat terjadi eksepsi, melalui mekanisme
__exit__yang selalu dipanggil.- Protokol context manager:
__enter__()dijalankan saat masuk blokwith,__exit__()dijalankan saat keluar — nilai kembalian__enter__diikat ke variabelas.return Truedi__exit__menyerap eksepsi — gunakan ini dengan hati-hati; jangan sembunyikan error yang seharusnya ditangani.@contextmanageruntuk kasus sederhana — tulis context manager sebagai generator dengan polatry/yield/finallytanpa perlu mendefinisikan kelas.- Nilai setelah
yielddalam@contextmanagerdiikat ke variabelasdi blokwith.- Gabungkan beberapa context manager dalam satu
withmenggunakan koma untuk menghindari nesting yang tidak perlu.ExitStackuntuk jumlah context manager yang dinamis — ideal saat kamu tidak tahu berapa banyak resource yang perlu dikelola saat kode ditulis.