Ana içeriğe geç
Kesit Python
Orta · Nesne yönelimli programlama

Hata tasarımı: özel istisnalar, logging, EAFP

Kendi istisna hiyerarşini kurar, raise from ile nedeni korur, logging ile iz bırakır, EAFP ve assert'i doğru yerde kullanırsın.

Uygulamada aç, 22 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?

Temel seviyede try/except ile hatayı yakaladın. Ama Ilgaz Bank'ın transfer servisinde "bir hata oldu" demek yetmez: yetersiz bakiye mi, hesap bulunamadı mı, günlük limit mi aşıldı? Çağıran kod bunları ayırt edip farklı tepki vermeli: birinde müşteriye mesaj, diğerinde operasyon ekibine alarm. Hata tasarımı bu ayrımı kodun bir parçası yapar: anlamlı istisna sınıfları, nedeni koruyan zincirleme, sonradan araştırılabilen log kayıtları. Üretimde gece üçte uyandıran hataların çoğu, kötü yakalanmış ve sessizce yutulmuş istisnalardır.

Özel istisna hiyerarşisi

Exception'dan türeyen bir temel sınıf ve ondan türeyen özel sınıflar tanımlarsın. Hiyerarşi sayesinde çağıran, ister tek tek ister hepsini birden yakalar. Gövde genellikle boştur (pass ya da docstring); gerekirse ek nitelik taşır.

class BankaHatasi(Exception):
    """Ilgaz Bank iş kurallarının temel hatası."""

class HesapBulunamadi(BankaHatasi):
    def __init__(self, no):
        super().__init__(f"hesap bulunamadı: {no}")
        self.no = no

class YetersizBakiye(BankaHatasi):
    def __init__(self, hesap_no, istenen, mevcut):
        super().__init__(f"{hesap_no}: {istenen} istendi, {mevcut} var")
        self.eksik = istenen - mevcut

class LimitAsildi(BankaHatasi):
    pass

hesaplar = {"TR01": 500.0, "TR02": 12000.0}

def cek(no, tutar):
    if no not in hesaplar:
        raise HesapBulunamadi(no)
    if tutar > 10000:
        raise LimitAsildi(f"günlük limit 10000, istenen {tutar}")
    if tutar > hesaplar[no]:
        raise YetersizBakiye(no, tutar, hesaplar[no])
    hesaplar[no] -= tutar
    return hesaplar[no]

for no, t in [("TR01", 100), ("TR09", 50), ("TR01", 900), ("TR02", 11000)]:
    try:
        print("Kalan:", cek(no, t))
    except YetersizBakiye as e:
        print("Müşteriye:", e, "| eksik:", e.eksik)
    except BankaHatasi as e:           # kalan tüm banka hataları
        print("Operasyona:", type(e).__name__, "-", e)
Sıralama önemliexcept blokları yukarıdan aşağı denenir. Özel sınıfı (YetersizBakiye) temel sınıftan (BankaHatasi) önce yaz; tersini yazarsan özel blok hiç çalışmaz.

Hiyerarşi nasıl kurulur?

KatmanÖrnekKim yakalar
KökBankaHatasi(Exception)En dış katman: "bizim hatamız mı, Python'ın mı"
AlanHesapHatasi, TransferHatasiServis katmanı: alana göre tepki
SomutYetersizBakiye, HesapBulunamadiKullanıcı arayüzü: anlamlı mesaj
YerleşikValueError, KeyErrorGenel programlama hataları; iş hatasına çevrilir

raise from: nedeni koru

Alt seviyede KeyError ya da ValueError fırladı, sen bunu iş diline çevirmek istiyorsun. raise YeniHata(...) from eski yazarsan Python iki hatayı zincirler: iz kaydında "The above exception was the direct cause of the following exception" satırıyla asıl neden görünür. from None ise zinciri bilinçli olarak keser (nedeni gizlemek istediğin, örneğin kullanıcıya iç detay sızdırmamak gerektiği durumlarda).

class VeriHatasi(Exception):
    pass

