Ana içeriğe geç
Kesit Python
Orta · Pythonic kod

Dekoratörler (decorators) ve bağlam yöneticileri (context managers)

Fonksiyonu saran fonksiyon fikrinden dekoratöre, functools.wraps ve parametreli dekoratöre geçer; zamanlama, loglama, yeniden deneme ve lru_cache yazar; with, contextlib.contextmanager ve sınıf tabanlı bağlam yöneticisi kurarsın.

Uygulamada aç, 23 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

Neden önemli?

Ilgaz Bank'ın işlem servisinde on farklı fonksiyon var: para transferi, bakiye sorgusu, kredi başvurusu... Hepsinde aynı üç şey isteniyor: çağrı loglansın, süresi ölçülsün, geçici hata olursa yeniden denensin. Bu üç davranışı on fonksiyona tek tek kopyalamak hem tekrar hem bakım kâbusudur. Dekoratör (decorator), bir fonksiyonu alıp etrafına davranış ekleyen ve yeni fonksiyon döndüren bir araçtır; @zamanla yazarsın, iş biter. Bağlam yöneticisi (context manager) ise aynı fikrin "bir blok öncesi ve sonrası" sürümüdür: dosyayı aç ve ne olursa olsun kapat, bağlantıyı kur ve bırak, işlemi başlat ve hata olursa geri al. İkisi de önceki dersteki closure ve "fonksiyon bir değerdir" fikrinin üstüne kuruludur.

Fonksiyonu saran fonksiyon

Dekoratör mekanizmasını önce elle kur. Bir fonksiyon alan, içinde onu çağıran yeni bir fonksiyon tanımlayan ve bu yeni fonksiyonu döndüren bir fonksiyon yaz.

def logla(fonk):
    def sarici(*args, **kwargs):
        print(f"-> {fonk.__name__} çağrıldı, args={args}")
        sonuc = fonk(*args, **kwargs)
        print(f"<- {fonk.__name__} bitti, sonuç={sonuc}")
        return sonuc
    return sarici

def transfer(kaynak, hedef, tutar):
    return f"{kaynak}->{hedef}: {tutar}"

transfer = logla(transfer)      # elle sarmalama
print(transfer("TR1", "TR2", 500))

Üç parça: sarici her türlü argümanı kabul etsin diye *args, **kwargs alır; asıl fonksiyonu çağırıp sonucunu döndürür (unutulursa dekore edilen fonksiyon None vermeye başlar); logla sarıcıyı döndürür. transfer = logla(transfer) satırı, ismi yeni fonksiyona bağlar. Python bu satırı yazmanın kısa yolunu sunar:

def logla(fonk):
    def sarici(*args, **kwargs):
        print(f"-> {fonk.__name__}")
        return fonk(*args, **kwargs)
    return sarici

@logla
def bakiye_sorgu(hesap):
    return 12500

print(bakiye_sorgu("TR1"))
print(bakiye_sorgu.__name__)   # sarici! asıl adı kayboldu

@logla, tam olarak bakiye_sorgu = logla(bakiye_sorgu) demektir; def biter bitmez uygulanır. Son satır bir sorunu gösteriyor: dekore edilen fonksiyonun adı ve docstring'i sarıcınınkiyle değişti. Hata izlerinde, yardım metinlerinde ve testlerde bu kafa karıştırır.

functools.wraps: kimliği koru

from functools import wraps

def logla(fonk):
    @wraps(fonk)
    def sarici(*args, **kwargs):
        print(f"-> {fonk.__name__}")
        return fonk(*args, **kwargs)
    return sarici

@logla
def bakiye_sorgu(hesap):
    """Hesabın güncel bakiyesini döndürür."""
    return 12500

print(bakiye_sorgu.__name__)
print(bakiye_sorgu.__doc__)
print(bakiye_sorgu.__wrapped__("TR1"))   # dekoratörsüz asıl fonksiyon
KuralHer dekoratörün sarıcısına @wraps(fonk) koy. İstisnası yoktur; unutmak kod incelemesinde (code review) kesin bir not getirir.

Parametreli dekoratör: üç katman

"3 kez yeniden dene" gibi ayar alan dekoratör için bir katman daha gerekir: dıştaki fonksiyon ayarı alır ve asıl dekoratörü döndürür. @yeniden_dene(3) önce yeniden_dene(3)'ü çağırır, dönen dekoratör fonksiyona uygulanır.

from functools import wraps

