Dosyalar, istisnalar ve sistematik hata ayıklama
Gerçek bir programa her zaman doğru girdi gelmez. Dosya bulunmayabilir, bir satırda sayı yerine metin olabilir ya da program çalışıp yanlış sonuç verebilir. Program çalışırken böyle bir sorun çıkınca Python normal akışı keser ve bir istisna (exception) oluşturur. 1. haftada gördüğünüz ValueError ve TypeError birer istisnaydı. Bu hafta dosyadan okuyup dosyaya yazacak, istisnaları yakalayacak ve gerektiğinde kendimiz yükselteceğiz. Bir hata mesajı gördüğümüzde kodu rastgele değiştirmeyecek, mesajı sorunun nedenini gösteren bir kanıt gibi okuyacağız.
Bu sayfadaki kod hücreleri kendiliğinden çalışmaz. Her hücrede önce çıktıyı tahmin edin, sonra Run Code düğmesine basın. Kodu değiştirip yeniden çalıştırabilir, Start Over ile ilk hâline döndürebilirsiniz.
Bu bölümün kapsamı
with open(...)kalıbı ve dosyanın blok bitiminde kapanması- Metin dosyalarında neden
encoding="utf-8"yazıldığı "r","w"ve"a"kipleri,"w"kipinin eski içeriği silmesi- Belirli istisna türünü yakalama ve istisnayı yakalayıp hiçbir şey yapmamanın zararı
raiseileprintfarkı- Söz dizimi, çalışma zamanı ve mantık hatalarının ayrımı
- Traceback çıktısını hata türünden başlayarak okuma
pathlibve ikili dosya kipleriloggingmodülü- Kendi istisna sınıfını tanımlama (12. bölümün konusu)
assertileraisearasındaki seçim (14. bölümün konusu)
Bu başlıklar konunun devamıdır. İleride karşınıza çıkar, ama bu derste ezberlemeniz beklenmiyor.
Not dosyasını okuyan programı C’de nasıl yazardınız?
Not programı artık notları bir dosyadan okuyacak. notes.txt dosyasının her satırında bir öğrencinin adı ve puanı var: Ayşe,80. Program şu beş işi yapacak:
- Dosyayı açıp satır satır okumak, iş bitince dosyayı kapatmak
- Sonuç raporunu her çalışmada baştan yazmak, çalışma günlüğüne ise eski kayıtları silmeden eklemek
- Dosya yoksa anlaşılır bir mesaj vermek, puan yerine
bozukyazan satırı atlayıp devam etmek - Hiç geçerli not yoksa, ortalamayı hesaplayan fonksiyonun bunu çağırana bildirmesi
- Program çöktüğünde ya da yanlış ortalama verdiğinde hatanın yerini bulmak
Programlama Temelleri’nde C ile yazdığınız programları düşünün. Bu işlerin her birini C’de nasıl yapardınız? Okumaya devam etmeden önce her iş için bir satır yazın. Hangi fonksiyonu çağıracağınızı ya da neyi denetleyeceğinizi yazmanız yeter: “fopen ile açarım” da bir cevap.
Bölümde bu beş işin Python’da nasıl yazıldığını göreceğiz. Bölümün sonunda listenize döneceğiz.
with ile dosya açmak
C’de dosyayı fopen ile açar, işiniz bitince fclose ile kapatırdınız. Arada bir return ya da hata olursa fclose satırına hiç gelinmeyebilirdi. Python’da dosya open() ile açılır ve çoğu zaman bir with bloğu içinde kullanılır:
with open("notes.txt", "r", encoding="utf-8") as file:
content = file.read()
print(content)open() dosya adını, kipi ve karakter kodlamasını alır, bir dosya nesnesi döndürür. as file bu nesneyi file adına bağlar. file.read() dosyanın bütün içeriğini tek bir string olarak döndürür. with bloğu bittiğinde dosya kapatılır, blok içinde bir istisna oluşsa bile. Elle close() çağırmayı unutma riski kalmaz.
Neden encoding="utf-8" yazıyoruz?
Metin dosyası baytlardan oluşur. Karakterlerin bu baytlara nasıl çevrileceğini bir karakter kodlaması belirler. C’de fopen kodlamayla ilgilenmez, baytları olduğu gibi okur. Python ise metin dosyasını okurken baytları karakterlere çevirir. Kodlamayı yazmazsanız işletim sisteminin varsayılanını kullanır, bu yüzden aynı dosya farklı makinelerde farklı okunabilir. Bu sorun özellikle Windows’ta ve Türkçe ç, ğ, ı, İ, ö, ş, ü karakterlerinde çıkar.
Bu ders boyunca metin dosyalarında kodlamayı açıkça encoding="utf-8" diye yazacağız. Böylece dosyanın hangi kodlamayla okunup yazılacağı makineden makineye değişmez.
Satır satır okuma
C’de dosyayı fgets ile satır satır okurdunuz. Python’da dosya nesnesini bir for döngüsüyle dolaşırsınız: döngü değişkeni her turda bir satırı alır. Kod hücreleri tarayıcının içinde sanal bir dosya sistemi kullanır, bu yüzden dosya deneylerini burada yapabilirsiniz. Hücredeki ilk with bloğu okunacak dosyayı oluşturuyor ("w" kipini bir sonraki başlıkta göreceğiz). İkinci blok dosyayı okuyor. Hücre ne yazar? Her satırın sonundaki \n karakterine ne oluyor?
Kip yazılmazsa open() dosyayı okumak için ("r") açar. Dosyadan gelen her satır satır sonu karakteriyle (\n) biter. 2. haftadaki gibi satırı önce strip() ile temizler, sonra split() ile bölersiniz. Parçalar metin olduğu için puan int() ile sayıya çevrilir.
Bu derste dosyaları çoğunlukla for line in file ile okuyacağız. read() bütün içeriği tek bir metin olarak, readlines() bütün satırları bir liste olarak belleğe alır. Küçük dosyalarda sorun çıkarmazlar, ama satır satır yapılan işlerin çoğunda gerekmezler.
Dosya kipleri: r, w, a
C’deki fopen kipleri Python’da aynı harflerle yazılır:
| Kip | Anlam | Dikkat edilecek nokta |
|---|---|---|
"r" |
oku | dosya yoksa FileNotFoundError |
"w" |
yaz | dosya varsa mevcut içeriği silip baştan yazar |
"a" |
sona ekle | mevcut içeriği korur, sonuna yazar |
"w" kipi eski içeriği siler
with open("odev.txt", "w", encoding="utf-8") as file:
file.write("Yeni içerik")odev.txt zaten varsa eski içerik silinir. Var olan dosyanın sonuna eklemek istiyorsanız "a" kullanın. Kipi yazmadan önce kendinize “eski içerik kalmalı mı?” diye sorun.
Not programında sonuç raporu her çalışmada "w" ile baştan yazılır, çalışma günlüğü ise "a" ile açılıp büyür. Aşağıdaki üç with bloğu aynı dosyayı sırayla "w", "a" ve "r" kipiyle açıyor. Son print() kaç satır yazar? Hangileri?
"a" dosyanın sonuna ekler, "w" ise dosyayı her açılışta baştan yazar. Hücreyi bir kez daha çalıştırırsanız ne olur? Satırlar çoğalır mı?
İlk blok dosyaya python ve oop satırlarını yazar. İkinci blok istisna satırını sona ekler. Son blok üç satırı da okur: python, oop, istisna. Hücreyi yeniden çalıştırdığınızda ilk blok dosyayı yine baştan yazdığı için çıktı değişmez.
Hatayı yakalamak: try ve except
C’de fopen dosyayı bulamazsa NULL döndürürdü, siz de if (f == NULL) diye denetlerdiniz. atoi("bozuk") ise hiçbir uyarı vermeden 0 döndürürdü. Python’da iki durumda da istisna oluşur: dosya yoksa FileNotFoundError, sayıya çevrilemeyen metinde ValueError. İstisnayı try ve except ile yakalarsınız. Hücrede raw değişkeni "42x". Hücre ne yazar? print(number * 2) satırı çalışır mı?
try bloğundaki bir satır istisna oluşturursa bloğun geri kalanı atlanır ve türü uyan except bloğu çalışır. as error yakalanan istisnayı error adına bağlar. Hata mesajını yazdırmak ya da ayrıntısına bakmak için bu adı kullanırız.
Eksik dosya için de aynı kalıp kullanılır. Sanal dosya sisteminde missing.txt adlı bir dosya yok. Hücre ne yazar?
except Exception: yazıp her istisnayı yakalamak başta rahat görünebilir. Ama bu, yanlış yazılmış bir ad ya da uyuşmayan türler gibi kodunuzdaki hataları da gizler. Hangi istisnayı bekliyorsanız except satırına onun türünü yazın.
try:
risky_operation()
except Exception:
passBu kalıp neyin yanlış gittiğini gizler: program hiçbir mesaj vermeden devam eder, ama sonucu yanlış olabilir. Öğrenirken hata mesajını görmek, onu susturmaktan çoğu zaman daha yararlıdır.
else ve finally
try deyiminin tam biçiminde şu bloklar da bulunabilir:
try:
value = int(raw)
except ValueError as error:
print("Dönüşüm başarısız:", error)
else:
print("Dönüşüm başarılı:", value)
finally:
print("Bu satır her durumda çalışır")else,trybloğu hiç istisna oluşmadan biterse çalışır.finally, istisna oluşsa da oluşmasa da çalışır. Açılan bir dosyayı kapatmak gibi her durumda yapılması gereken işler için kullanılır.
Dosyaları çoğu zaman with ile açtığımız için finally içine elle close() yazmamız gerekmez. Yine de finally başka kodlarda karşınıza çıkacak, ne işe yaradığını bilin.
Hatayı çağırana bildirmek: raise
C’de ortalama fonksiyonu hiç not almadıysa bunu çağırana -1 gibi özel bir değer döndürerek bildirirdi. Çağıran bu değeri denetlemeyi unutursa program -1’i ortalama sanıp devam ederdi. Python’da bir fonksiyon, işini yapamayacağını bir istisna yükselterek bildirir.
Bazı istisnaları Python kendisi yükseltir: int("abc") çalışınca ValueError çıkar. Notu olmayan bir öğrencinin ortalamasının hesaplanamayacağını ise Python bilemez, bunu programın kendi kuralı söyler. Böyle bir kural bozulduğunda istisnayı raise ile kendimiz yükseltiriz. Hücredeki sum() listedeki sayıların toplamını döndürür. Hücre ne yazar? return satırı çalışır mı?
raise ValueError(...) çalıştığı anda fonksiyonun geri kalanı atlanır ve uygun except bloğu aranır. print("hata") ise yalnızca mesaj yazar: fonksiyonu çağıran kod bir sorun olduğunu anlamaz ve program devam eder. raise ise çağırana işlemin normal biçimde tamamlanamadığını bildirir. Çağıran istisnayı yakalamazsa program durur. -1 gibi bir dönüş değeri gözden kaçabilir, yakalanmayan bir istisna kaçmaz.
6–12. haftalarda nesnelerin uyması gereken kuralları (örneğin “bakiye eksiye düşemez”) korurken en çok raise kullanacağız. Nesnenin her zaman uyması gereken bu tür kurallara 8. haftada değişmez koşul (invariant) adını vereceğiz.
Üç hata sınıfını ayırın
Program çöktüğünde ya da yanlış sonuç verdiğinde, hatanın yerini aramadan önce hangi sınıftan olduğunu ayırın. Üçünü de 1. haftada kısaca gördünüz.
Söz dizimi hatası
if score > 50
print("Geçti")if satırının sonundaki : eksik. Python kodu okurken takılır ve dosyanın tek satırını bile çalıştırmaz. C’de derleyicinin verdiği söz dizimi hatalarına benzer.
Çalışma zamanı hatası
score = int("seksen")Satırın yazımı doğrudur ama satır çalışırken sorun çıkar: "seksen" metni tam sayıya çevrilemez ve ValueError yükselir.
Mantık hatası
# Yüzde 20 indirim yapmak isterken:
final = price * 1.20Program çalışır ama yanlış sonuç verir. Mantık hataları çoğu zaman istisna oluşturmaz. Onları bulmak için sonucu beklenen değerle karşılaştırmak, yani test yazmak ve programı örnek girdilerle denemek gerekir.
Beklenen sonucu assert ile yazmak
assert satırı bir koşulu denetler. Koşul doğruysa hiçbir şey yapmaz, yanlışsa programı AssertionError ile durdurur. Beklenen sonucu bir assert satırına yazarsanız, mantık hatası artık hata vermeden geçmez. Aşağıdaki average fonksiyonu C alışkanlığıyla yazılmış. 70 ve 85’in ortalaması 77.5 olmalı. Hücre ne yazar? Test geçti satırına gelinir mi?
// taban bölmesidir, 155 // 2 işlemi 77 verir. İlk satır 77 yazar. assert satırındaki koşul yanlış olduğu için program AssertionError ile durur ve Test geçti yazılmaz. Düzeltmek için // yerine / yazın.
Sonraki haftaların çalışır kod görevlerinde sonuçları assert satırlarıyla denetleyeceksiniz. assert ile raise arasındaki seçime 14. haftada döneceğiz.
Hata iz dökümü (traceback) nasıl okunur?
C’de sıfıra bölen bir program çoğu zaman hangi satırda çöktüğünü söylemeden kapanırdı. Python’da yakalanmayan bir istisna programı durdurduğunda ekrana şöyle bir döküm yazılır:
Traceback (most recent call last):
File "app.py", line 12, in <module>
total = calculate(data)
File "app.py", line 5, in calculate
return 100 / data["count"]
ZeroDivisionError: division by zero
İlk bakılacak yer en alttaki satırdır: hata türü ZeroDivisionError. Sonra hemen üstte hatanın oluştuğu kod satırını bulun. Daha yukarıdaki satırlar o noktaya hangi fonksiyon çağrılarıyla gelindiğini gösterir.
Hata ayıklama döngüsü
Hata ayıklama (debugging), bir hatanın nedenini bulup düzeltme işidir. Rastgele denemek yerine şu adımları izleyin:
- Hatayı tekrarlayın: Hata hangi girdide her seferinde oluşuyor?
- Daraltın: Hatayı hâlâ veren en küçük kod parçası hangisi?
- Kanıt toplayın: Traceback’i okuyun. C’de
printfile yaptığınız gibi ara değerleriprint()ile, türlerinitype()ile yazdırın. - Hipotez kurun: Sorunun nedeni ne olabilir?
- Tek değişiklik yapın: Aynı anda birkaç şeyi rastgele değiştirmeyin.
- Yeniden deneyin: Hata gitti mi? Başka bir şey bozuldu mu?
Nesne Tabanlı Programlama II dersinde aynı adımları debugger (kodu satır satır çalıştırıp değişkenlere bakmanızı sağlayan araç), logging ve testlerle izleyeceğiz.
Soruya dönelim: beş iş Python’da
Bölümün başında not dosyasını okuyan programın beş işini C’de nasıl yapacağınızı sormuştuk. Python’daki karşılıkları şöyle:
| İş | C’de | Python’da | Dikkat |
|---|---|---|---|
| Dosyayı satır satır okuyup kapatmak | fopen, fgets döngüsü ve fclose |
with open("notes.txt", encoding="utf-8") as file: ve for line in file: |
with dosyayı blok bitince kapatır. Kodlamayı açıkça yazın |
| Raporu baştan yazmak, günlüğe eklemek | fopen(..., "w") ve fopen(..., "a") |
open(..., "w", ...) ve open(..., "a", ...) |
"w" var olan dosyanın içeriğini siler |
| Eksik dosya ve bozuk satır | fopen sonucunu NULL ile karşılaştırırsınız, atoi bozuk satırda 0 döndürür |
try ile except FileNotFoundError ve except ValueError |
Yalnız beklediğiniz türü yakalayın. except Exception: pass hataları gizler |
| Ortalama hesaplanamıyor | -1 gibi özel bir değer döndürürsünüz |
raise ValueError("Geçerli not yok") |
Yakalanmayan istisna programı durdurur, gözden kaçmaz |
| Hatanın yerini bulmak | printf ile ara değerleri yazdırırsınız |
Traceback’i en alttan okumak, print() ve assert |
Mantık hatası istisna oluşturmaz, sonucu beklenen değerle karşılaştırın |
Listenizi tabloyla karşılaştırın. İlk iki satırda C bilginiz neredeyse olduğu gibi işe yarar, kip harfleri bile aynı. Fark üçüncü ve dördüncü satırda. C’de bir sorunu dönüş değeriyle bildirirdiniz ve o değeri denetlemek çağıranın işiydi. Denetlemeyi unutursanız program yanlış değerle devam ederdi. Python’da sorun bir istisnayla bildirilir. Çağıran onu yakalamazsa program traceback’le durur ve sorun gözden kaçmaz.
Bu bölümle Python’a geçiş bitiyor. Beş haftada değerleri ve türleri, kontrol akışını, koleksiyonları, fonksiyonları, modülleri, dosyaları ve istisnaları gördünüz. 6. haftadan itibaren soru değişiyor. “Python’da döngü nasıl yazılır?” yerine “Bir banka hesabının bakiyesi nerede durmalı, onu hangi kod değiştirebilmeli?” diye soracağız. C’de bir struct ile onu değiştiren fonksiyonlar ayrı ayrı dururdu. Nesne yönelimli programlamada (OOP) veri ile onu değiştiren kod aynı yapıda toplanır, raise de o yapının kurallarını korur.
Alıştırma: Doğru hata türünü yakala
except ValueError as error:Alıştırma: raise ile hata yükselt
Aşağıdaki fonksiyonu tamamlayın. score 0–100 aralığının dışındaysa ValueError yükseltmeli, geçerliyse değeri döndürmelidir:
Fonksiyonu sınırdaki -1, 0, 100 ve 101 değerleriyle ayrı ayrı deneyin.
Alıştırma: Mantık hatasını bul
Program hata vermeden çalışıyor. Ama yüzde 20 indirimde beklenen sonuç 80. Bu hatanın neden istisna oluşturmadığını açıklayın ve fonksiyonu düzeltin. Düzelttikten sonra beklenen sonucu bir assert satırıyla denetleyin.
Alıştırma: Dosya verisini doğrula
Kodu çalıştırmadan önce okuyun. Hangi satır atlanır? values listesi ne olur?
Sonra bozuk yerine 30 yazıp yeniden çalıştırın.
Sıra sizde: Dayanıklı kayıt okuyucu
name,score biçiminde dört satırlık bir dosya oluşturun. Bir satırda geçersiz puan bulunsun. Program şunları yapsın:
- Dosyayı
encoding="utf-8"ile açsın - Dosyayı
for line in fileile dolaşsın - Geçerli kayıtları listeye eklesin
- Geçersiz satırı
except ValueError as errorile yakalayıp açık bir mesajla bildirsin - Geçerli puanların ortalamasını yazsın
- Hiç geçerli kayıt yoksa ortalama fonksiyonu bunu
raiseile bildirsin, ana program bu istisnayı yakalayıp mesaj yazsın - Sonuç raporunu ayrı bir dosyaya
"w"kipiyle, eski içeriğin silineceğini bilerek yazsın
Çalışır kod görevi: Programı üç dosyayla çalıştırın: düzgün bir dosya, bir satırı bozuk bir dosya ve hiç geçerli kaydı olmayan bir dosya. Üçünde de program çökmeden bitmelidir.
Tek sayfa özet
with open(...)bloğu bitince dosya kapanır, blok içinde istisna oluşsa bile.- Türkçe karakterli metinler her makinede aynı okunsun diye kodlamayı açıkça
encoding="utf-8"diye yazın. - Dosya satırlarını
for line in fileile dolaşın. Her satır\nile biter, öncestrip()ile temizleyin. "w"var olan dosyanın içeriğini siler,"a"sonuna ekler.- Yalnızca beklediğiniz istisna türünü yakalayın, gerekirse ayrıntısına
as errorile bakın.except Exception: passhataları iz bırakmadan gizler. raisenormal akışı keser ve geçersiz durumu fonksiyonu çağıran koda bildirir. C’deki-1dönüşünün tersine, yakalanmayan istisna gözden kaçmaz.- Kodun çalışması, doğru olduğunu göstermez. Mantık hatası istisna oluşturmaz. Beklenen sonucu
assertile yazarsanız yanlış sonuçAssertionErrorverir. - Traceback’i en alttan okuyun: önce hata türü ve mesajı, sonra hatanın oluştuğu satır.
- Hata mesajını kanıt gibi okuyun, kodu rastgele değiştirmeyin. Her denemede tek bir şeyi değiştirin.
- Gelecek haftadan itibaren “stok eksiye düşemez” gibi kuralları nesnelerin içinde
raiseile koruyacağız.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
with open(...)kalıbını kullanır ve dosyanın blok bitiminde kapandığını bilir."r","w"ve"a"kiplerini ayırır ve"w"kipinin eski içeriği sildiğini bilir.- Metin dosyalarında
encoding="utf-8"yazmanın gerekçesini açıklar. - Söz dizimi, çalışma zamanı ve mantık hatalarını birbirinden ayırır.
raiseileprintfarkını gösterir ve programın kuralı bozulduğunda istisna yükseltir.- Belirli bir istisna türünü yakalar ve istisnayı yakalayıp hiçbir şey yapmamanın neden zararlı olduğunu açıklar.
- Traceback çıktısını hata türünden başlayarak okur.