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,
  • assert ile 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:

  1. Ne alıyor?
  2. Ne yapıyor?
  3. Ne döndürüyor?

Örneğin:

def saat_ucreti(toplam_ucret, saat):
    return toplam_ucret / saat

Bu 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.

Note

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 * oran

Bu 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 == 5

Koş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) == 4

Bu üç kontrol farklı durumları kapsar.

Important

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.

Fonksiyon sözleşmesindeki girdi, davranış ve çıktı beklentilerinin farklı 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) == 0

500 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 fiyat

Sadece şu testi yazarsak:

assert indirim(1500) == 1350

fonksiyonun 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) == 1350

Burada:

  • 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") == 6

Bir 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) == 4

Karşılaştırmak için:

toplam = 0

def ekle(deger):
    global toplam
    toplam = toplam + deger

Bu 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.

Tip

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.90

Sonra 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_gun 0 ise yalnızca aidat döndürülmeli.
  • Her gecikme günü için 2.5 TL 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) == 510

Doğ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

  1. Fonksiyon sözleşmesini açıklamak için hangi üç temel soru kullanılabilir?
  2. Ön koşul nedir?
  3. assert 2 + 2 == 4 satırı neyi kontrol eder?
  4. Geçme sınırı 60 ise neden 59, 60, 61 iyi bir test grubudur?
  5. Boş metin neden bazı fonksiyonlarda uç durum sayılabilir?
  6. Saf fonksiyonun iki temel özelliği nedir?
  7. 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.

Back to top