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 setelah as menerima nilai kembalian dari __enter__(), bukan instance context manager itu sendiri. Untuk open(), __enter__() mengembalikan objek file — itulah mengapa kamu bisa menulis with open(...) as f dan langsung menggunakan f.

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 dengan return True di __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 blok with, __exit__() dijalankan saat keluar — nilai kembalian __enter__ diikat ke variabel as.
  • return True di __exit__ menyerap eksepsi — gunakan ini dengan hati-hati; jangan sembunyikan error yang seharusnya ditangani.
  • @contextmanager untuk kasus sederhana — tulis context manager sebagai generator dengan pola try/yield/finally tanpa perlu mendefinisikan kelas.
  • Nilai setelah yield dalam @contextmanager diikat ke variabel as di blok with.
  • Gabungkan beberapa context manager dalam satu with menggunakan koma untuk menghindari nesting yang tidak perlu.
  • ExitStack untuk jumlah context manager yang dinamis — ideal saat kamu tidak tahu berapa banyak resource yang perlu dikelola saat kode ditulis.

← Sebelumnya: Multi Process   Berikutnya: Decorator →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact