Fonksiyon tasarımı ve doğrulama
Fonksiyon yazarken def satırını doğru yazmak yetmez. Fonksiyonun hangi girdileri beklediğini, hangi sonucu döndürdüğünü ve yanlış kullanıldığında ne olacağını da düşünmek gerekir.
Bu hafta her fonksiyon için ne alacağını ve ne döndüreceğini baştan yazacağız. Yerel ve küresel kapsamı, varsayılan parametreleri göreceğiz. Beklenen sonucu assert ile kontrol etmeyi ve sınır değerlerini atlamadan denemeyi öğreneceğiz.
Bu bölümün kapsamı
- Fonksiyon sözleşmesi ve ön koşullar
- Yerel değişkenin kapsamı ve dış bağımlılığı parametreye çevirme
- Varsayılan parametrenin davranışı
assertile beklenen davranışı doğrulamafloatsonuçlarda tam eşitlik yerine toleranslı karşılaştırma- Sınır ve uç durumların seçilerek sınanması
- Saf fonksiyonun özellikleri ve
globalkullanımına temkin
unittestvepytestgibi test çerçeveleri- Tip ipuçları (type hints)
Bu başlıklar konunun devamıdır; ileride karşınıza çıkar ama bu derste ezberlemeniz beklenmiyor.
Bu fonksiyonun doğru çalıştığını nereden biliyorum?
Fonksiyonu bir kez çalıştırıp doğru sonuç görmeniz, başka girdilerde de doğru çalışacağını göstermez. Örneğin 60 ve üstünü geçer sayması gereken bir fonksiyon 75 için doğru, 60 için yanlış cevap verebilir. Bu bölümde fonksiyonun hangi girdide ne döndürmesi gerektiğini önceden yazacağız, sonra bunu özenle seçilmiş birkaç örnekle sınayacağız.
Fonksiyon sözleşmesi
Bir fonksiyonun nasıl kullanılacağını düşünmek için üç basit soru sorabiliriz:
- Ne alıyor?
- Ne yapıyor?
- Ne döndürüyor?
Örneğin:
def saat_ucreti(toplam_ucret, saat):
return toplam_ucret / saatBu üç sorunun yazılı cevabına fonksiyonun sözleşmesi denir. saat_ucreti için sözleşme şöyle yazılabilir:
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
Buradaki saat > 0 bir ön koşuldur, yani fonksiyon çağrılmadan önce girdinin sağlaması gereken bir şart. Bir sayı sıfıra bölünemediği için bu şart gerekir.
Sözleşmeyi Python çalıştırmaz. Fonksiyonu yazan ve kullanan kişi için tutulan bir nottur: fonksiyon ne bekler, ne döndürür.
Ö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 * oranoran = 3 verildiğinde de fonksiyon hata vermeden çalışır. Ama problemimizde oran 0 ile 1 arasında olmalıysa bunu sözleşmeye yazmalıyız:
Ön koşul:
0 <= oran <= 1
Beklenen sonuç da şöyle yazılır:
Sonuç:
tutar * oran değeri döndürülür
Ön koşulu ve sonucu yazdığınızda test yazmak da kolaylaşır, çünkü neyi deneyeceğiniz bellidir.
Kapsam (scope): ad nerede geçerlidir?
6. haftada fonksiyon içindeki değişkenlerin çoğunlukla fonksiyonun kendi çalışma alanında kaldığını söylemiştik. Bir adın kullanılabildiği kod bölgesine kapsam (scope) denir. Şimdi bunu biraz daha açalım.
def cevre_hesapla(kenar):
sonuc = kenar * 4
return sonuc
print(cevre_hesapla(5))
# print(sonuc) # NameErrorsonuc, fonksiyonun içinde oluşturulan yerel (local) bir değişkendir. Fonksiyonun dışındaki kod bu yerel adı kullanamaz.
Fonksiyon dışında oluşturulan bir ad ise küresel (global) kapsamda bulunabilir:
oran = 0.20
def kdv_hesapla(tutar):
return tutar * oranBu kod çalışır; fonksiyon dışarıdaki oran değerini okuyabilir. Fakat fonksiyonun sonucunu anlamak için artık yalnızca parametrelerine değil, dışarıdaki bir değere de bakmamız gerekir.
Çoğu hesaplamada oran gibi dışarıdan gelen bir değeri parametreyle vermeyi tercih ederiz:
def kdv_hesapla(tutar, oran):
return tutar * oranBu sürümde fonksiyonun hangi bilgiye ihtiyaç duyduğu imzasından, yani def satırından okunur.
Aynı ad fonksiyonun içinde ve dışında kullanılabilir. Bu iki adın her zaman aynı değişken olduğunu varsaymayın; adın nerede oluşturulduğuna ve nerede kullanıldığına bakın. Python’ın bir adı hangi sırayla aradığını anlatan LEGB kuralları sınavda sorulmaz. Yerel ile küresel arasındaki farkı ve dışarıdan gelen değeri parametreyle alma alışkanlığını bilmeniz yeterlidir.
Varsayılan parametreler
Bazen bir parametre çoğu çağrıda aynı değeri alır. Böyle bir parametreye tanımda varsayılan değer (default value) verebiliriz:
def indirimli_fiyat(fiyat, oran=0.10):
return fiyat - fiyat * oranİkinci argüman verilmezse varsayılan %10 kullanılır:
print(indirimli_fiyat(1000)) # 900.0Gerektiğinde çağrıda verilen argüman varsayılanın yerine geçer:
print(indirimli_fiyat(1000, 0.25)) # 750.0Varsayılan değer sözleşmeye de yazılır:
fiyat: indirim uygulanacak tutar
oran: indirim oranı; verilmezse 0.10
Zorunlu parametreler, başlangıç düzeyinde varsayılan değerli parametrelerden önce yazılır: def f(a, b=1): .... def f(a=1, b): ... geçerli değildir.
Varsayılan değeri, çağrıların çoğunda gerçekten o değer kullanılıyorsa verin. Çağıranın mutlaka bilmesi gereken bir kuralı varsayılan değerin arkasına saklamayın.
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 program AssertionError hatasıyla durur.
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 pozitif sayıyı, sıfırı ve negatif sayıyı ayrı ayrı dener.
Kullanıcının yanlış girdiği değerleri assert ile yakalamaya çalışmayın. Burada assert’ü, programcı olarak “bu koşul doğru olmalı” dediğiniz yerleri denetlemek için kullanıyoruz. Program çalışırken geçersiz bir durumu bildirmek için istisna yükseltmeyi (raise) 13. haftada göreceğiz.
float sonuçlarında tam eşitlik her zaman uygun değildir
2. haftada bazı ondalık sayıların float olarak tam değil yaklaşık tutulduğunu görmüştük. Bu yüzden şu kontrol başarısız olabilir:
# assert 0.1 + 0.2 == 0.30.1 + 0.2 işleminin sonucu 0.30000000000000004 değerine çok yakındır ve bellekte 0.3 ile aynı biçimde tutulmayabilir.
Hesapta küçük bir fark kabul edilebiliyorsa sonuç ile beklenen değer arasındaki farkın, kabul ettiğimiz küçük bir sınırdan (tolerans) az olup olmadığına bakarız:
sonuc = 0.1 + 0.2
beklenen = 0.3
assert abs(sonuc - beklenen) < 0.000001Örneğin parayla çalışırken bir kuruştan küçük farkları önemsemiyorsak toleransı 0.01 seçebiliriz:
assert abs(sonuc - beklenen) < 0.01Tolerans için “her zaman 0.01 kullanın” gibi bir kural yok; ne kadar farkın kabul edilebileceğine problemin kendisi karar verir. Ayrıca sonucu ekranda iki ondalık basamakla göstermek, hesabın kendisini kesinleştirmez.
Tahmin et: hangi assert başarısız olur?
Aşağıdaki kodu çalıştırmadan önce hangi satırın hata vereceğ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 en çok sınır değerlerini, yani kararın değiştiği değerleri denemek gerekir.
Bir ücretsiz kargo fonksiyonu düşünelim:
def kargo_ucreti(sepet):
if sepet >= 500:
return 0
return 39.90Burada 100 ve 1000 gibi sınırdan uzak değerleri denemek yetmez. Sınırın hemen altını, kendisini ve hemen üstünü deneyin:
assert kargo_ucreti(499) == 39.90
assert kargo_ucreti(500) == 0
assert kargo_ucreti(501) == 0Karar tam 500 değerinde değişir.
Alıştırma: sınır testini düzeltin
Aşağıdaki fonksiyonda geçme sınırı 60 olmalıdır. Şu anki kod sınır değerinde yanlış sonuç veriyor.
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ı")Tek test yalnız bir durumu sınar
Şu fonksiyona bakalım:
def indirim(fiyat):
if fiyat >= 1000:
return fiyat * 0.90
return fiyatSadece şu testi yazarsak:
assert indirim(1500) == 1350fonksiyonun 1000’in altındaki fiyatlarda doğru çalışıp çalışmadığını bilemeyiz.
Daha iyi bir test kümesi:
assert indirim(500) == 500
assert indirim(999) == 999
assert indirim(1000) == 900
assert indirim(1500) == 1350Bu dört kontrol sırasıyla şunları dener:
- Sıradan bir fiyat (
500) - Sınırın hemen altı (
999) - Sınırın kendisi (
1000) - Sınırın üstü (
1500)
Uç durum (edge case)
Dikkat edilmesi gereken her girdi sayısal bir eşik değildir. Fonksiyonun sıradan girdilerden farklı davranabileceği özel girdilere uç durum (edge case) denir.
Metnin uzunluğunu döndüren şu fonksiyona bakalım:
def karakter_sayisi(metin):
return len(metin)Bu fonksiyon için boş metin bir uç durumdur:
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ı test etmek kolaydır:
assert cevre(4, 6) == 20
assert cevre(1, 1) == 4Şimdi şu fonksiyona bakın:
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.
global toplam satırı olmasaydı Python, fonksiyonun içinde atanan toplam adını yerel sayardı. Sağ taraftaki toplam o anda henüz değer almamış olacağı için de UnboundLocalError hatası verirdi. global toplam satırı sayesinde atama dışarıdaki küresel toplam değişkenini değiştirir. Bu örnekte global’i önermek için değil, yan etkiyi göstermek için kullandık.
Hesaplama yapan fonksiyonlarda global’i ilk çözüm olarak kullanmayacağız. Fonksiyon bir bilgiye ihtiyaç duyuyorsa onu parametreyle alsın, bir sonuç hesaplıyorsa return ile döndürsün. Böylece fonksiyonun neye bağlı olduğu def satırından okunur ve fonksiyonu test etmek kolaylaşır.
Her fonksiyonu saf yazmak zorunda değiliz. Bu kural hesaplama yapan fonksiyonlar içindir.
Bir fonksiyonun davranışını anlamak için programın başka yerlerindeki değişkenleri takip etmek zorunda kalıyorsanız fonksiyon dışarıdaki değişkenlere gereğinden fazla bağlı 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 birbirinden farklı işleri bir arada yapıyor:
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)Kod çalışır, ama hesaplama ile ekrana yazma iç içe geçmiştir.
Bunu daha küçük parçalara ayırabiliriz:
def urun_toplami(fiyat, adet):
return fiyat * adet
def kargo_ucreti(tutar):
if tutar >= 500:
return 0
return 39.90Programın geri kalanı bu fonksiyonları çağırır:
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 fonksiyonun Celsius sıcaklığını Fahrenheit’a çevirmesi gerekiyor:
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 apartmanda aylık aidat geç ödendiğinde geciken her gün için 2.5 TL gecikme bedeli eklendiğini düşünelim.
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ıç kodu:
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 bitirdiğinizde kendinize şunları sorun:
- Fonksiyonun tek ve açık bir işi var mı?
- Parametrelerin neyi temsil ettiği anlaşılır mı?
- Varsayılan parametre varsa neden o değerin seçildiği açık 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?
floatsonucu karşılaştırırken tam eşitlik gerçekten uygun mu?- Hesaplama fonksiyonu gereksiz
print(),globalveya dış değişken kullanıyor mu?
Her soru her fonksiyon için aynı ölçüde önemli olmayabilir. Yine de bir hata çıktığında rastgele değer denemek yerine bu listeyi sırayla izleyebilirsiniz.
Tek sayfa özet
Bu hafta bir fonksiyonun ne alıp ne döndüreceğini yazmayı ve bunu assert ile sınamayı gördük.
- Fonksiyon sözleşmesi fonksiyonun ne aldığını, ne yaptığını ve ne döndürdüğünü yazar.
- Ön koşul, fonksiyonun hangi girdilerle doğru çalıştığını söyler.
- Yerel değişken yalnızca kendi fonksiyonunun içinde kullanılabilir. Hesaplama fonksiyonu dışarıdan bir değere ihtiyaç duyuyorsa onu parametreyle almak kodu daha anlaşılır yapar.
- Varsayılan parametre, argüman verilmediğinde hangi değerin kullanılacağını açıkça belirler.
global, fonksiyonun dışarıdaki küresel bir değişkeni değiştirmesine izin verir; hesaplama fonksiyonlarında ilk çözüm olarak kullanılmaz.assert, beklediğiniz bir sonucu hızlıca sınar; koşul yanlışsaAssertionErrorverir.floatsonuçlarını karşılaştırırken==yerine probleme uygun bir tolerans gerekebilir.- Koşullu fonksiyonlarda sınırın hemen altı, kendisi ve hemen üstü ayrı ayrı denenir.
- Boş metin, boş liste ya da hiç eşleşme bulunmaması gibi durumlar uç durum olabilir.
- Saf fonksiyon argümanlarını kullanır, sonuç döndürür ve dışarıdaki hiçbir değeri değiştirmez.
- Tek iş yapan küçük fonksiyonları ayrı ayrı sınamak daha kolaydır.
Bir sonraki bölümde fonksiyonlarla birlikte sık kullanacağımız metin (str) değerlerini daha ayrıntılı inceleyeceğiz.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
- Fonksiyon sözleşmesini ve ön koşulları açıklar.
- Yerel değişkenin kapsamını bilir ve dış bağımlılığı parametreye çevirmeyi gerekçelendirir.
- Varsayılan parametrenin davranışını uygular.
assertile beklenen davranışı doğrular.floatsonuçlarda tam eşitlik yerine toleranslı karşılaştırma seçer.- Sınır ve uç durumları seçerek test eder.
- Saf fonksiyonun özelliklerini tanır ve
globalkullanımına neden temkinli yaklaşıldığını açıklar.