def tutar_oku(kayit):
    try:
        return float(kayit["tutar"])
    except KeyError as e:
        raise VeriHatasi(f"kayıtta 'tutar' alanı yok: {kayit}") from e
    except ValueError as e:
        raise VeriHatasi(f"tutar sayı değil: {kayit['tutar']!r}") from e

for k in [{"tutar": "12.5"}, {"fiyat": 3}, {"tutar": "abc"}]:
    try:
        print(tutar_oku(k))
    except VeriHatasi as e:
        print("VeriHatasi:", e)
        print("   neden:", type(e.__cause__).__name__, "-", e.__cause__)
except içinde çıplak raiseHatayı sadece loglayıp yukarı iletmek istiyorsan raise yaz (argümansız); aynı istisna izi kaybolmadan devam eder. raise e yazmak da çalışır ama iz kaydına gereksiz bir satır ekler.

logging: print değil

Üretim kodunda print ile hata takibi yapılmaz: seviyesi yok, zamanı yok, kapatılamaz, dosyaya yönlendirilemez. logging modülü bunları verir. Beş seviye vardır: DEBUG < INFO < WARNING < ERROR < CRITICAL; yapılandırılan eşiğin altındakiler yazılmaz. Bu ortamda çıktıyı stdout'a yönlendiriyoruz.

import logging, sys

logging.basicConfig(
    stream=sys.stdout,
    level=logging.INFO,
    format="%(asctime)s %(levelname)-8s %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)
log = logging.getLogger("anadolu.transfer")

def transfer(gonderen, alici, tutar):
    log.info("transfer başlıyor %s -> %s, %.2f", gonderen, alici, tutar)
    if tutar > 10000:
        log.warning("yüksek tutar, ek onay gerekebilir: %.2f", tutar)
    log.debug("bu satır görünmez: seviye INFO")
    return True

transfer("TR01", "TR02", 250)
transfer("TR01", "TR03", 25000)

Dikkat: mesajı %s ile ve argümanları ayrı veriyoruz, f-string ile değil. Böylece seviye kapalıysa dize hiç biçimlenmez ve log kayıtları toplayan araçlar (Sentry, ELK) aynı mesaj şablonunu gruplayabilir.

İstisnayı logla: exception()

except bloğunda log.exception("...") çağırırsan mesajla birlikte tam iz kaydı (traceback) yazılır; seviye ERROR'dur. Hatayı yutmadan loglayıp yeniden yükseltmek en sağlıklı kalıptır.

import logging, sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO, format="%(levelname)s %(message)s")
log = logging.getLogger("anadolu.rapor")

def kayit_isle(kayit):
    try:
        return kayit["tutar"] / kayit["adet"]
    except ZeroDivisionError:
        log.exception("adet sıfır, kayıt atlandı: %s", kayit)
        return None

print(kayit_isle({"tutar": 100, "adet": 4}))
print(kayit_isle({"tutar": 100, "adet": 0}))
except Exception: passEn tehlikeli üç satır. Hata yutulur, program yanlış veriyle devam eder, kimse fark etmez. En azından log.exception yaz; mümkünse yakaladığın hatayı dar tut (except KeyError) ve gerisini yukarı bırak.

EAFP ile LBYL

İki yaklaşım: LBYL (Look Before You Leap, önce bak sonra atla) koşulla kontrol eder; EAFP (Easier to Ask Forgiveness than Permission, izin istemektense af dile) dener ve hatayı yakalar. Python topluluğu çoğunlukla EAFP'yi tercih eder: daha az kod, daha az yarış durumu (kontrol ile kullanım arasında dünya değişebilir), ve istisna yolu nadir durumsa daha hızlıdır.

musteriler = {"M1": {"ad": "Ayşe", "eposta": "ayse@mail.com"}, "M2": {"ad": "Can"}}

# LBYL
def eposta_lbyl(no):
    if no in musteriler and "eposta" in musteriler[no]:
        return musteriler[no]["eposta"]
    return None

# EAFP
def eposta_eafp(no):
    try:
        return musteriler[no]["eposta"]
    except KeyError:
        return None

