İçeriğe geç

Python'da Context Manager ve with Bloğu Derinlemesine

T
Tolgahan
· · 10 dk okuma · 59 görüntülenme

Python'da Context Manager ve with Bloğu Derinlemesine

Dosya açtın, işlemini yaptın, kapatmayı unuttun. Veritabanına bağlandın, sorguyu çalıştırdın, bağlantıyı kapatmadın. Kilitleme (lock) aldın, exception fırladı, kilidi bırakmadın. Bu senaryoların hepsi aynı kök problemi paylaşıyor: kaynak yönetimi. Bir kaynağı aldıktan sonra ne olursa olsun — başarılı da olsa, hata da fırlasa — geri vermek zorundasın.

Python'un with bloğu ve context manager'lar tam olarak bu problemi çözer. Ve bunu o kadar zarif yapar ki, bir kez alıştığında with olmadan kaynak yönetimi yapmak sana ilkel gelmeye başlar. Bu yazıda context manager'ları derinlemesine öğreneceksin — sadece kullanmayı değil, kendi context manager'ını yazmayı, contextlib'in güçlü araçlarını ve gerçek dünya senaryolarını.

Neden Context Manager?

Bunu bir otel odası analojisiyle düşün. Resepsiyondan kartı alırsın (kaynağı edinirsin), odayı kullanırsın (işlemini yaparsın), çıkışta kartı iade edersin (kaynağı bırakırsın). Kartı iade etmezsen, otel sistemi o odayı müsait göremez ve başkası kullanamaz. İşte kaynak sızıntısı (resource leak) tam olarak budur.

Context manager olmadan Python'da kaynak yönetimi şöyle görünür:

# ❌ Tehlikeli — hata durumunda dosya kapanmaz
f = open("veri.txt", "r")
icerik = f.read()           # Ya burada exception fırlarsa?
f.close()                   # Bu satıra hiç ulaşılmayabilir!

Try-finally ile düzeltmeye çalışırsan:

# 😐 Çalışır ama boilerplate fazla
f = open("veri.txt", "r")
try:
    icerik = f.read()
    # işlemler...
finally:
    f.close()               # Ne olursa olsun çalışır

Context manager ile:

# ✅ Temiz, güvenli, Pythonic
with open("veri.txt", "r") as f:
    icerik = f.read()
    # işlemler...
# f otomatik olarak kapatıldı — exception fırlasa bile

with bloğundan çıkıldığında — normal çıkış veya exception fark etmez — f.close() otomatik olarak çağrılır. Unutma riski sıfır.

with Bloğu Nasıl Çalışır?

with ifadesi çalıştığında Python arka planda şunu yapar:

  1. İfadeyi değerlendir (evaluate) → context manager nesnesini al

  2. Nesnenin __enter__() metodunu çağır → dönen değeri as değişkenine ata

  3. with bloğunun gövdesini çalıştır

  4. Blok bittiğinde (normal veya hata ile) __exit__() metodunu çağır

# with ifadesi:
with expression as variable:
    body

# Python'un arka planda yaptığı:
manager = expression              # context manager nesnesini al
variable = manager.__enter__()    # kaynağı edin
try:
    body                          # iş mantığını çalıştır
except:
    if not manager.__exit__(*sys.exc_info()):  # hata bilgisi ile çık
        raise                     # __exit__ True dönmediyse hatayı tekrar fırlat
else:
    manager.__exit__(None, None, None)  # normal çıkış

Bu iki sihirli metot — __enter__ ve __exit__ — context manager protokolünün tamamı. Herhangi bir nesne bu iki metodu implement ederse, with ile kullanılabilir.

Kendi Context Manager'ını Yaz: Sınıf Tabanlı

En temel yaklaşım bir sınıf yazmak:

import time

class TimerContext:
    """Bir kod bloğunun ne kadar sürede çalıştığını ölçer."""
    
    def __init__(self, label="İşlem"):
        self.label = label
        self.start_time = None
        self.elapsed = None
    
    def __enter__(self):
        """with bloğuna girerken çağrılır. Zamanlayıcıyı başlat."""
        self.start_time = time.perf_counter()
        return self  # as değişkenine atanacak değer
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """with bloğundan çıkarken çağrılır. Süreyi hesapla."""
        self.elapsed = time.perf_counter() - self.start_time
        print(f"⏱ {self.label}: {self.elapsed:.4f} saniye")
        
        # False döndür → exception varsa tekrar fırlatılsın
        # True döndürürsen exception yutulur (genelde istemezsin)
        return False