def yeniden_dene(deneme):
    def dekorator(fonk):
        @wraps(fonk)
        def sarici(*args, **kwargs):
            son_hata = None
            for i in range(1, deneme + 1):
                try:
                    return fonk(*args, **kwargs)
                except ConnectionError as e:
                    son_hata = e
                    print(f"  deneme {i} başarısız: {e}")
            raise son_hata
        return sarici
    return dekorator

sayac = {"cagri": 0}

@yeniden_dene(3)
def banka_servisi():
    sayac["cagri"] += 1
    if sayac["cagri"] < 3:
        raise ConnectionError("zaman aşımı")
    return "OK"

print(banka_servisi())
print("toplam çağrı:", sayac["cagri"])

Katmanları ezberleme, oku: ayar → dekoratör → sarıcı. Sarıcı asıl işi yapar; dekoratör sarıcıyı üretir; en dıştaki katman ayarı closure'a hapseder. Gerçek sistemlerde denemeler arasına time.sleep ile artan bekleme konur; burada çıktıyı sade tutmak için atlandı.

İş hayatından dört dekoratör

Zamanlama

import time
from functools import wraps

def zamanla(fonk):
    @wraps(fonk)
    def sarici(*args, **kwargs):
        t0 = time.perf_counter()
        try:
            return fonk(*args, **kwargs)
        finally:
            sure = time.perf_counter() - t0
            print(f"{fonk.__name__}: {sure:.4f} sn")
    return sarici

@zamanla
def toplam_ciro(siparisler):
    return sum(s["tutar"] for s in siparisler)

veri = [{"tutar": i % 500} for i in range(200_000)]
print(toplam_ciro(veri))

Ölçülen süre her çalıştırmada farklı çıkar; bu doğaldır. finally sayesinde fonksiyon hata fırlatsa da süre basılır. time.perf_counter, süre ölçümü için time.time'dan daha hassastır.

Önbellek: functools.lru_cache

Aynı argümanla tekrar tekrar çağrılan pahalı bir fonksiyonun sonucunu hatırlamak için dekoratör yazmana gerek yok; standart kütüphanede hazır.

from functools import lru_cache

hesap_sayisi = {"n": 0}

@lru_cache(maxsize=128)
def kur_sorgula(para_birimi):
    hesap_sayisi["n"] += 1       # "pahalı" servis çağrısını temsil eder
    return {"USD": 32.5, "EUR": 35.1}[para_birimi]

for pb in ["USD", "EUR", "USD", "USD", "EUR"]:
    kur_sorgula(pb)
print("gerçek hesap:", hesap_sayisi["n"])
print(kur_sorgula.cache_info())
kur_sorgula.cache_clear()

@lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(80))
lru_cache'in sınırlarıArgümanlar hashlenebilir olmalı: liste ya da sözlük geçiremezsin (demet ya da frozenset kullan). Sonuç zamanla değişiyorsa (kur, stok) önbellek bayatlar; süreli önbellek için ayrı çözüm gerekir. Ve maxsize=None sınırsız bellek demektir; uzun süren süreçlerde sınır koy.

Loglama ve yetki kontrolü

Dekoratörler üst üste konabilir; en yakındaki önce uygulanır. Aşağıda yetki_gerekli önce transfer'i sarar, sonra logla o sarılmış hali sarar.

from functools import wraps

def logla(fonk):
    @wraps(fonk)
    def sarici(*args, **kwargs):
        print(f"[LOG] {fonk.__name__} {kwargs}")
        return fonk(*args, **kwargs)
    return sarici

def yetki_gerekli(fonk):
    @wraps(fonk)
    def sarici(*args, kullanici, **kwargs):
        if kullanici.get("rol") != "yonetici":
            raise PermissionError(f"{kullanici['ad']} yetkisiz")
        return fonk(*args, kullanici=kullanici, **kwargs)
    return sarici

@logla
@yetki_gerekli
def transfer(tutar, *, kullanici):
    return f"{tutar} TL gönderildi"

print(transfer(500, kullanici={"ad": "Ayşe", "rol": "yonetici"}))
try:
    transfer(500, kullanici={"ad": "Ali", "rol": "stajyer"})
except PermissionError as e:
    print("Hata:", e)
İhtiyaçAraçNot
Çağrıları kaydetKendi logla dekoratörünGerçekte logging modülüne yazar
Süre ölçzamanla + perf_counterfinally ile hata durumunda da ölç
Geçici hatada tekraryeniden_dene(n)Yalnız geçici hataları yakala; mantık hatasını tekrarlama
Sonucu hatırlafunctools.lru_cacheHashlenebilir argüman; bayatlama riski
Kimlik korufunctools.wrapsHer sarıcıda zorunlu
Blok öncesi/sonrasıBağlam yöneticisiKaynak aç/kapat, işlem başlat/bitir

