Fonksiyon tasarımı ve doğrulama
Bir fonksiyon yazmak, yalnızca def satırını doğru yazmak değildir. Fonksiyonun hangi girdileri beklediğini, hangi sonucu ürettiğini ve yanlış kullanımda ne olacağını düşünmek gerekir.
Bu hafta fonksiyonları küçük birer sözleşme gibi ele alacağız. Ayrıca assert kullanarak beklenen davranışı kontrol etmeyi ve sınır değerlerini sistematik biçimde denemeyi öğreneceğiz.
Bu hafta neleri yapabilmelisiniz?
Bölümün sonunda:
- bir fonksiyonun amacını ve beklenen girdilerini açıkça ifade edebilmeli,
- fonksiyon sözleşmesinin ne olduğunu başlangıç düzeyinde açıklayabilmeli,
- uygun ön koşulları ve beklenen sonuçları belirleyebilmeli,
assertile kısa doğrulama kontrolleri yazabilmeli,- sınır değeri ve uç durumları fonksiyon testlerinde kullanabilmeli,
- saf fonksiyon fikrini açıklayabilmeli,
- bir fonksiyonun birden fazla sorumluluk üstlenmesini fark edebilmeli,
- hatalı bir fonksiyonu örnek girdilerle sistematik biçimde düzeltebilmelisiniz.
Fonksiyon sözleşmesi
Bir fonksiyonun nasıl kullanılacağını düşünmenin basit yollarından biri üç sorudur:
- Ne alıyor?
- Ne yapıyor?
- Ne döndürüyor?
Örneğin:
def saat_ucreti(toplam_ucret, saat):
return toplam_ucret / saatBu fonksiyon için şu sözleşmeyi yazabiliriz:
Girdi:
- toplam_ucret: 0 veya daha büyük sayı
- saat: 0'dan büyük sayı
Davranış:
- toplam_ucret değerini saat değerine böler
Çıktı:
- saat başına ücret
Burada saat > 0 önemli bir ön koşuldur. Çünkü sıfıra bölme yapılamaz.
Fonksiyon sözleşmesi, Python’ın özel bir sözdizimi değildir. Fonksiyonun beklenen kullanımını ve sonucunu açık biçimde düşünme yöntemidir.
Ön koşul ve sonuç
Bir fonksiyonun doğru çalışması için bazı varsayımlar gerekebilir.
Örneğin yüzde hesabı:
def yuzde_hesapla(tutar, oran):
return tutar * oranBu fonksiyon teknik olarak oran = 3 ile de çalışır. Fakat bizim problemimizde oranı 0 ile 1 arasında bekliyorsak bunu tasarımda açıkça belirtmeliyiz.
Ön koşul:
0 <= oran <= 1
Beklenen sonuç ise şöyle ifade edilebilir:
Sonuç:
tutar * oran değeri döndürülür
Bu açıklık, daha sonra test yazmayı kolaylaştırır.
assert ile beklentiyi kontrol etmek
assert, doğru olmasını beklediğimiz bir koşulu kontrol eder.
sonuc = 2 + 3
assert sonuc == 5Koşul True ise program sessizce devam eder. Koşul False ise AssertionError oluşur.
Fonksiyon davranışını küçük örneklerle doğrulayabiliriz:
def kare(sayi):
return sayi * sayi
assert kare(3) == 9
assert kare(0) == 0
assert kare(-2) == 4Bu üç kontrol farklı durumları kapsar.
assert, kullanıcıdan gelen hatalı girdiyi yönetmek için bir arayüz değildir. Burada onu geliştiricinin “bu koşul doğru olmalı” beklentisini doğrulamak için kullanıyoruz.

