Context Manager ve with Protokolü
Giriş: Otomatik Temizlik
Hayatında hiç bir odaya girip ışığı açtın, işini yaptın ve çıkarken ışığı kapatmayı unuttun mu? Muhtemelen evet. Yazılımda da aynı durum var: dosya açarsın ama kapatmayı unutursun, veritabanı bağlantısı kurarsın ama serbest bırakmayı atlarsın, kilit alırsın ama kilit açmayı ihmal edersin.
Context manager tam olarak bu sorunu çözer. "Odaya gir, işini yap, çıkarken ne olursa olsun ışığı kapat" garantisi verir. Python'daki with statement'ı bunu sağlar.
Bu derste with statement'ın nasıl çalıştığını, kendi context manager'larını nasıl yazacağını ve contextlib kütüphanesinin sunduğu kolaylıkları öğreneceksin.
Context Manager Nedir?
Analoji: Otel Odası
Bir otele gittiğini düşün. Resepsiyonda check-in yaparsın (kaynak edinme), odayı kullanırsın (işini yapma), check-out yaparken oda temizlenir ve anahtar geri alınır (temizlik). Oda temizliğini sen yapmazsın — otel sistemi otomatik halleder.
Context manager de böyle çalışır:
Giriş (
__enter__): Kaynağı edin (dosya aç, bağlantı kur)Kullanım: İşini yap
Çıkış (
__exit__): Ne olursa olsun kaynağı temizle (dosyayı kapat, bağlantıyı kes)
with Statement Olmadan
# Tehlikeli yol — dosya kapanmayabilir!
f = open("data.txt", "r")
content = f.read()
# Burada bir hata olursa? f.close() çalışmaz!
f.close()
# Daha güvenli ama çirkin
f = open("data.txt", "r")
try:
content = f.read()
finally:
f.close()with Statement ile
# Temiz ve güvenli yol
with open("data.txt", "r") as f:
content = f.read()
# Bloktan çıkınca dosya otomatik kapanır
# Hata olsa bile kapanır!with statement, try/finally bloğunun güzel ve okunaklı hali. Ama arka planda neler oluyor?
with Statement Nasıl Çalışır: __enter__ ve __exit__
Bir nesne with statement ile kullanılabilmek için iki özel metod implemente etmelidir:
__enter__(self)
withbloğuna girerken çağrılırDöndürdüğü değer
askeyword'ünden sonraki değişkene atanırGenellikle kaynağı hazırlar ve döndürür
__exit__(self, exc_type, exc_val, exc_tb)
withbloğundan çıkarken çağrılır (her durumda!)Hata olsa da çağrılır
Üç parametre alır: exception tipi, değeri ve traceback
Truedöndürürse exception yutulur,False/Nonedöndürürse exception yeniden fırlatılır
Akış Şeması
# with expression as variable:
# body
# Arka planda olan:
manager = expression # Context manager nesnesini al
value = manager.__enter__() # __enter__ çağır
variable = value # as değişkenine ata
try:
body # with bloğunun gövdesi
except Exception as e:
if not manager.__exit__(type(e), e, e.__traceback__):
raise # __exit__ True dönmediyse hatayı fırlat
else:
manager.__exit__(None, None, None) # Hata yoksa da çağrılırBu mekanizma sayesinde __exit__ her koşulda çağrılır. İşte context manager'ın güvenlik garantisi bu.
Kendi Context Manager Sınıfını Yazma
Örnek 1: Basit Timer
import time
class Timer:
"""Kod bloğunun çalışma süresini ölçer."""
def __enter__(self):
self.start = time.perf_counter()
return self # as değişkenine atanacak
def __exit__(self, exc_type, exc_val, exc_tb):
self.end = time.perf_counter()
self.elapsed = self.end - self.start
print(f"⏱️ Geçen süre: {self.elapsed:.4f} saniye")
return False # Exception'ları yutma
# Kullanım
with Timer():
total = sum(range(1_000_000))
print(f"Toplam: {total}")
# Çıktı:
# Toplam: 499999500000
# ⏱️ Geçen süre: 0.0312 saniye
# as ile süreye sonra erişme
with Timer() as t:
data = [x ** 2 for x in range(100_000)]
print(f"Sonra da erişebilirim: {t.elapsed:.4f}s")Örnek 2: Veritabanı Bağlantısı (Simülasyon)
class DatabaseConnection:
"""Veritabanı bağlantısı context manager'ı."""
def __init__(self, host, database):
self.host = host
self.database = database
self.connection = None
def __enter__(self):
print(f"🔌 {self.host}/{self.database} bağlantısı kuruluyor...")
self.connection = f"Connection({self.host}/{self.database})"
return self
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"🔌 Bağlantı kapatılıyor: {self.connection}")
self.connection = None
if exc_type is not None:
print(f"⚠️ Hata oluştu: {exc_val}")
return False # Hataları yutma
def query(self, sql):
if self.connection is None:
raise RuntimeError("Bağlantı yok!")
print(f"📊 SQL çalıştırılıyor: {sql}")
return [{"id": 1, "name": "Ali"}, {"id": 2, "name": "Veli"}]
# Kullanım
with DatabaseConnection("localhost", "mydb") as db:
results = db.query("SELECT * FROM users")
for row in results:
print(row)
# Çıktı:
# 🔌 localhost/mydb bağlantısı kuruluyor...
# 📊 SQL çalıştırılıyor: SELECT * FROM users
# {'id': 1, 'name': 'Ali'}
# {'id': 2, 'name': 'Veli'}
# 🔌 Bağlantı kapatılıyor: Connection(localhost/mydb)Örnek 3: Geçici Dizin
import os
import shutil
import tempfile
class TempDirectory:
"""Geçici dizin oluşturur, işlem bitince siler."""
def __init__(self, prefix="tmp_"):
self.prefix = prefix
self.path = None
def __enter__(self):
self.path = tempfile.mkdtemp(prefix=self.prefix)
print(f"📁 Geçici dizin oluşturuldu: {self.path}")
return self.path
def __exit__(self, exc_type, exc_val, exc_tb):
if self.path and os.path.exists(self.path):
shutil.rmtree(self.path)
print(f"🗑️ Geçici dizin silindi: {self.path}")
return False
# Kullanım
with TempDirectory(prefix="myapp_") as tmpdir:
filepath = os.path.join(tmpdir, "temp_data.txt")
with open(filepath, "w") as f:
f.write("Geçici veri")
print(f"Dosya oluşturuldu: {filepath}")
# Bloktan çıkınca dizin ve içindekiler silinirException Handling in __exit__
__exit__ metodu üç parametre alır:
exc_type: Exception'ın tipi (örn:ValueError)exc_val: Exception'ın değeri (hata mesajı)exc_tb: Traceback nesnesi
Eğer with bloğunda hata olmadıysa üçü de None olur.
Hataları Yutmak (Suppress)
class SuppressErrors:
"""Belirli hataları yutar."""
def __init__(self, *exception_types):
self.exception_types = exception_types
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
if issubclass(exc_type, self.exception_types):
print(f"🔇 Hata yutuldu: {exc_val}")
return True # True = exception'ı yut
return False # False = exception'ı fırlat
# ValueError yutulur
with SuppressErrors(ValueError, TypeError):
int("abc") # ValueError oluşur ama yutulur
print("Bu satır çalışmaz")
print("Program devam ediyor!") # Bu çalışır
# ZeroDivisionError yutulmaz
# with SuppressErrors(ValueError):
# 1 / 0 # ZeroDivisionError — yutulmaz, fırlatılır⚠️ Dikkat:
__exit__'tenTruedöndürmek exception'ı yok eder. Bunu dikkatli kullan! Hataları yutmak genellikle kötü bir pratik — sadece gerçekten istediğin durumlarda yap.
Temizlik + Hata Raporlama
class ManagedResource:
"""Temizlik yapan ve hataları raporlayan context manager."""
def __init__(self, name):
self.name = name
def __enter__(self):
print(f"✅ {self.name} açıldı")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
# Temizlik HER DURUMDA yapılır
print(f"🧹 {self.name} temizleniyor...")
if exc_type is not None:
print(f"❌ Hata tespit edildi: {exc_type.__name__}: {exc_val}")
# False döndür = hata yeniden fırlatılır
print(f"✅ {self.name} kapatıldı")
return False
with ManagedResource("Dosya") as r:
print("İşlem yapılıyor...")
# raise ValueError("Bir şeyler ters gitti!")@contextmanager Decorator: Daha Kolay Yol
Sınıf yazmak bazen fazla zahmetli olabiliyor. contextlib modülündeki @contextmanager decorator'ı, generator fonksiyonla context manager yazmana izin verir.
Temel Kullanım
from contextlib import contextmanager
@contextmanager
def timer():
"""Generator tabanlı timer context manager."""
import time
start = time.perf_counter()
yield # Burası with bloğunun çalıştığı yer
end = time.perf_counter()
print(f"⏱️ Geçen süre: {end - start:.4f} saniye")
with timer():
total = sum(range(1_000_000))Nasıl Çalışır?
@contextmanager ile dekore edilen generator fonksiyon:
yield'den önce:__enter__gibi çalışıryieldedilen değer:asdeğişkenine atanıryield'den sonra:__exit__gibi çalışır
@contextmanager
def managed_file(filename, mode="r"):
"""Dosya context manager'ı — generator ile."""
print(f"📂 Dosya açılıyor: {filename}")
f = open(filename, mode)
try:
yield f # f, as değişkenine atanır
finally:
f.close()
print(f"📂 Dosya kapatıldı: {filename}")
with managed_file("data.txt", "w") as f:
f.write("Merhaba Dünya!")Hata Yönetimi
try/finally kullanmak önemli — yoksa hata durumunda yield'den sonraki kod çalışmaz:
@contextmanager
def safe_resource(name):
"""Hata durumunda da temizlik yapan context manager."""
print(f"✅ {name} açıldı")
try:
yield name
except Exception as e:
print(f"❌ Hata yakalandı: {e}")
raise # Hatayı yeniden fırlat (yutmak istemiyorsak)
finally:
print(f"🧹 {name} temizlendi")
with safe_resource("Veritabanı") as db:
print(f"Kullanıyorum: {db}")
# raise RuntimeError("Bağlantı koptu!")Timer — Gelişmiş Versiyon
@contextmanager
def timer(label="İşlem"):
"""Etiketli timer."""
import time
start = time.perf_counter()
try:
yield
finally:
elapsed = time.perf_counter() - start
print(f"⏱️ {label}: {elapsed:.4f}s")
with timer("Liste oluşturma"):
data = [x ** 2 for x in range(1_000_000)]
with timer("Generator toplama"):
total = sum(x ** 2 for x in range(1_000_000))contextlib Utilities
contextlib modülü çok faydalı araçlar sunar. Hepsini bilmen gerekmez ama en önemlilerini görelim.
suppress: Belirli Hataları Yut
from contextlib import suppress
# Bu:
with suppress(FileNotFoundError):
os.remove("olmayan_dosya.txt")
# Şuna eşdeğer:
try:
os.remove("olmayan_dosya.txt")
except FileNotFoundError:
passsuppress çok temiz ve okunaklı. "Bu hata olabilir, önemli değil" demenin şık yolu.
redirect_stdout: Çıktıyı Yönlendir
from contextlib import redirect_stdout
import io
# Çıktıyı string'e yönlendir
f = io.StringIO()
with redirect_stdout(f):
print("Bu ekrana değil, string'e yazılır")
print("Bu da")
output = f.getvalue()
print(f"Yakalanan çıktı: {output!r}")
# Çıktıyı dosyaya yönlendir
with open("output.txt", "w") as f:
with redirect_stdout(f):
print("Bu dosyaya yazılır")
help(list) # help() çıktısı da dosyaya giderredirect_stderr: Hata Çıktısını Yönlendir
from contextlib import redirect_stderr
import io
f = io.StringIO()
with redirect_stderr(f):
import warnings
warnings.warn("Test uyarısı")
print(f"Yakalanan: {f.getvalue()}")closing: close() Metodu Olan Nesneler İçin
from contextlib import closing
from urllib.request import urlopen
# urlopen context manager değil (eski Python'larda)
# closing() ile sarmalayabiliriz
with closing(urlopen("https://example.com")) as page:
content = page.read()ExitStack: Dinamik Context Manager Yığını
from contextlib import ExitStack
# Dinamik sayıda dosya açma
filenames = ["file1.txt", "file2.txt", "file3.txt"]
with ExitStack() as stack:
files = [
stack.enter_context(open(fname, "w"))
for fname in filenames
]
for f in files:
f.write("Merhaba!\n")
# Tüm dosyalar otomatik kapanırExitStack özellikle dosya sayısının runtime'da belirlenmesi gereken durumlarda kullanışlı.
Nested Context Managers
Tek Satırda Birden Fazla Context Manager
Python 3.1'den itibaren birden fazla context manager'ı tek with statement'ında kullanabilirsin:
# Dosya kopyalama — iki dosya aynı anda açık
with open("kaynak.txt", "r") as src, open("hedef.txt", "w") as dst:
for line in src:
dst.write(line.upper())Python 3.10'dan itibaren parantezli syntax de var:
# Python 3.10+ parantezli syntax
with (
open("kaynak.txt", "r") as src,
open("hedef.txt", "w") as dst,
Timer() as t,
):
for line in src:
dst.write(line.upper())İç İçe with Blokları
# Bazen iç içe yazmak daha okunaklı
with DatabaseConnection("localhost", "users_db") as db:
users = db.query("SELECT * FROM users")
with open("users_export.csv", "w") as f:
f.write("id,name\n")
for user in users:
f.write(f"{user['id']},{user['name']}\n")💡 İpucu: Eğer context manager'lar birbirine bağımlı değilse tek satırda yaz. Bağımlıysa (biri diğerinin sonucunu kullanıyorsa) iç içe yaz.
Pratik: İleri Örnekler
Çalışma Dizini Değiştirici
import os
from contextlib import contextmanager
@contextmanager
def change_directory(path):
"""Geçici olarak çalışma dizinini değiştirir."""
original = os.getcwd()
try:
os.chdir(path)
print(f"📁 Dizin değiştirildi: {path}")
yield path
finally:
os.chdir(original)
print(f"📁 Dizin geri alındı: {original}")
with change_directory("/tmp"):
print(f"Şu an: {os.getcwd()}")
# /tmp içinde işlemler yap
print(f"Geri döndük: {os.getcwd()}")Ortam Değişkeni Geçici Değiştirici
@contextmanager
def temp_env(**kwargs):
"""Geçici olarak ortam değişkenlerini değiştirir."""
old_values = {}
for key, value in kwargs.items():
old_values[key] = os.environ.get(key)
os.environ[key] = value
try:
yield
finally:
for key, old_value in old_values.items():
if old_value is None:
del os.environ[key]
else:
os.environ[key] = old_value
with temp_env(DEBUG="1", DATABASE_URL="sqlite:///test.db"):
print(os.environ["DEBUG"]) # 1
print(os.environ["DATABASE_URL"]) # sqlite:///test.db
# Blok dışında eski değerler geri gelirTransaction Manager
@contextmanager
def transaction(connection):
"""Veritabanı transaction yönetimi."""
cursor = connection.cursor()
try:
yield cursor
connection.commit()
print("✅ Transaction commit edildi")
except Exception as e:
connection.rollback()
print(f"❌ Transaction rollback yapıldı: {e}")
raise
# Kullanım (pseudo-code)
# with transaction(db_connection) as cursor:
# cursor.execute("INSERT INTO users VALUES (1, 'Ali')")
# cursor.execute("INSERT INTO orders VALUES (1, 1, 100)")
# Her iki INSERT ya birlikte başarılı olur, ya birlikte geri alınırİzinleri Geçici Değiştirici
@contextmanager
def temp_permissions(filepath, new_mode):
"""Dosya izinlerini geçici olarak değiştirir."""
import stat
original_mode = os.stat(filepath).st_mode
try:
os.chmod(filepath, new_mode)
yield
finally:
os.chmod(filepath, original_mode)
# Dosyayı geçici olarak yazılabilir yap
# with temp_permissions("config.ini", 0o644):
# with open("config.ini", "w") as f:
# f.write("new_config=value")Context Manager Tasarım Desenleri
1. Setup/Teardown Pattern
En yaygın kullanım: bir kaynağı aç, kullan, kapat.
@contextmanager
def smtp_connection(host, port):
"""Email bağlantısı aç/kapat."""
import smtplib
server = smtplib.SMTP(host, port)
try:
server.starttls()
yield server
finally:
server.quit()2. State Değiştirme Pattern
Bir durumu geçici olarak değiştir, sonra geri al.
@contextmanager
def precision(decimal_places):
"""Geçici olarak ondalık hassasiyeti değiştir."""
import decimal
old_context = decimal.getcontext().copy()
decimal.getcontext().prec = decimal_places
try:
yield
finally:
decimal.setcontext(old_context)3. Loglama/Ölçüm Pattern
Bir işlemin başlangıç ve bitişini logla.
@contextmanager
def log_operation(operation_name):
"""İşlemi logla."""
import logging
logger = logging.getLogger(__name__)
logger.info(f"🚀 {operation_name} başladı")
try:
yield
logger.info(f"✅ {operation_name} tamamlandı")
except Exception as e:
logger.error(f"❌ {operation_name} başarısız: {e}")
raise4. Lock Pattern
Thread güvenliği için kilit al/bırak.
import threading
@contextmanager
def timed_lock(lock, timeout=5.0):
"""Zaman aşımlı kilit."""
acquired = lock.acquire(timeout=timeout)
if not acquired:
raise TimeoutError(f"Kilit {timeout}s içinde alınamadı")
try:
yield
finally:
lock.release()
lock = threading.Lock()
with timed_lock(lock, timeout=3.0):
# Kritik bölge
passAsync Context Manager
Python 3.5+ ile async context manager'lar da var. __aenter__ ve __aexit__ kullanır:
class AsyncTimer:
async def __aenter__(self):
import time
self.start = time.perf_counter()
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
import time
self.elapsed = time.perf_counter() - self.start
print(f"⏱️ Async süre: {self.elapsed:.4f}s")
return False
# Kullanım:
# async with AsyncTimer():
# await some_async_operation()contextlib ile de yazılabilir:
from contextlib import asynccontextmanager
@asynccontextmanager
async def async_db_connection(url):
"""Async veritabanı bağlantısı."""
print(f"🔌 Bağlanıyor: {url}")
connection = await create_connection(url) # async bağlantı
try:
yield connection
finally:
await connection.close()
print("🔌 Bağlantı kapatıldı")Yaygın Hatalar
1. __exit__'te Temizliği Unutmak
# YANLIŞ: Hata olursa temizlik yapılmaz
class BadManager:
def __exit__(self, *args):
if args[0] is None: # Sadece hata yoksa temizle
self.cleanup()
# Hata varsa temizlik YOK!
# DOĞRU: Her durumda temizle
class GoodManager:
def __exit__(self, *args):
self.cleanup() # Her durumda temizle
return False2. yield Sonrasında finally Kullanmamak
# YANLIŞ: Hata olursa temizlik yapılmaz
@contextmanager
def bad_manager():
resource = acquire()
yield resource
release(resource) # Hata olursa buraya ulaşılmaz!
# DOĞRU
@contextmanager
def good_manager():
resource = acquire()
try:
yield resource
finally:
release(resource) # Her durumda çalışır3. as Değişkenini Bloğun Dışında Kullanmak
with open("data.txt") as f:
content = f.read()
# f hala erişilebilir ama KAPALI
# f.read() # ValueError: I/O operation on closed file
# content kullanılabilir — o bir string, dosya değil
print(content) # Bu çalışırÖzet
Context manager,
withstatement ile kaynak yönetimini otomatikleştiren bir protokoldür.__enter__giriş,__exit__çıkış işlemlerini tanımlar.`with` statement,
try/finallybloğunun okunaklı alternatifidir. Hata olsa bile__exit__her zaman çağrılır.Sınıf tabanlı context manager
__enter__ve__exit__metodları ile, generator tabanlı context manager@contextmanagerdecorator'ı ile yazılır.`__exit__` metodu
Truedöndürürse exception yutulur. Dikkatli kullan — genellikleFalsedöndürmek (hatayı fırlatmak) daha güvenlidir.`contextlib` modülü
suppress,redirect_stdout,closing,ExitStackgibi kullanışlı araçlar sunar.Context manager'lar dosya, veritabanı, kilit, geçici durum değişikliği ve ölçüm gibi kaynak yönetimi senaryolarında vazgeçilmezdir.
AI Asistan
Sorularını yanıtlamaya hazır