for no in ["M1", "M2", "M9"]:
    print(no, eposta_lbyl(no), eposta_eafp(no))

# Tip kontrolü yerine davranışa güven (ördek tipleme ile EAFP)
def toplam(kaynak):
    try:
        return sum(kaynak.values())      # sözlük mü?
    except AttributeError:
        return sum(kaynak)               # değilse liste gibi davran

print(toplam({"a": 1, "b": 2}), toplam([3, 4]))

LBYL'in haklı olduğu yerler: kontrol ucuzsa ve hata yolu pahalıysa (dosya silmeden önce var mı diye bakmak), ya da istisna çok sık oluşuyorsa (istisna yakalamak başarılı yoldan daha yavaştır).

assert'in yeri

assert koşul, "mesaj" koşul yanlışsa AssertionError yükseltir. Amacı programcı varsayımlarını geliştirme sırasında doğrulamaktır: "bu noktada liste boş olamaz". Kullanıcı girdisi, dış veri ya da iş kuralı doğrulamak için kullanılmaz, çünkü Python -O bayrağıyla çalıştırıldığında tüm assert'ler silinir ve kontroller yok olur.

def ortalama(tutarlar):
    assert isinstance(tutarlar, list), "iç kullanım: liste bekleniyor"   # programcı varsayımı
    if not tutarlar:                                                      # iş kuralı: gerçek istisna
        raise ValueError("boş liste için ortalama tanımsız")
    return sum(tutarlar) / len(tutarlar)

print(ortalama([10, 20]))
try:
    ortalama([])
except ValueError as e:
    print("ValueError:", e)
try:
    ortalama("abc")
except AssertionError as e:
    print("AssertionError:", e)
Derinleş

Derinleş: zincirler, notlar, gruplar ve sınırlar

Hiyerarşi kurmayı, raise from'u ve loglamayı gördün. Bu bölümde gerçek sistemlerde fark yaratan dört konuya iniyoruz: iz kaydındaki iki farklı zincir cümlesini okumak, hataya bağlam notu eklemek (add_note), birden çok hatayı birlikte taşımak (ExceptionGroup ve except*) ve hataları sistem sınırında (API, CLI) anlamlı yanıtlara çevirmek. Sonunda finally tuzaklarını ve warnings modülünü ekliyoruz. Logger yapılandırmasının ayrıntısı "logging" dersinde, with ve bağlam yöneticisi (context manager) yazmak "Dekoratörler (decorator)" dersinde.

1. İki zincir cümlesi: cause ve context

İz kaydında iki farklı cümle görebilirsin ve anlamları farklıdır:

İz kaydındaki cümleNasıl oluşurNitelikAnlamı
The above exception was the direct cause of the following exceptionraise B from a__cause__Bilinçli çeviri: "B'nin nedeni a"
During handling of the above exception, another exception occurredexcept içinde from'suz yeni hata__context__Çoğunlukla kaza: hata işlenirken ikinci hata çıktı
(yalnız son hata)raise B from None__suppress_context__ = TrueNedeni bilinçli gizle

"During handling..." cümlesini görürsen önce except bloğunun içine bak: çoğu zaman log mesajını biçimlerken ya da temizlik yaparken yeni bir hata çıkmıştır.

def kur_al(kurlar, birim):
    try:
        return kurlar[birim]
    except KeyError:
        print("bilinmeyen birim: " + birim.upper())   # birim None ise burada 2. hata!

try:
    kur_al({"USD": 32.5}, None)
except AttributeError as e:
    print(type(e).__name__, "| context:", type(e.__context__).__name__, "| cause:", e.__cause__)

2. add_note: hatayı değiştirmeden bağlam eklemek

Python 3.11 ile her istisnaya e.add_note("...") ile not eklenebilir; notlar iz kaydının altında görünür ve e.__notes__ listesinde durur. Asıl farkı: hatanın türünü değiştirmezsin. Çağıran hâlâ ValueError yakalar ama ekstra bilgi (hangi satır, hangi dosya, hangi müşteri) kaybolmaz. Yeni istisna sınıfına çevirmek (raise from) ile not eklemek arasındaki seçim şudur: çağıranın tepkisi değişecekse çevir, yalnız teşhis bilgisi ekliyorsan not düş.

