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 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ı
- 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. İleride karşınıza çıkar, ama bu derste ezberlemeniz beklenmiyor.
Hesabı paylaştıran tarifi nasıl denersiniz?
Arkadaş grubunuz yemekten sonra hesabı eşit paylaştırıyor. Bir arkadaşınız bunun için bir tarif yazdı:
BAŞLA
hesabı al
kişi sayısını al
hesaba yüzde 10 bahşiş ekle
sonucu kişi sayısına böl
her kişinin payını göster
BİTİR
Tarif bir telefon uygulamasına dönüşecek ve her masada kullanılacak. Arkadaşınız şöyle diyor: “Dört kişiyle, 400 TL’lik hesapla denedim. Herkes 110 TL ödedi. Tarif doğru.”
Uygulama yapılmadan önce tarifi sizin denemeniz isteniyor. Şu soruların cevabını sözde kodla, Türkçe ve madde madde yazın:
- Tarif hangi bilgileri alıyor? Bu bilgiler hangi değerleri alamaz?
- Tarifi başka hangi masalarla denersiniz? Her masa için payın ne çıkması gerektiğini denemeden önce yazın.
- Bahşiş oranı tarifte sabit: yüzde 10. Bahşiş bırakmayan ya da yüzde 15 bırakan masa ne yapacak? Bir arkadaşınız “oranı kasadaki tabeladan okusun” diyor. Tabela değişirse bugün yaptığınız denemeler yarın da geçerli olur mu?
- Üç kişi 100 TL’lik hesabı bahşişsiz paylaşıyor. Herkes kaç lira öder? Üç payı toplayınca 100 TL’yi buluyor musunuz?
Python bilmeniz gerekmiyor: “Kişi sayısı 0 olamaz” da bir cevap. Cevaplarınızı bir kenara yazın. Bölümde her birinin Python’da nasıl yazıldığını göreceğiz. Son alıştırmadan önce listenize döneceğiz.
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?
Bu üç sorunun yazılı cevabına fonksiyonun sözleşmesi denir. Aşağıdaki fonksiyon bahşişsiz hesabı kişi sayısına bölüyor. Sözleşmesi şöyle yazılabilir:
Girdi:
- hesap: 0 ya da daha büyük sayı
- kisi: 0'dan büyük tam sayı
Davranış:
- hesap değerini kisi değerine böler
Çıktı:
- kişi başına düşen pay
Hücre ne yazar? Sonra kisi_basi(400, 4) çağrısındaki 4’ü 0 yapıp yeniden çalıştırın.
Sözleşmedeki “kisi 0’dan büyük olmalı” şartı 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. Şart sağlanmazsa Python ZeroDivisionError hatası verir.
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. Aşağıdaki yüzde hesabında oran 3 iken hücre hata verir mi?
Fonksiyon oran = 3 ile de 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.
Hücre ne yazar? Sonra son satırın başındaki # işaretini silip yeniden çalıştırın. Ne olur?
sonuc, 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) kapsamdadır ve fonksiyonun içinden okunabilir. Aşağıdaki hücrede kdv_hesapla(100) iki kez çağrılıyor. İki satır aynı mı yazar?
İlk çağrıda dışarıdaki oran 0.20’dir ve fonksiyon 20.0 döndürür. Sonra oran 0.10 yapılır. Aynı kdv_hesapla(100) çağrısı bu kez 10.0 döndürür. Fonksiyonun sonucu yalnız argümanına değil, dışarıdaki bir değişkene de bağlıdır.
Kod çalışır, fonksiyon dışarıdaki oran değerini okuyabilir. Ama fonksiyonun sonucunu anlamak için artık yalnızca parametrelerine değil, dışarıdaki bir değere de bakmamız gerekir. Aynı çağrı programın neresinde yapıldığına göre farklı sonuç verebilir.
Çoğu hesaplamada oran gibi dışarıdan gelen bir değeri parametreyle vermeyi tercih ederiz:
Bu 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. Aşağıdaki fonksiyonda oran verilmezse 0.10 kullanılır. İki satır ne yazar?
İkinci argüman verilmezse varsayılan değer kullanılır. Çağrıda verilen argüman varsayılanın yerine geçer.
Varsayı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. Bu hücre ekrana bir şey yazar mı? Sonra 5’i 6 yapıp yeniden çalıştırın.
Koşul True ise program hiçbir şey yazmadan devam eder. Koşul False ise program AssertionError hatasıyla durur.
Bir fonksiyonu seçilmiş girdilerle çalıştırıp sonucu önceden yazılmış beklenen değerle karşılaştırmaya test denir. assert ile küçük testler yazabiliriz:
Bu üç 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ını toleransla karşılaştırın
2. haftada bazı ondalık sayıların float olarak tam değil yaklaşık tutulduğunu görmüştük. İki satır ne yazar?
0.1 + 0.2 işleminin sonucu 0.30000000000000004 olur. Bu değer 0.3’e çok yakındır ama bellekte 0.3 ile aynı biçimde tutulmaz. Bu yüzden == karşılaştırması False verir ve assert 0.1 + 0.2 == 0.3 satırı başarısız olur.
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. abs() bir sayının mutlak değerini, yani işaretsiz büyüklüğünü döndürür:
Ö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 kodda hangi satır hata verir? Önce tahmin edin, sonra çalıştırın.
Şimdi ilk if satırını if sayi <= 0: yapın. Çalıştırmadan önce düşünün: davranış değişir mi?
İlk sürümde üç kontrol de geçer ve son satır yazdırılır. if sayi <= 0: yazıldığında 0 için -0 döndürülür. -0 ile 0 aynı değerdir, bu yüzden üç kontrol yine geçer. İki sürüm yalnız 0 değerinde farklı satırdan geçer, ama sonuç aynıdır.
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. 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. Hücredeki üç kontrol geçer mi?
Karar tam 500 değerinde değişir. >= yerine > yazılsaydı yalnız ortadaki kontrol başarısız olurdu.
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
Aşağıdaki fonksiyon 1000 TL ve üstündeki fiyatlara yüzde 10 indirim uygulamalı. Kodda bir hata var ama tek test bunu göstermiyor. Bu test geçer mi?
Bu test yalnız 1500 TL’lik fiyatı sınar. Fonksiyonun 1000’in altındaki ve tam 1000’deki davranışı hakkında bir şey söylemez.
Daha iyi bir test kümesi dört farklı durumu dener. Hangi assert başarısız olur?
Bu 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)
Üçüncü kontrol AssertionError verir. 1000 > 1000 yanlış olduğu için tam 1000 TL’lik fiyata indirim uygulanmaz ve fonksiyon 1000 döndürür. > yerine >= yazılınca dört test de geçer.
Uç durum (edge case)
Kararın değiştiği sayısal eşikler dışında da dikkat isteyen girdiler vardır. Fonksiyonun sıradan girdilerden farklı davranabileceği özel girdilere uç durum (edge case) denir.
Metnin uzunluğunu döndüren şu fonksiyon için boş metin bir uç durumdur. İki kontrol de geçer mi?
Bir liste fonksiyonunda boş liste, bir dosya işleminde boş dosya ya da bir arama probleminde hiç eşleşme bulunmaması da uç durum olabilir. Hesap paylaştıran bir fonksiyon için tek kişilik masa da bir uç durumdur.
Saf fonksiyon fikri
Aşağıdaki fonksiyon yalnızca argümanlarını kullanır ve bir değer döndürür:
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: sonucu yalnız argümanlar belirler.
Şimdi şu fonksiyona bakın. ekle(5) iki kez çağrılıyor. Son satır ne yazar?
Bu fonksiyon dışarıdaki toplam değişkenini değiştirir. Bu bir yan etki (side effect) örneğidir. ekle(5) her çağrıda aynı işi yapar ama programın durumu her seferinde değişir.
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. Hücrede global toplam satırını silip çalıştırarak bunu görebilirsiniz. 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. Hücre kaç satır yazar?
Kod çalışır, ama hesaplama ile ekrana yazma iç içe geçmiştir. Kargo hesabını ayrıca sınamak isteseniz bile fonksiyon bir değer döndürmediği için assert yazamazsınız.
Bunu daha küçük parçalara ayırabiliriz. Programın geri kalanı bu fonksiyonları çağırır:
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. Düzeltmeden önce çalıştırın: hangi assert önce başarısız olur?
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) == 212Soruya dönelim: hesap paylaştırma Python’da
Bölümün başında arkadaşınızın hesap paylaştırma tarifini dört soruyla denemiştiniz. Python’daki karşılıkları şöyle:
| Soru | Sözde kodda | Python’da | Dikkat |
|---|---|---|---|
| Hangi bilgiler, hangi değerler olmaz? | “Kişi sayısı 0 olamaz” | Sözleşmede ön koşul: kisi 0’dan büyük |
Python ön koşulu kendisi denetlemez. kisi 0 olursa ZeroDivisionError verir |
| Başka hangi masalar? | “Bir kişi, sıfır kişi, bahşişsiz masa; payı önceden yaz” | Her masa için bir assert: assert abs(kisi_basi_pay(250, 1) - 275) < 0.01 |
Arkadaşınızın tek denemesi yalnız bir masayı sınar. Tek kişilik masa uç durumdur |
| Bahşiş oranı | “Oran verilmezse yüzde 10” | def kisi_basi_pay(hesap, kisi, bahsis=0.10): |
Oran fonksiyonun dışındaki bir değişkenden (tabeladan) okunursa sonuç dışarıya bağlı kalır ve dünkü testler bugün başka sonuç verebilir. Oranı parametreyle alın |
100 TL, üç kişi |
“Herkes 33,33 TL, toplam bir kuruş eksik” | abs(pay - 33.33) < 0.01 |
Pay 33.333... çıkar, == 33.33 başarısız olur. Formül hesap * (1 + bahsis) / kisi ise 400 TL’lik dört kişilik masada bile 110.00000000000001 çıkar ve == 110 başarısız olur |
Listenizi tabloyla karşılaştırın. Payı denemeden önce yazdıysanız bir test yazmışsınız demektir: assert bu yazdığınızı Python’a denetletir. Arkadaşınızın “Herkes 110 TL ödedi, tarif doğru” cümlesi tek bir test. Sıfır kişilik masayı, tek kişilik masayı ve kuruşları denemediyseniz tarif o masalarda da sınanmamış demektir.
Üret: hesabı paylaştıran fonksiyon
Bölümün başındaki tarifi fonksiyona çevirin. kisi_basi_pay(hesap, kisi, bahsis) adlı fonksiyon şu kurallara uymalı:
bahsisverilmezse0.10kullanılmalı.- Hesaba bahşiş eklenmeli, sonuç kişi sayısına bölünmeli.
- Fonksiyon yalnızca hesaplama yapıp sonucu döndürmeli; içinde
print()kullanılmamalı. - Fonksiyonun üstüne, yorum satırı olarak sözleşmesini ve ön koşulunu yazın.
Başlangıç kodundaki def satırında bahsis parametresi henüz yok. Onu siz ekleyeceksiniz. Testler payı toleransla karşılaştırıyor.
Bahşişli hesap hesap + hesap * bahsis olur. Bunu kisi değerine bölün.
# Girdi: hesap 0 ya da daha büyük sayı, kisi 0'dan büyük tam sayı,
# bahsis 0 ile 1 arası oran (verilmezse 0.10)
# Çıktı: bahşiş eklenmiş hesabın kişi başına düşen payı
# Ön koşul: kisi > 0
def kisi_basi_pay(hesap, kisi, bahsis=0.10):
return (hesap + hesap * bahsis) / kisi
assert abs(kisi_basi_pay(400, 4) - 110) < 0.01
assert abs(kisi_basi_pay(400, 4, 0) - 100) < 0.01
assert abs(kisi_basi_pay(250, 1) - 275) < 0.01
assert abs(kisi_basi_pay(100, 3, 0) - 33.33) < 0.01
print("Kontroller tamamlandı")Doğ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ı?
- Parametre adları neyi tuttuğunu söylüyor mu?
- 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ı?
- Sıradan 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(),globalya da 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:abs(sonuc - beklenen) < 0.01.- 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.
- Tek bir testin geçmesi yalnız o örneği sınar. Sıradan bir örnek, sınır değeri ve uç durum ayrı ayrı denenir.
- 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ı açıklar ve dış bağımlılığı parametreye çevirmeyi gerekçelendirir.
- Varsayılan parametreli bir çağrıda parametrenin hangi değeri aldığını belirler.
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 fonksiyonu yan etkili fonksiyondan ayırt eder ve
globalkullanımına neden temkinli yaklaşıldığını açıklar.