İstisnalar, modüller ve sistematik hata ayıklama
Söz dizimi doğru olan bir program her durumda sorunsuz çalışmayabilir. Dosya bulunamayabilir, metin sayıya çevrilemeyebilir, beklenen anahtar sözlükte olmayabilir ya da bir hesapta sayı sıfıra bölünebilir.
Bu hafta istisna (exception) kavramını öğreneceğiz. Program çalışırken çıkan belirli sorunları try/except ile yakalamayı, gerektiğinde raise ile kendiniz istisna yükseltmeyi ve Python’la birlikte gelen modülleri kullanmayı göreceğiz. Hata ararken de rastgele değil, adım adım ilerlemeyi öğreneceğiz.
Bu bölümün kapsamı
- Söz dizimi, çalışma zamanı ve mantık hatasının ayrımı
try/exceptile belirli istisnayı ele alma ve genişexceptriskiFileNotFoundError,KeyErrorveValueErroreşlemesiraiseile sözleşme ihlalini bildirme veassertten ayırmaNonesinyali veis Nonedenetimi- Traceback’te hata türü, mesaj ve kendi kodundaki satır
importile modül kullanımı ve main guard’ın ayırdığı iki durum
- Kendi istisna sınıfını tanımlama
loggingmodülü- Adım adım çalıştıran hata ayıklayıcı arayüzü
Bu başlıklar konunun devamıdır; ileride karşınıza çıkar ama bu derste ezberlemeniz beklenmiyor.
Program hata vermiyor ama sonuç yanlış; bu ne tür bir hatadır?
Hataların hepsi kırmızı bir mesajla gelmez. Önce hataları türlerine ayıracağız. Sonra beklediğimiz hataları program çökmeden yakalamayı ve bir hata iz dökümünü (traceback) okumayı öğreneceğiz. Traceback, Python’ın hata anında yazdığı ve hatanın hangi satırlardan geçerek oluştuğunu gösteren dökümdür.
Hata türlerini ayırmak
Söz dizimi hatası
if puan >= 60
print("Geçti")
if satırının sonundaki iki nokta eksik. Python kodu çalıştırmaya başlamadan SyntaxError verir.
Çalışma zamanı hatası
sayi = int("abc") # ValueError
# 10 / 0 # ZeroDivisionError
# [1, 2][5] # IndexErrorKodun yazımı kurallara uygun; sorun ancak satır çalıştığında ortaya çıkar.
Mantık hatası
Program çalışır ama yanlış sonuç verir. Örneğin daire çevresinin formülünü eksik yazalım:
def daire_cevresi(yaricap):
return 3.14 * yaricap # Hatalı: 2 çarpanı eksikDoğru formülde π sayısı için math.pi değerini kullanabiliriz. math, Python’la birlikte gelen standart kütüphanenin modüllerinden biridir:
import math
def daire_cevresi(yaricap):
return 2 * math.pi * yaricapassert abs(daire_cevresi(1) - 2 * math.pi) < 0.000001Bu tür hatayı Python kendiliğinden yakalayamayabilir. Sonucu beklenen değerle karşılaştırmak size düşer; yukarıdaki assert satırı da bunu yapar.
Hata ayıklarken “program çalışıyor mu?” sorusu yetmez. Hatayı nasıl düzelteceğinizi hatanın türü ve programın ne yapması gerektiği belirler.
İstisna nedir?
Python bir satırı çalıştırırken devam edemeyeceği bir durumla karşılaşırsa bir istisna oluşturur. İstisna yakalanmazsa program o noktada durur:
int("on")ValueError oluşur. Türün adı ve yanındaki mesaj sorunun ne olduğunu söyler: "on" metni tam sayıya çevrilemiyor.
try / except
metin = "42"
try:
sayi = int(metin)
print("Sayı:", sayi)
except ValueError:
print("Geçerli bir tam sayı değil")metin = "abc" olsaydı dönüşüm başarısız olur, print("Sayı:", sayi) satırı atlanır ve yalnızca except ValueError bloğu çalışırdı.
Hata nesnesinin mesajını okumak: as e
Bazen Python’ın verdiği asıl mesajı da görmek isteriz: kullanıcıya göstermek ya da hatayı ararken okumak için.
metin = "abc"
try:
sayi = int(metin)
except ValueError as e:
print("Hata türü: ValueError")
print("Ayrıntı:", e)e, oluşan istisna nesnesine bu blok içinde verdiğimiz addır. Bu örnekte print("Ayrıntı:", e) satırı invalid literal for int() with base 10: 'abc' mesajını yazdırır.
Python’ın mesajını kullanıcıya olduğu gibi göstermek zorunda değilsiniz. Kod yazarken ve hata ararken ise except ... as e sorunun ayrıntısını görmenizi sağlar.
Tahmin et: hangi blok çalışır?
Çalıştırmadan önce çıktıyı tahmin edin. Sonra girdi_metni değerini "yetmis" yapıp yeniden tahmin edin.
Alıştırma: dönüşüm hatasını ele alın
Kodu try/except ValueError kullanacak biçimde değiştirin; geçersiz metinde Geçerli sayı değil yazdırılsın.
girdi_metni = "abc"
try:
sayi = int(girdi_metni)
print(sayi)
except ValueError:
print("Geçerli sayı değil")Belirli hata türünü yakalamak
Türü yazılmamış bir except bloğu, beklemediğiniz birçok hatayı da sessizce yutabilir:
try:
...
except:
...Yalnızca sayı dönüşümünde çıkan hatayı bekliyorsanız türünü yazın:
try:
sayi = int(metin)
except ValueError:
...Böylesi daha açıklayıcıdır: kodu okuyan kişi hangi hatanın beklendiğini görür.
“Program hiç hata vermesin” diye her şeyi türsüz, geniş bir except ile kapatmak hata ayıklamayı zorlaştırır. Yalnızca beklediğiniz ve karşılığında ne yapacağınızı bildiğiniz hata türlerini yakalayın.
Birden fazla hata türü
def bol(metin, bolen):
sayi = int(metin)
return sayi / bolen
try:
sonuc = bol("20", 0)
print(sonuc)
except ValueError:
print("Metin sayıya dönüştürülemedi")
except ZeroDivisionError:
print("Sıfıra bölme yapılamaz")Her türü ayrı bir except bloğunda yakaladığınızda hatanın nedenini ayırt edebilirsiniz. Bu örnekte ZeroDivisionError bloğu çalışır.
12. haftanın dosya hataları
Geçen hafta dosyanın bulunamayabileceğini, bir sütunun eksik olabileceğini ya da bir değerin sayıya çevrilemeyebileceğini söylemiştik. Şimdi bu durumları istisnalarla yakalayalım.
FileNotFoundError
try:
with open("veri.csv", "r", encoding="utf-8") as dosya:
icerik = dosya.read()
except FileNotFoundError as e:
print("Dosya bulunamadı: veri.csv")
print("Ayrıntı:", e)except bloğu yalnızca dosya bulunamadığında çalışır. Dosyanın içindeki veri hataları bu bloğa karışmaz.
KeyError: beklenen sütun/alan yok
DictReader’ın verdiği kayıtta "puan" sütunu yoksa kayit["puan"] erişimi KeyError verir:
kayit = {"ad": "Deniz"}
try:
puan = int(kayit["puan"])
except KeyError as e:
print("Beklenen alan bulunamadı:", e)Bir CSV dosyasını okurken üç hata bir arada şöyle yakalanır:
import csv
try:
with open("ogrenciler.csv", "r", encoding="utf-8-sig", newline="") as dosya:
okuyucu = csv.DictReader(dosya, delimiter=";")
for kayit in okuyucu:
puan = int(kayit["puan"])
print(kayit["ad"], puan)
except FileNotFoundError as e:
print("CSV dosyası bulunamadı:", e)
except KeyError as e:
print("CSV sütunu eksik:", e)
except ValueError as e:
print("Sayısal dönüşüm başarısız:", e)delimiter yanlışsa DictReader satırları doğru sütunlara bölemez. Beklediğiniz sütunlar hiç oluşmayabilir ve sorun sonradan KeyError olarak karşınıza çıkar. Bu yüzden istisnayı yakalamakla yetinmeyin, hatanın kök nedenini (asıl kaynağını) da arayın.
İstisna mı, koşul mu?
Puanın 0 ile 100 arasında olması programın koyduğu bir kuraldır. Bunu if ile denetleriz:
if 0 <= puan <= 100:
...Metnin int() ile sayıya çevrilememesi ise dönüşüm sırasında Python’ın oluşturduğu bir istisnadır. Bunu try/except ile yakalarız:
try:
puan = int(girdi_metni)
except ValueError:
...raise: fonksiyon sözleşmesi ihlalini açıkça bildirmek
7. haftada assert ile programcı olarak kendi beklentimizi sınamış, kullanıcı girdisini denetlemek için uygun olmadığını söylemiştik. Bir fonksiyon geçersiz bir argümanı program çalışırken açıkça reddetmek istiyorsa raise ile kendisi istisna yükseltebilir:
def saat_ucreti(toplam_ucret, saat):
if saat <= 0:
raise ValueError("saat 0'dan büyük olmalı")
return toplam_ucret / saatÇağıran kod isterse bunu yakalar:
try:
print(saat_ucreti(1000, 0))
except ValueError as e:
print("Geçersiz girdi:", e)raise ValueError(...) çağıran koda şunu bildirir: verilen değer bu fonksiyonun sözleşmesine uymuyor.
Her geçersiz durumda raise kullanmak zorunda değilsiniz; sıradaki başlıktaki gibi None döndürmek de bir seçenektir. Hangisini seçeceğiniz, çağıran kodun hatayı nasıl karşılayacağına bağlıdır. Önemli olan, fonksiyonun sözleşmesinin açık ve tutarlı olmasıdır.
None ile hata sinyali: kolay ama yükü çağırana bırakır
Aşağıdaki fonksiyon puan geçersizse None döndürüyor:
def puani_coz(metin):
try:
puan = int(metin)
except ValueError:
return None
if 0 <= puan <= 100:
return puan
return NoneAvantajı: istisna yükselmez; çağıran kod sonucu sıradan bir dönüş değeri gibi alır ve kendisi kontrol eder.
Bedeli: fonksiyonu çağıran her yer None olasılığını hesaba katmak zorundadır.
puan = puani_coz("abc")
# print(puan + 10) # TypeError: None ile int toplanamazBu durumda hata dönüşüm satırında değil, daha ileride, nereden geldiği belli olmayan bir satırda çıkabilir.
Bu yüzden çağıran kod sözleşmeye uyar ve sonucu kullanmadan önce denetler:
puan = puani_coz("abc")
if puan is None:
print("Puan çözülemedi")
else:
print("Yeni puan:", puan + 10)Neden is None?
Python’da tek bir None nesnesi vardır. Bu yüzden önerilen kontrol şöyle yazılır:
if sonuc is None:
...
if sonuc is not None:
...sonuc == None yerine is None yazılır. is, iki tarafın aynı nesneyi gösterip göstermediğini sınar; en sık kullanıldığı yer de None kontrolüdür.
14. haftadaki bütünleştirici uygulamada if kayit is None: ve assert ... is None satırlarını göreceksiniz. Orada == yerine is bilerek seçildi: None için kimlik kontrolü yapılıyor.
None mı raise mı?
İki yol da duruma göre doğru olabilir:
None döndürme
→ geçersizlik beklenen bir sonuç seçeneği olabilir
→ çağıran her seferinde kontrol etmelidir
raise ValueError
→ sözleşme ihlalini akıştan ayırır
→ çağıran isterse uygun sınırda except ile yakalar
Bu derste iki yolu da kullanıyoruz. Benzer problemlerde ikisini rastgele karıştırmıyor, hangisini seçtiğimizi fonksiyonun sözleşmesinde açıkça yazıyoruz.
Traceback okumak
Traceback (most recent call last):
File "rapor.py", line 12, in <module>
toplam = fiyat_hesapla("abc")
File "rapor.py", line 5, in fiyat_hesapla
fiyat = float(metin)
ValueError: could not convert string to float: 'abc'
Bir traceback’i şu sırayla okuyun:
- En alttaki hata türünü bulun.
- Hata mesajını okuyun.
- Kendi dosyanıza ait en alttaki satırı bulun.
- O satıra hangi değerlerin geldiğini kontrol edin.
- Sorunu daha küçük bir örnekle yeniden üretin.
Sistematik hata ayıklama
- Yeniden üret: Hata hangi girdiyle oluşuyor?
- Beklenen davranışı yaz: Ne olmalıydı?
- Küçült: Hatayı gösteren en küçük örneği bulun.
- Gözlemle: Değişkenlerin değerlerine ya da traceback’e bakın.
- Hipotez kur: Hatanın nedeni hakkında tek bir tahmin seçin.
- Tek değişiklik yap: Yalnızca bu tahmini sınayacak küçük bir düzeltme yapın.
- Yeniden doğrula: Hem sorunlu hem sorunsuz girdiyi deneyin.
Bu adımları izleyen biri satırları rastgele değiştirmez; her adımda neyi sınadığını bilir.
Modül nedir?
Modül, fonksiyon ve değer tanımlarını bir arada tutan bir Python dosyasıdır. Bir modülü import ile programınıza alır, içindekileri kullanırsınız. Standart kütüphaneden bir örnek:
import math
print(math.sqrt(81))
print(math.pi)Tarih işlemleri için datetime modülü:
from datetime import date
bugun = date.today()
print(bugun)Karekök, π sayısı ya da bugünün tarihi için kendi kodunuzu yazmanız gerekmez; bu işler standart kütüphanedeki modüllerde hazır bulunur.
Tahmin et ve çalıştır: math
İkinci print, 8. haftada gördüğümüz f-string biçimlendirmesiyle sonucu iki ondalık basamakla yazdırır.
Kendi modülümüz
Kendi yazdığınız her .py dosyası da bir modüldür. Örneğin hesaplar.py dosyası:
def kdv_hesapla(tutar, oran):
return tutar * oran
def indirimli_fiyat(fiyat, oran):
return fiyat - fiyat * oranAynı klasördeki başka bir dosyadan bu modülü şöyle kullanırsınız:
import hesaplar
print(hesaplar.kdv_hesapla(500, 0.20))Modülden yalnızca bir fonksiyonu da alabilirsiniz:
from hesaplar import kdv_hesapla
print(kdv_hesapla(500, 0.20))Main guard
Aşağıdaki if __name__ == "__main__": kalıbına main guard denir:
def main():
print("Program başladı")
if __name__ == "__main__":
main()Bu kalıp iki durumu birbirinden ayırır:
- Dosya doğrudan çalıştırılırsa
main()çağrılır. - Dosya başka bir dosyadan modül olarak içe aktarılırsa (
import)main()kendiliğinden çağrılmaz.
Alıştırma: modül fonksiyonunu kullanın
Beklenen sonuç 5.0’dır.
import math
def hipotenus(a, b):
return math.sqrt(a ** 2 + b ** 2)Mantık hatasında assert
def indirimli_fiyat(fiyat, oran):
return fiyat * oran
assert indirimli_fiyat(100, 0.20) == 80Fonksiyon çalışır ve istisna oluşmaz, ama 80 yerine 20.0 döndürür. assert satırı beklenen değeri gerçek sonuçla karşılaştırır ve AssertionError verir; hata böylece fark edilir. Doğru fonksiyon:
def indirimli_fiyat(fiyat, oran):
return fiyat - fiyat * oranÜret: güvenli puan dönüştürme
Fonksiyon şu kurallara uysun:
- Metin tam sayıya çevrilemiyorsa
Nonedöndürür. - Sayı
0–100arasındaysa sayının kendisini döndürür. - Sayı bu aralığın dışındaysa
Nonedöndürür. - Fonksiyonun içinde
print()kullanılmaz.
Beklenen çıktı: 85, None, None.
def puani_coz(metin):
try:
puan = int(metin)
except ValueError:
return None
if 0 <= puan <= 100:
return puan
return NoneFonksiyon çalışınca sonuçları yazdırmakla kalmayın, sözleşmeyi assert ile de sınayın:
assert puani_coz("85") == 85
assert puani_coz("abc") is None
assert puani_coz("120") is NoneHata ayıklama kontrol listesi
- Hata türü nedir: söz dizimi, çalışma zamanı, mantık?
- Sorunu aynı girdiyle yeniden üretebiliyor muyum?
- Beklenen sonuç nedir?
- Sorunu daha küçük örneğe indirebilir miyim?
- Traceback hangi hata türünü, mesajı ve satırı gösteriyor?
- O satırdaki değişkenlerin değerleri nedir?
except ... as eile gerçek mesajı görmem yararlı olur mu?Nonedöndürebilen bir fonksiyonun sonucunu çağıran kod kontrol ediyor mu?- Tek bir hipotezi sınayacak küçük bir değişiklik yapabilir miyim?
- Düzeltme sonrası hem sorunlu hem normal örneği yeniden denedim mi?
Tek sayfa özet
- Söz dizimi hatası kod çalışmadan önce yazımda, çalışma zamanı hatası kod çalışırken, mantık hatası ise yanlış sonuçta ortaya çıkar.
- Mantık hatasında program çalışır ama sonuç yanlıştır;
daire_cevresiörneğinde doğru formül2 * math.pi * yaricaptır. try/exceptile beklediğiniz belirli istisnaları yakalarsınız; türü yazılmamış genişexceptgerçek hataları gizleyebilir.except ... as eile istisnanın mesajını okuyabilirsiniz.- haftadaki eksik dosya, eksik sütun ve sayıya çevrilemeyen değer sorunları sırasıyla
FileNotFoundError,KeyErrorveValueErrorile yakalanabilir.
- haftadaki eksik dosya, eksik sütun ve sayıya çevrilemeyen değer sorunları sırasıyla
raise ValueError(...), sözleşmeye uymayan bir değeri çağıran koda açıkça bildirir.- Hata sinyali olarak
Nonedöndürülebilir; bunun bedeli, çağıranın her seferindeis Nonekontrolü yapmasıdır. Noneiçin önerilen kontrolis None/is not Nonebiçimidir.- Traceback’i okumaya en alttaki hata türü ve mesajdan başlayın, sonra kendi kodunuzdaki satırı bulun.
- Hata ayıklarken sorunu yeniden üretir, küçültür, gözlemler, bir hipotez kurar ve düzeltmeyi doğrularsınız.
- Standart kütüphane modülleri
importile kullanılır; kendi yazdığınız Python dosyaları da birer modüldür. - Main guard, dosyanın doğrudan çalıştırıldığı durumu modül olarak içe aktarıldığı durumdan ayırır.
Son bölümde yeni kavram yok. İlk 13 haftanın araçlarıyla CSV okuyan, modüllere ayrılmış, hataları yakalayan, koleksiyonlarla çalışan ve kendini assert ile sınayan küçük bir uygulama yazacağız.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
- Söz dizimi, çalışma zamanı ve mantık hatasını birbirinden ayırır.
try/exceptile belirli bir istisnayı yakalar ve genişexceptriskini bilir.FileNotFoundError,KeyErrorveValueError’ı doğru duruma eşler.raiseile sözleşme ihlalini bildirir ve bunuassertten ayırır.Nonesinyalini kullanır veis Nonedenetimini uygular.- Traceback’ten hata türünü, mesajı ve hatanın kendi kodundaki yerini okur.
importile modül kullanır ve main guard’ın ne ayırdığını açıklar.