satirlar = ["Ayşe;1200", "Can;12OO", "Ece;300"]

def toplam(satirlar):
    t = 0
    for no, satir in enumerate(satirlar, start=1):
        try:
            ad, tutar = satir.split(";")
            t += int(tutar)
        except ValueError as e:
            e.add_note(f"satır {no}: {satir!r}")
            raise                       # aynı hata, ek notla
    return t

try:
    toplam(satirlar)
except ValueError as e:
    print("Hata:", e)
    print("Notlar:", e.__notes__)

3. ExceptionGroup ve except*: hepsini birden bildir

Kredi başvuru formunda üç alan hatalıysa kullanıcıyı üç kez geri göndermek kötü deneyimdir. Tüm hataları bir listede toplayıp sonunda raise ExceptionGroup("mesaj", hatalar) yaparsın. Yakalayan taraf except* ile gruptan türe göre parça seçer: her except* bloğu kendi türündeki hataları alt grup olarak alır ve birden çok blok aynı grup için çalışabilir.

def basvuru_dogrula(b):
    hatalar = []
    if b.get("yas", 0) < 18:
        hatalar.append(ValueError(f"yaş 18'den küçük: {b.get('yas')}"))
    if not str(b.get("tckn", "")).isdigit():
        hatalar.append(ValueError("TCKN yalnız rakam olmalı"))
    if not isinstance(b.get("gelir"), (int, float)):
        hatalar.append(TypeError("gelir sayı olmalı"))
    if hatalar:
        raise ExceptionGroup("başvuru geçersiz", hatalar)
    return True

try:
    basvuru_dogrula({"yas": 16, "tckn": "12a", "gelir": "yok"})
except* ValueError as g:
    for h in g.exceptions:
        print("Değer hatası:", h)
except* TypeError as g:
    print("Tür hatası:", [str(h) for h in g.exceptions])
except* kurallarıexcept* ile düz except aynı try'da karıştırılamaz. Hiçbir except* bloğunun yakalamadığı parçalar yeni bir grup olarak yukarı fırlatılır. Grubu düz except ExceptionGroup as g ile de yakalayıp g.exceptions'ı dolaşabilirsin (testlerde pratik).

4. Sınırda çeviri: hata → kullanıcı yanıtı

Bir web servisinde iç katmanlar anlamlı istisnalar fırlatır; en dış katman (istek işleyicisi) bunları HTTP durum koduna ve kullanıcıya gösterilecek kısa koda çevirir. Bu eşlemeyi istisna sınıflarına sınıf niteliği olarak koymak, devasa bir if/elif zincirini ortadan kaldırır. Beklenmeyen her şey 500 olur, ayrıntısı yalnız loga gider.

class UygulamaHatasi(Exception):
    http, kod = 400, "GENEL"

class Bulunamadi(UygulamaHatasi):
    http, kod = 404, "BULUNAMADI"

class YetkiYok(UygulamaHatasi):
    http, kod = 403, "YETKI_YOK"

def yanitla(f, *args):
    try:
        return 200, f(*args)
    except UygulamaHatasi as e:
        return e.http, {"kod": e.kod, "mesaj": str(e)}
    except Exception:
        # gerçek kodda: log.exception("beklenmeyen hata")
        return 500, {"kod": "IC_HATA", "mesaj": "beklenmeyen bir hata oluştu"}

def hesap_getir(no):
    if no == "TR00":
        raise YetkiYok("bu hesaba erişim yetkiniz yok")
    if no != "TR01":
        raise Bulunamadi(f"hesap yok: {no}")
    return {"no": no, "bakiye": 1500}

for no in ["TR01", "TR09", "TR00", None]:
    print(yanitla(hesap_getir, no) if no else yanitla(lambda: 1 / 0))

5. finally tuzakları ve else'in yeri