# Kullanım
with TimerContext("Veri işleme") as timer:
    # ağır bir işlem simülasyonu
    total = sum(range(10_000_000))

print(f"Ölçülen süre: {timer.elapsed:.4f}s")

Çıktı:

⏱ Veri işleme: 0.3821 saniye
Ölçülen süre: 0.3821s

`__exit__` parametreleri:

  • exc_type: Exception sınıfı (hata yoksa None)

  • exc_val: Exception nesnesi (hata yoksa None)

  • exc_tb: Traceback nesnesi (hata yoksa None)

  • Dönüş değeri: True dönerse exception bastırılır, False veya None dönerse exception tekrar fırlatılır

⚠️ Dikkat: __exit__'ten True döndürmek exception'ı yutmak demektir. Bunu sadece bilinçli olarak, çok spesifik durumlar için yap. Genel kural: False veya None döndür.

Gerçek Dünya Örneği: Veritabanı Bağlantı Yönetimi

Veritabanı bağlantıları en kritik kaynaklardan biri. Kapatılmayan bağlantılar pool tükenmesine ve uygulamanın çökmesine yol açar:

import sqlite3

class DatabaseConnection:
    """Veritabanı bağlantısını güvenle yöneten context manager.
    
    Başarılı işlemlerde commit, hata durumunda rollback yapar.
    Her durumda bağlantıyı kapatır.
    """
    
    def __init__(self, db_path, autocommit=True):
        self.db_path = db_path
        self.autocommit = autocommit
        self.connection = None
        self.cursor = None
    
    def __enter__(self):
        """Bağlantıyı aç ve cursor oluştur."""
        self.connection = sqlite3.connect(self.db_path)
        self.connection.row_factory = sqlite3.Row  # dict-like erişim
        self.cursor = self.connection.cursor()
        return self.cursor  # with ... as cursor şeklinde kullanılacak
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        """Hata varsa rollback, yoksa commit. Her durumda kapat."""
        if exc_type is not None:
            # Exception fırladı — değişiklikleri geri al
            print(f"⚠️ Hata oluştu, rollback yapılıyor: {exc_val}")
            self.connection.rollback()
        elif self.autocommit:
            # Başarılı — değişiklikleri kaydet
            self.connection.commit()
        
        # Her durumda bağlantıyı kapat
        self.cursor.close()
        self.connection.close()
        
        return False  # exception'ı tekrar fırlat

# Kullanım — başarılı senaryo
with DatabaseConnection("app.db") as cursor:
    cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT, email TEXT)")
    cursor.execute("INSERT INTO users (name, email) VALUES (?, ?)", ("Tolgahan", "tolgahan@example.com"))
    cursor.execute("INSERT INTO users (name, email) VALUES (?, ?)", ("Ahmet", "ahmet@example.com"))
    # with bloğu bittiğinde otomatik commit

# Kullanım — hata senaryosu
try:
    with DatabaseConnection("app.db") as cursor:
        cursor.execute("INSERT INTO users (name, email) VALUES (?, ?)", ("Ali", "ali@example.com"))
        raise ValueError("Bir şeyler ters gitti!")  # Hata fırladı
        cursor.execute("INSERT INTO users (name, email) VALUES (?, ?)", ("Veli", "veli@example.com"))
except ValueError:
    print("Hata yakalandı — Ali de Veli de eklenmedi (rollback yapıldı)")

Bu pattern'in güzelliği şu: veritabanı işlemlerinin atomik (atomic) olmasını garanti ediyorsun. Ya hepsi başarılı olur ya da hiçbiri. Ve bağlantı ne olursa olsun kapatılır.

contextlib: Daha Az Kodla Context Manager

Sınıf yazmak bazen fazla ceremoni (boilerplate) getirir. Python'un contextlib modülü bunu dramatik şekilde sadeleştirir.

@contextmanager Decorator

@contextmanager decorator'ı, bir generator fonksiyonu context manager'a dönüştürür:

from contextlib import contextmanager
import time

@contextmanager
def timer(label="İşlem"):
    """Sınıf yazmadan context manager — generator tabanlı."""
    start = time.perf_counter()
    try:
        yield start  # yield'den ÖNCE = __enter__, yield değeri = as değişkeni
    finally:
        elapsed = time.perf_counter() - start  # yield'den SONRA = __exit__
        print(f"⏱ {label}: {elapsed:.4f}s")

# Kullanım — sınıf tabanlı ile aynı, ama çok daha az kod
with timer("Hesaplama"):
    total = sum(x**2 for x in range(1_000_000))

Nasıl çalışıyor?

  1. yield'den önceki kod → __enter__() gibi çalışır

  2. yield edilen değer → as değişkenine atanır

  3. yield'den sonraki kod → __exit__() gibi çalışır

  4. try/finally → exception olsa bile cleanup yapılır

Bu yaklaşım, tek seferlik veya basit context manager'lar için idealdir. Karmaşık durum yönetimi gerekiyorsa (birden fazla kaynak, conditional logic) sınıf tabanlı yaklaşımı tercih et.

Veritabanı Bağlantısı — contextlib ile

from contextlib import contextmanager
import sqlite3

@contextmanager
def db_connection(db_path, autocommit=True):
    """Veritabanı bağlantı yönetimi — contextlib versiyonu."""
    conn = sqlite3.connect(db_path)
    conn.row_factory = sqlite3.Row
    cursor = conn.cursor()
    try:
        yield cursor
        if autocommit:
            conn.commit()
    except Exception:
        conn.rollback()
        raise  # exception'ı tekrar fırlat
    finally:
        cursor.close()
        conn.close()

# Kullanım aynı
with db_connection("app.db") as cursor:
    cursor.execute("SELECT * FROM users")
    for row in cursor.fetchall():
        print(dict(row))

Dosya Kilitleme (File Locking) Context Manager

Birden fazla process aynı dosyaya yazmaya çalıştığında veri bozulur. Dosya kilitleme ile bunu önleyebilirsin:

from contextlib import contextmanager
import fcntl
import os

@contextmanager
def file_lock(filepath):
    """Dosya tabanlı kilitleme — aynı anda tek process yazabilir.
    
    Unix/Linux sistemlerde fcntl.flock kullanır.
    """
    lock_path = filepath + ".lock"
    lock_file = open(lock_path, "w")
    try:
        # Kilidi al — başka process kilitlediyse bekle
        fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
        print(f"🔒 Kilit alındı: {filepath}")
        yield filepath
    finally:
        # Kilidi bırak
        fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
        lock_file.close()
        # Lock dosyasını temizle (opsiyonel)
        try:
            os.remove(lock_path)
        except OSError:
            pass
        print(f"🔓 Kilit bırakıldı: {filepath}")

# Kullanım
with file_lock("/tmp/shared_data.txt") as path:
    with open(path, "a") as f:
        f.write("Bu satır güvenle yazıldı\n")

İç İçe Context Manager ve ExitStack

Birden fazla kaynağı aynı anda yönetmen gerektiğinde:

# İç içe with — basit ama derinleşince okunması zorlaşır
with open("input.txt", "r") as fin:
    with open("output.txt", "w") as fout:
        with open("log.txt", "a") as flog:
            # üç dosya birden açık
            data = fin.read()
            fout.write(data.upper())
            flog.write("Dönüşüm tamamlandı\n")

# Aynı satırda — Python 3.10+ parenthesized context managers
with (
    open("input.txt", "r") as fin,
    open("output.txt", "w") as fout,
    open("log.txt", "a") as flog,
):
    data = fin.read()
    fout.write(data.upper())
    flog.write("Dönüşüm tamamlandı\n")

Peki ya kaç tane kaynak yöneteceğin çalışma zamanında belli oluyorsa? ExitStack tam bunu çözer:

from contextlib import ExitStack