assert kontrolleriyle doğrulanmasını gösteren şema.Tahmin et — hangi assert başarısız olur?
Aşağıdaki kodu çalıştırmadan önce hangi satırın hata üreteceğini tahmin edin:
Bu sürümde üç kontrol de geçer. Şimdi ilk if satırını if sayi <= 0: yaparsanız davranış değişir mi? Tahmin edin ve deneyin.
Sınır değerlerini fonksiyonlarda kullanmak
Koşul içeren fonksiyonlarda sınır değerleri özellikle önemlidir.
Bir ücretsiz kargo fonksiyonu düşünelim:
def kargo_ucreti(sepet):
if sepet >= 500:
return 0
return 39.90Önemli testler yalnızca 100 ve 1000 değildir:
assert kargo_ucreti(499) == 39.90
assert kargo_ucreti(500) == 0
assert kargo_ucreti(501) == 0500 tam kararın değiştiği noktadır.
Alıştırma — sınır testini düzeltin
Aşağıdaki fonksiyonda geçme sınırı 60 olmalıdır. Mevcut kod sınır değerini yanlış ele alıyor.
Fonksiyonu üç assert de geçecek biçimde düzeltin.
> yerine >= kullanmayı düşünün.
def gecti_mi(puan):
return puan >= 60
assert gecti_mi(59) == False
assert gecti_mi(60) == True
assert gecti_mi(61) == True
print("Kontroller tamamlandı")Bir test tek başına yeterli değildir
Şu fonksiyona bakalım:
def indirim(fiyat):
if fiyat >= 1000:
return fiyat * 0.90
return fiyatSadece şu testi yazarsak:
assert indirim(1500) == 1350fonksiyonun düşük fiyatlarda doğru çalıştığını bilmiyoruz.
Daha dengeli testler:
assert indirim(500) == 500
assert indirim(999) == 999
assert indirim(1000) == 900
assert indirim(1500) == 1350Burada:
- normal durum,
- sınırın hemen altı,
- sınırın kendisi,
- sınırın üstü
kontrol edilir.
Uç durum (edge case)
Her problemde sınır değeri sayısal bir eşik olmak zorunda değildir.
Örneğin metnin uzunluğunu döndüren fonksiyon:
def karakter_sayisi(metin):
return len(metin)Boş metin önemli bir uç durumdur (edge case):
assert karakter_sayisi("") == 0
assert karakter_sayisi("Python") == 6Bir liste fonksiyonunda boş liste, bir dosya işleminde boş dosya veya bir arama probleminde hiç eşleşme bulunmaması benzer uç durumlar olabilir.
Saf fonksiyon fikri
Aşağıdaki fonksiyon yalnızca argümanlarını kullanır ve bir değer döndürür:
def cevre(uzunluk, genislik):
return 2 * (uzunluk + genislik)Aynı argümanlarla çağrıldığında aynı sonucu verir ve dışarıdaki bir değeri değiştirmez. Bu tür fonksiyonlara saf fonksiyon (pure function) denir.
Saf fonksiyonların önemli bir avantajı test edilmelerinin kolay olmasıdır:
assert cevre(4, 6) == 20
assert cevre(1, 1) == 4Karşılaştırmak için:
toplam = 0
def ekle(deger):
global toplam
toplam = toplam + degerBu fonksiyon dışarıdaki toplam değişkenini değiştirir. Bu bir yan etki (side effect) örneğidir.
Başlangıç programlarında her şeyi saf fonksiyon yapmak zorunda değiliz; ancak hesaplama yapan fonksiyonlarda mümkün olduğunca argüman alıp değer döndürmek iyi bir varsayılan yaklaşımdır.
Bir fonksiyonun davranışını anlamak için programın başka yerlerindeki değişkenleri takip etmek zorunda kalıyorsanız fonksiyon gereğinden fazla dış duruma bağımlı olabilir.
Alıştırma — saf hesaplama fonksiyonu
Aşağıdaki fonksiyon, kilometre cinsinden mesafeyi metreye çevirmelidir.
Fonksiyonu tamamlayın.
return km * 1000 yeterlidir.
def metreye_cevir(km):
return km * 1000
assert metreye_cevir(1) == 1000
assert metreye_cevir(0) == 0
assert metreye_cevir(2.5) == 2500
print("Kontroller tamamlandı")Fonksiyon tek bir işi mi yapıyor?
Aşağıdaki fonksiyon çok farklı sorumlulukları bir araya getiriyor:
def siparis_isle(fiyat, adet):
toplam = fiyat * adet
if toplam >= 500:
kargo = 0
else:
kargo = 39.90
print("Ürün toplamı:", toplam)
print("Kargo:", kargo)
print("Genel toplam:", toplam + kargo)Bu kod çalışabilir; fakat hesaplama ile çıktı üretme iç içedir.
Daha küçük parçalar düşünebiliriz:
def urun_toplami(fiyat, adet):
return fiyat * adet
def kargo_ucreti(tutar):
if tutar >= 500:
return 0
return 39.90Sonra programın ana akışı bunları kullanabilir:
toplam = urun_toplami(120, 3)
kargo = kargo_ucreti(toplam)
print("Genel toplam:", toplam + kargo)Bu tasarımda iki hesaplamayı ayrı ayrı doğrulayabiliriz.
İncele ve düzelt — yanlış formül
Aşağıdaki fonksiyon Celsius sıcaklığını Fahrenheit’a çevirmeyi amaçlıyor:
Formülde eksik olan kısmı bulun ve düzeltin.
Formül: celsius * 9 / 5 + 32.
def fahrenheit(celsius):
return celsius * 9 / 5 + 32
assert fahrenheit(0) == 32
assert fahrenheit(100) == 212Üret — aidat gecikme hesabı
Bir apartman yönetiminde aylık aidatın zamanında ödenmediği her gün için 2.5 TL gecikme bedeli eklendiğini varsayalım.
gecikmeli_tutar(aidat, geciken_gun) adlı bir fonksiyon yazın.
Kurallar:
geciken_gun0ise yalnızca aidat döndürülmeli.- Her gecikme günü için
2.5TL eklenmeli. - Fonksiyon yalnızca hesaplama yapıp sonuç döndürmeli; içinde
print()kullanılmamalı.
Başlangıç:
Toplam gecikme bedeli geciken_gun * 2.5 olur.
def gecikmeli_tutar(aidat, geciken_gun):
return aidat + geciken_gun * 2.5
assert gecikmeli_tutar(500, 0) == 500
assert gecikmeli_tutar(500, 1) == 502.5
assert gecikmeli_tutar(500, 4) == 510Doğrulama için küçük bir kontrol listesi
Bir fonksiyonu tamamladıktan sonra şu sorular yararlıdır:
- Fonksiyonun tek ve açık bir işi var mı?
- Parametrelerin neyi temsil ettiği anlaşılır mı?
- Hangi girdilerin geçerli olduğu belli mi?
- Dönüş değerinin anlamı açık mı?
- Normal bir örnek denendi mi?
- Sınır değeri denendi mi?
- Uç durum varsa denendi mi?
- Hesaplama fonksiyonu gereksiz
print()veya dış değişken kullanıyor mu?
Bu soruların hepsi her fonksiyonda aynı derecede önemli olmayabilir. Ama bir hata olduğunda rastgele değer denemek yerine sistematik düşünmeye yardım eder.
Bölüm özeti
Bu hafta fonksiyonları yalnızca kod tekrarını azaltan yapılar olmaktan çıkarıp küçük, doğrulanabilir bileşenler olarak ele aldık.
- Fonksiyon sözleşmesi, beklenen girdi, davranış ve sonucu açıklar.
- Ön koşullar fonksiyonun hangi girdilerle anlamlı olduğunu belirtir.
assert, geliştiricinin beklediği bir davranışı hızlıca doğrulamak için kullanılabilir.- Koşullu fonksiyonlarda sınır değerleri özellikle önemlidir.
- Boş metin, boş koleksiyon veya hiç eşleşme bulunmaması gibi durumlar uç durum olabilir.
- Saf fonksiyonlar argümanlarını kullanır, sonuç döndürür ve dış durumu değiştirmez.
- Küçük ve tek sorumluluklu fonksiyonları ayrı ayrı doğrulamak daha kolaydır.
Kendinizi kontrol edin
- Fonksiyon sözleşmesini açıklamak için hangi üç temel soru kullanılabilir?
- Ön koşul nedir?
assert 2 + 2 == 4satırı neyi kontrol eder?- Geçme sınırı
60ise neden59,60,61iyi bir test grubudur? - Boş metin neden bazı fonksiyonlarda uç durum sayılabilir?
- Saf fonksiyonun iki temel özelliği nedir?
- Hesaplama yapan bir fonksiyonun içindeki gereksiz
print()çağrıları neden tasarımı zorlaştırabilir?
Bir sonraki bölümde fonksiyonlarla birlikte sık kullanacağımız metin (str) değerlerini daha ayrıntılı inceleyeceğiz.