finally her durumda çalışır; bu yüzden içine return, break ya da continue yazarsan fırlatılmakta olan istisnayı sessizce yutar. Python 3.14 bunun için uyarı vermeye başladı; kural basit: finally yalnız temizlik yapar (dosya kapat, kilidi bırak), sonuç döndürmez. else bloğu ise "try başarılı olduysa" çalışır; yalnız hata çıkarabilecek satırı try'da tutup gerisini else'e koymak, yanlışlıkla başka bir satırın hatasını yakalamanı engeller.

6. warnings: hata değil, uyarı

Bazı durumlar programı durdurmayı gerektirmez ama bilinmelidir: eskiyen (deprecated) bir fonksiyonun kullanılması, şüpheli ama işlenebilen veri. warnings.warn("mesaj", DeprecationWarning, stacklevel=2) bunu yapar; stacklevel=2 uyarıyı çağıranın satırında gösterir. Testlerde warnings.catch_warnings(record=True) ile uyarıları yakalayıp doğrularsın; python -W error ile çalıştırınca uyarılar hataya dönüşür (CI'da yaygın).

import warnings

def cevir(tutar, kur):
    return round(tutar / kur, 2)

def eski_cevir(tutar):
    warnings.warn("eski_cevir kaldırılacak; cevir(tutar, kur) kullanın",
                  DeprecationWarning, stacklevel=2)
    return cevir(tutar, 32.5)

with warnings.catch_warnings(record=True) as kayit:
    warnings.simplefilter("always")
    print(eski_cevir(650))
print(len(kayit), kayit[0].category.__name__, "-", kayit[0].message)
Gerçek işteOlgun ekiplerde hata politikası yazılıdır: "İç katmanlar yerleşik hataları (KeyError, ValueError) alan istisnalarına from ile çevirir; yalnız sınır katmanı (API/CLI) HTTP koduna ya da çıkış koduna çevirir; beklenmeyen her hata tam iz kaydıyla loglanır ve kullanıcıya iç ayrıntı sızmaz." Toplu veri yüklemelerde de iki mod bulunur: katı mod ilk grupta durur (ExceptionGroup), esnek mod hatalı satırları rapora yazıp geçerlileri işler. Hangi modun çalıştığı her zaman loglanır.
Uzmanlaş

İstisna gruplarıyla toplu doğrulama (3.11+)

Bir formda beş alan hatalıysa kullanıcıya ilkini değil hepsini göstermek istersin. ExceptionGroup("doğrulama", [h1, h2, ...]) birden çok istisnayı tek nesnede taşır; except* sözdizimi gruptan türe göre seçer. Toplu veri yükleme ve eşzamanlı görevlerde (asyncio TaskGroup) standart araçtır.

Hata mesajı yazma kuralları

  • Ne bekleniyordu, ne geldi: "tutar pozitif olmalı, gelen: -5".
  • Bağlam ekle: hangi hesap, hangi satır, hangi dosya.
  • Çözümü ima et: "hesap bulunamadı: TR09 (numara TR ile başlamalı)".
  • Kullanıcıya giden mesajla loga giden mesajı ayır: iç detay (SQL, dosya yolu) loga, sade cümle kullanıcıya.

Logger hiyerarşisi

getLogger("anadolu.transfer") adı noktayla ayrılmış bir ağaçtır: anadolu logger'ının seviyesi ve işleyicileri alt loggerlara miras geçer. Modül başında log = logging.getLogger(__name__) yazmak standarttır; uygulama girişinde bir kez basicConfig ya da dictConfig ile yapılandırma yapılır, kütüphane kodu asla basicConfig çağırmaz.

Hata yönetimi stratejisi

Her katman yalnızca anlamlı tepki verebildiği hatayı yakalar; gerisini yukarı bırakır. Hatayı en altta yakalayıp None döndürmek sorunu ertelemektir: "None nereden geldi?" sorusu üç katman yukarıda, saatler sonra sorulur. En dış katmanda (CLI giriş noktası, web isteği işleyicisi) tek bir except BankaHatasi kullanıcı mesajını, tek bir except Exception ile log.exception beklenmeyeni raporlar.

Alıştırmalara geç (22)