with ve bağlam yöneticileri

with open(...) as f: yazdığında Python iki şey garanti eder: bloğa girerken __enter__ çağrılır (dosya açılır, dönen değer f olur), bloktan çıkarken, hata olsa bile, __exit__ çağrılır (dosya kapanır). Bu "kur-kullan-temizle" kalıbı dosyadan çok daha geniş: veritabanı işlemi, kilit, geçici ayar, zaman ölçümü.

contextlib.contextmanager: üreteçten bağlam yöneticisi

En kısa yol, tek yield içeren bir üreteç (generator) fonksiyonunu @contextmanager ile süslemek. yield'den öncesi girişte, sonrası çıkışta çalışır; yield ettiğin değer as ile alınır.

from contextlib import contextmanager

@contextmanager
def banka_islemi(hesap):
    print("işlem başladı, bakiye:", hesap["bakiye"])
    yedek = hesap["bakiye"]
    try:
        yield hesap
        print("işlem onaylandı, bakiye:", hesap["bakiye"])
    except Exception as e:
        hesap["bakiye"] = yedek
        print(f"hata ({e}); geri alındı, bakiye: {hesap['bakiye']}")

hesap = {"bakiye": 1000}

with banka_islemi(hesap) as h:
    h["bakiye"] -= 300

with banka_islemi(hesap) as h:
    h["bakiye"] -= 900
    if h["bakiye"] < 0:
        raise ValueError("yetersiz bakiye")

print("son bakiye:", hesap["bakiye"])

try/except yield'i sarar; blok içinde fırlayan hata, üreteçte tam yield satırında ortaya çıkar. Burada hatayı yutup geri alma yaptık; hatanın yukarı devam etmesini istiyorsan except bloğunun sonunda raise yaz ya da try/finally kullan.

Sınıf tabanlı bağlam yöneticisi

Durum karmaşıksa ya da nesnenin başka metotları da olacaksa __enter__ ve __exit__ metotlu bir sınıf yazarsın. __exit__ üç argüman alır (hata türü, hata nesnesi, iz); hata yoksa üçü de None'dır. True döndürürse hata bastırılır.

import time

class Kronometre:
    def __init__(self, etiket):
        self.etiket = etiket
        self.sure = None

    def __enter__(self):
        self.t0 = time.perf_counter()
        return self

    def __exit__(self, hata_turu, hata, iz):
        self.sure = time.perf_counter() - self.t0
        durum = "hata ile" if hata_turu else "başarıyla"
        print(f"{self.etiket} {durum} bitti")
        return False   # hatayı bastırma

with Kronometre("rapor") as k:
    toplam = sum(range(300_000))
print("süre ölçüldü mü:", k.sure is not None, "| toplam:", toplam)

try:
    with Kronometre("hatalı iş"):
        raise ValueError("beklenmedik")
except ValueError as e:
    print("dışarıda yakalandı:", e)
Birden çok bağlamwith A() as a, B() as b: ikisini sırayla açar ve ters sırayla kapatır. Python 3.10 ve sonrasında parantezle çok satıra bölebilirsin. contextlib'de ayrıca suppress(FileNotFoundError) (hatayı sessizce yut), redirect_stdout ve ExitStack gibi hazır yöneticiler vardır.
Derinleş

Derinleş: dekoratör ve bağlam yöneticisinin ince işleri

Sarıcı fonksiyonu, functools.wraps'u, parametreli dekoratörü, lru_cache'i ve bağlam yöneticilerini gördün. Bu bölüm, çerçevelerin (Flask, pytest, Click) içinde her gün karşılaştığın ama nadiren açıklanan noktaları topluyor: dekoratörün ne zaman çalıştığı, kayıt (registry) dekoratörleri, üst üste dekoratörlerin çalışma sırası, hem parantezli hem parantezsiz kullanılabilen dekoratörler, önbelleği yönetmek ve contextlib'in az bilinen araçları. Sınıfın içindeki @property, @staticmethod gibi yerleşik dekoratörler Kapsülleme ve özellikler dersinde, metasınıflar ve sınıf dekoratörlerinin derini Metaprogramlama dersinde.

Dekoratör tanım anında çalışır

@dekorator satırı fonksiyon tanımlanırken, yani modül yüklenirken (import anında) bir kez çalışır; sarıcı ise her çağrıda çalışır. Bu fark sayesinde "kayıt dekoratörü" yazabilirsin: fonksiyonu hiç sarmadan bir sözlüğe ekler ve aynen geri döndürür. Flask'ın @app.route("/")'u ve pytest'in fikstürleri bu kalıbı kullanır.