def process_files(input_paths, output_dir):
    """Dinamik sayıda dosyayı güvenle yönet."""
    with ExitStack() as stack:
        # Kaç dosya olduğu runtime'da belli
        input_files = [
            stack.enter_context(open(path, "r"))
            for path in input_paths
        ]
        
        output_files = [
            stack.enter_context(open(f"{output_dir}/{i}.txt", "w"))
            for i in range(len(input_paths))
        ]
        
        # Tüm dosyalar açık — işlemleri yap
        for fin, fout in zip(input_files, output_files):
            fout.write(fin.read().upper())
    
    # ExitStack with bloğundan çıkınca TÜM dosyaları kapatır
    # Açılma sırasının tersinde kapatır (LIFO — son açılan ilk kapanır)

ExitStack ayrıca callback fonksiyonları da kayıt edebilir:

from contextlib import ExitStack

with ExitStack() as stack:
    # Cleanup callback'i kaydet
    stack.callback(print, "3. Bu en son çalışır")
    stack.callback(print, "2. Bu ikinci çalışır")
    stack.callback(print, "1. Bu ilk çalışır (LIFO)")
    
    print("With bloğu içindeyiz")

# Çıktı:
# With bloğu içindeyiz
# 1. Bu ilk çalışır (LIFO)
# 2. Bu ikinci çalışır
# 3. Bu en son çalışır

contextlib'in Diğer Kullanışlı Araçları

suppress — Belirli Exception'ları Yut

from contextlib import suppress
import os

# ❌ Geleneksel yol
try:
    os.remove("temp.txt")
except FileNotFoundError:
    pass

# ✅ suppress ile — tek satır
with suppress(FileNotFoundError):
    os.remove("temp.txt")

redirect_stdout / redirect_stderr — Çıktıyı Yönlendir

from contextlib import redirect_stdout
import io

# stdout'u bir string buffer'a yönlendir
buffer = io.StringIO()
with redirect_stdout(buffer):
    print("Bu ekrana değil buffer'a yazılır")
    print("Bu da")

captured = buffer.getvalue()
print(f"Yakalanan çıktı: {captured}")
# Yakalanan çıktı: Bu ekrana değil buffer'a yazılır\nBu da\n

closing — close() metodu olan nesneleri with ile kullan

from contextlib import closing
from urllib.request import urlopen

# urlopen context manager değildir (eski Python versiyonlarında)
# closing() ile with kullanılabilir hale gelir
with closing(urlopen("https://example.com")) as page:
    content = page.read()

Async Context Manager (Python 3.10+)

Asenkron kodda da context manager kullanabilirsin. __aenter__ ve __aexit__ metodlarını implement et:

import aiohttp
import asyncio
from contextlib import asynccontextmanager

@asynccontextmanager
async def api_session(base_url, timeout=30):
    """Asenkron HTTP session yönetimi."""
    timeout_config = aiohttp.ClientTimeout(total=timeout)
    session = aiohttp.ClientSession(base_url, timeout=timeout_config)
    try:
        yield session
    finally:
        await session.close()

# Kullanım
async def fetch_data():
    async with api_session("https://api.example.com") as session:
        async with session.get("/users") as response:
            data = await response.json()
            return data

# asyncio.run(fetch_data())

async with ifadesi, with ile aynı mantıkta çalışır — ama await ile asenkron kaynak yönetimi yapar.

Yaygın Hatalar ve Tuzaklar

1. __enter__'dan self Yerine Yanlış Değer Döndürmek

# ❌ __enter__ None döndürüyor
class BadContext:
    def __enter__(self):
        print("Girdim")
        # return unutuldu — None döner!
    
    def __exit__(self, *args):
        print("Çıktım")

with BadContext() as ctx:
    print(ctx)  # None! — muhtemelen istediğin bu değil

__enter__ her zaman as değişkenine atanacak değeri döndürmeli. Genellikle return self veya yönetilen kaynağın kendisi (cursor, file handle, vb.) döndürülür.

2. __exit__'te Exception Yutmak

# ❌ TEHLİKELİ — tüm exception'ları yutar!
class SilentContext:
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            print(f"Hata oldu ama yuttum: {exc_val}")
        return True  # True = exception'ı bastır

with SilentContext():
    raise ValueError("Kritik hata!")
    # Bu hata sessizce yutulur — çok tehlikeli!

print("Hayat devam ediyor")  # Bu satır çalışır — bug gizlenir