RAPORLAR = {}

def rapor(ad):
    def kaydet(fonk):
        print(f"kayıt: {ad} → {fonk.__name__}")
        RAPORLAR[ad] = fonk
        return fonk                 # sarmadan aynen döndür
    return kaydet

@rapor("ciro")
def ciro_raporu(veri):
    return sum(veri)

@rapor("adet")
def adet_raporu(veri):
    return len(veri)

print("Program başladı, kayıtlı raporlar:", list(RAPORLAR))
for ad in ["adet", "ciro"]:
    print(ad, "=", RAPORLAR[ad]([120, 80, 300]))
if/elif yerine kayıtYeni bir rapor eklemek için ana programa dokunmazsın; yeni fonksiyonu yazar, üstüne @rapor("yeni") koyarsın. Ana program yalnız RAPORLAR sözlüğünü dolaşır.

Üst üste dekoratörler: içten dışa sarar, dıştan içe çalışır

@a üstte, @b altta ise sonuç a(b(f)) olur: önce b sarar, sonra a onu sarar. Çağrı geldiğinde ise en dıştaki (a) sarıcı ilk çalışır. Bu yüzden sıra anlamlıdır: yetki kontrolü önbellekten önce (daha dışta) olmalı, yoksa yetkisiz kullanıcı önbellekteki sonucu alır.

from functools import wraps

def etiket(ad):
    def dekorator(fonk):
        @wraps(fonk)
        def sarici(*args, **kwargs):
            print(f"{ad} girdi")
            sonuc = fonk(*args, **kwargs)
            print(f"{ad} çıktı")
            return sonuc
        return sarici
    return dekorator

@etiket("DIŞ")
@etiket("İÇ")
def hesapla():
    print("  asıl iş")
    return 42

print(hesapla())

Parantezli de parantezsiz de çalışan dekoratör

Parametreli dekoratörde @logla yazıp parantezi unutmak sık hatadır. Bazı kütüphaneler iki kullanımı da kabul eder: @logla ve @logla(onek="BANKA"). Hile: ilk parametre fonksiyon olarak gelirse doğrudan sar, gelmezse (yalnız anahtar sözcüklü argümanlar) dekoratör döndür. Ayarların * sonrasında yalnız anahtar sözcükle verilmesi bu ayrımı güvenli kılar.

from functools import wraps

def logla(fonk=None, *,):
    def dekorator(f):
        @wraps(f)
        def sarici(*args, **kwargs):
            print(f"[{onek}] {f.__name__}{args}")
            return f(*args, **kwargs)
        return sarici
    if fonk is None:            # @logla(onek=...) biçimi
        return dekorator
    return dekorator(fonk)      # @logla biçimi

@logla
def topla(a, b):
    return a + b

@logla(onek="BANKA")
def faiz(anapara, oran):
    return round(anapara * oran, 2)

print(topla(2, 3), faiz(10000, 0.035))

Önbelleği yönetmek

@lru_cache(maxsize=128) sınırlı sayıda sonuç tutar ve en uzun süredir kullanılmayanı atar; @functools.cache (3.9+) sınırsız sürümdür. Dekore edilen fonksiyona iki metot eklenir: cache_info() isabet (hit) ve ıska (miss) sayılarını verir, cache_clear() önbelleği boşaltır. Kur tablosu gibi zamanla değişen veride önbelleği gün başında temizlemek gerekir.

from functools import lru_cache

@lru_cache(maxsize=2)
def kur(birim):
    print("  servisten çekildi:", birim)
    return {"USD": 34.2, "EUR": 37.1, "GBP": 44.0}[birim]

for b in ["USD", "USD", "EUR", "GBP", "USD"]:
    kur(b)
print(kur.cache_info())
kur.cache_clear()
print("Temizlendi:", kur.cache_info().currsize)
Önbellek ve değiştirilebilir sonuçÖnbellekli fonksiyon liste ya da sözlük döndürüyorsa her çağrı aynı nesneyi verir. Çağıranlardan biri onu değiştirirse sonraki herkes bozuk veriyi alır. Değiştirilemez tür (tuple, frozenset) döndür ya da çağıran taraf kopya alsın. Argümanların da hashlenebilir olması gerekir: liste değil demet ver.

contextlib araç kutusu

AraçNe yapar?Örnek kullanım
@contextmanagerÜreteçten bağlam yöneticisiGeçici ayar, zamanlama
suppress(*hatalar)Belirtilen hataları sessizce yutarwith suppress(FileNotFoundError): os.remove(y)
ExitStackSayısı çalışma anında belli olan bağlamları tek yerde toplarKlasördeki tüm dosyaları aynı anda açmak
redirect_stdout(f)print çıktısını başka akışa yönlendirirEski fonksiyonun çıktısını yakalamak
closing(nesne)Çıkışta nesne.close() çağırırwith desteklemeyen eski bağlantılar
nullcontext()Hiçbir şey yapmayan bağlamKoşullu kilit/işlem

Sınıf tabanlı bağlam yöneticisinde __exit__'in dönüş değeri önemlidir: True döndürürse bloktaki hata yutulur, False/None ise hata yukarı çıkar. suppress tam olarak bunu yapar.

import io
from contextlib import suppress, ExitStack, redirect_stdout

class HataSay:
    def __init__(self, *turler):
        self.turler, self.sayi = turler, 0
    def __enter__(self):
        return self
    def __exit__(self, tur, deger, iz):
        if tur is not None and issubclass(tur, self.turler):
            self.sayi += 1
            return True           # hatayı yut
        return False

sayac = HataSay(ValueError)
for ham in ["10", "x", "7", "?"]:
    with sayac:
        print("geçerli:", int(ham))
print("yutulan hata:", sayac.sayi)

with suppress(KeyError):
    {}["yok"]
print("suppress sonrası devam")

with ExitStack() as yigin:
    dosyalar = [yigin.enter_context(io.StringIO(f"şube {i}")) for i in range(3)]
    print([f.read() for f in dosyalar])

tampon = io.StringIO()
with redirect_stdout(tampon):
    print("bu ekrana değil tampona")
print("yakalandı:", tampon.getvalue().strip())
Gerçek işteKurumsal kodda dekoratörler çoğu zaman sen yazmadan önce oradadır: web çerçevesinde @app.route ve @login_required, testte @pytest.fixture ve @pytest.mark.parametrize, komut satırında @click.command, veri hatlarında @task. Bu kalıpların hepsi bu bölümdeki iki fikre dayanır: kayıt dekoratörü (tanım anında listeye ekle) ve sarıcı (her çağrıda önce/sonra iş yap). Kendi yazacağın dekoratörler genelde üç türdür: tekrar deneme, zamanlama/loglama ve yetki kontrolü. Her birine @wraps eklemeyi unutursan loglarda bütün fonksiyonların adı "sarici" görünür ve hata ayıklamak zorlaşır.
Uzmanlaş

Sınıf tabanlı dekoratör ve metot dekoratörleri

Dekoratör, çağrılabilir herhangi bir şey olabilir; __call__ metodu olan bir sınıf da. Çağrı sayısı gibi durumu tutmak için bu yol closure'dan daha açıktır. Sınıf metotlarını dekore ederken self sarıcının *args'ının ilk öğesi olarak gelir; bu yüzden *args, **kwargs kalıbı metotlarda da sorunsuz çalışır. @property, @staticmethod, @classmethod ve @dataclass zaten her gün kullandığın yerleşik dekoratörlerdir; OOP derslerinde ayrıntıya gireceksin.

Dekoratörün gizli maliyeti

Her dekoratör bir katman ekler; çok katmanlı dekoratör yığınlarında hata izleri uzar ve davranış "sihirli" görünmeye başlar. Ekip kuralı genellikle şudur: dekoratör yalnız fonksiyonun işinden bağımsız çapraz kaygıları (logging, cache, retry, yetki, ölçüm) taşısın; iş mantığı dekoratöre sızmasın. Test ederken fonk.__wrapped__ ile dekoratörsüz hale erişebilmen için wraps şarttır.

İş hayatında nasıl görünür?

Web çerçevelerinde @app.get("/siparis") bir fonksiyonu URL'ye bağlar; pytest'te @pytest.fixture ve @pytest.mark.parametrize test davranışını şekillendirir; tenacity gibi paketler burada yazdığın yeniden deneme dekoratörünün üretim sürümüdür. Bağlam yöneticisi tarafında veritabanı kütüphanelerinin with conn: (otomatik commit/rollback), threading.Lock'un with kilit: kullanımı ve tempfile.TemporaryDirectory aynı kalıptır. Bir kaynağı açıp kapatan her kodu gördüğünde "bu bir bağlam yöneticisi olmalı" refleksi, Python'da olgunluğun işaretidir.

Alıştırmalara geç (23)