__exit__'ten True döndürmek çok spesifik durumlar dışında yanlıştır. Hataları yutmak bug'ları gizler. Her zaman False veya None döndür, veya sadece belirli, bilinen exception türlerini bastır.

3. Generator Tabanlı Context Manager'da yield Sonrası Kodu finally'a Koymamak

from contextlib import contextmanager

# ❌ Exception olursa cleanup çalışmaz!
@contextmanager
def bad_resource():
    resource = acquire_resource()
    yield resource
    release_resource(resource)  # Exception fırlarsa buraya ulaşılmaz!

# ✅ finally ile garanti et
@contextmanager
def good_resource():
    resource = acquire_resource()
    try:
        yield resource
    finally:
        release_resource(resource)  # Her durumda çalışır

@contextmanager kullanırken cleanup kodunu her zaman try/finally içine koy. yield'den sonraki düz kod, exception durumunda çalışmaz.

4. Context Manager'ı with Bloğu Dışında Kullanmak

# ❌ Anlamsız — with olmadan context manager'ın faydası yok
db = DatabaseConnection("app.db")
cursor = db.__enter__()  # manuel çağrı — tehlikeli!
cursor.execute("SELECT 1")
# __exit__ çağrılmayabilir — kaynak sızıntısı!

# ✅ Her zaman with ile kullan
with DatabaseConnection("app.db") as cursor:
    cursor.execute("SELECT 1")

Best Practices

  1. Her zaman `with` kullan. Dosya, veritabanı bağlantısı, kilit, HTTP session — eğer kapanması/bırakılması gereken bir kaynak varsa with kullan.

  2. `contextlib` tercih et. Basit context manager'lar için sınıf yazmak yerine @contextmanager decorator'ını kullan. Daha az kod, daha okunabilir.

  3. `__exit__`'ten `True` döndürme. Exception bastırmak neredeyse her zaman yanlıştır. Sadece çok bilinçli, spesifik durumlar için kullan. (Örneğin, suppress() bile bunu sadece belirli exception türleri için yapar.)

  4. Cleanup kodunu `finally`'a koy. Generator tabanlı context manager'larda yield sonrası kod try/finally içinde olmalı. Sınıf tabanlında __exit__ zaten her durumda çağrılır.

  5. Dinamik kaynak sayısı için `ExitStack` kullan. Sabit sayıda kaynak için iç içe with veya virgüllü with yeterli. Dinamik durumlar için ExitStack çok daha temiz.

  6. `__enter__` anlamlı değer döndürsün. return self veya yönetilen kaynağın kendisi. None döndürmek genelde bir bug.

  7. Async kod için `async with` kullan. aiohttp, asyncpg gibi async kütüphaneler context manager destekler. @asynccontextmanager ile kendi async context manager'ını yaz.

Sonuç

Python'un context manager'ları kaynak yönetiminin en zarif çözümü. Bu yazıda öğrendiklerini özetleyelim:

  • `with` bloğu __enter__ ve __exit__ protokolünü kullanır — ne olursa olsun cleanup garanti edilir

  • Sınıf tabanlı context manager: __enter__ ve __exit__ metotlarını implement et

  • Generator tabanlı: @contextmanager ile daha az kodla context manager yaz

  • `ExitStack` ile dinamik sayıda kaynağı güvenle yönet

  • `suppress`, `redirect_stdout`, `closing` gibi hazır araçları tanı

  • Async context manager ile asenkron kaynak yönetimi yap

Context manager yazmak Pythonic kodun temel taşlarından biri. try/finally ile uğraşmak yerine, kaynağı with bloğuyla sar ve Python'un sihrine güven. Bir sonraki adım olarak kendi projendeki kaynak yönetim pattern'lerini context manager'a dönüştürmeyi dene — veritabanı bağlantıları, API session'ları, geçici dosyalar, kilitleme mekanizmaları. Her birine bir with yakışır.

Paylaş:
Son güncelleme: Jun 04, 2026

Yorumlar

Giriş yapın ve yorum bırakın.

Henüz yorum yok

Düşüncelerinizi paylaşan ilk siz olun!

Bu yazıyı beğendiniz mi?

Bültene abone olun ve yeni yazılardan ilk siz haberdar olun. Spam yok, söz.

İlgili Yazılar