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 yerel/küresel kapsamı, varsayılan parametreleri ve assert kullanarak beklenen davranışı kontrol etmeyi; sınır değerlerini sistematik biçimde 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?
Bir kez çalıştırıp doğru sonuç görmek yeterli değildir. Bu bölümde bir fonksiyonun ne zaman doğru sayılacağını önceden yazmayı ve bunu seçilmiş birkaç örnekle sınamayı öğreneceğiz.
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.
Kapsam (scope): ad nerede geçerlidir?
- haftada fonksiyon içindeki değişkenlerin çoğunlukla fonksiyonun kendi çalışma alanında kaldığını söylemiştik. Şimdi bunu açıklaştıralı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. Fonksiyon çağrısı dışındaki kod bu yerel adı doğrudan 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.
Başlangıç düzeyinde tercihimiz çoğu hesaplama için bağımlılığı parametreyle açıkça vermektir:
def kdv_hesapla(tutar, oran):
return tutar * oranBu sürüm, fonksiyonun hangi bilgiye ihtiyaç duyduğunu imzasında görünür kılar.
Aynı isim fonksiyon içinde ve dışında kullanılabilir; bunların her zaman aynı değişken olduğunu varsaymayın. Bir adı nerede oluşturduğunuz ve nerede kullandığınız önemlidir. Bu derste ayrıntılı LEGB kapsam kurallarına girmiyoruz; yerel/küresel ayrımı ve parametre kullanım ilkesi yeterlidir.
Varsayılan parametreler
Bazı fonksiyonlarda bir parametrenin çoğu çağrıda aynı değeri kullanması mantıklı olabilir. Bu durumda 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.0İhtiyaç olduğunda çağrıdaki argüman varsayılanı değiştirir:
print(indirimli_fiyat(1000, 0.25)) # 750.0Bu fonksiyonun sözleşmesinde varsayılanı açıkça belirtmek gerekir:
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ğer, gerçekten makul bir varsayılan davranış varsa kullanılmalıdır. Önemli bir iş kuralını çağıranın fark etmeyeceği biçimde gizlemek için varsayılan parametre eklemeyin.
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. 13. haftada geçersiz koşulları çalışma zamanında açıkça bildirmek için istisna üretmeyi (raise) ayrıca göreceğiz.
float sonuçlarında tam eşitlik her zaman uygun değildir
- haftada bazı ondalık sayıların
floatolarak yaklaşık temsil edildiğini görmüştük. Bu nedenle şu beklenti başarısız olabilir:
# assert 0.1 + 0.2 == 0.3Çünkü 0.1 + 0.2 tipik olarak 0.30000000000000004 değerine çok yakındır, ama 0.3 ile bit düzeyinde aynı temsil olmayabilir.
Bir hesapta küçük bir sayısal fark kabul edilebiliyorsa sonuç ile beklenen değer arasındaki farkı bir tolerans ile sınayabiliriz:
sonuc = 0.1 + 0.2
beklenen = 0.3
assert abs(sonuc - beklenen) < 0.000001Para bağlamında örneğin bir kuruştan küçük farkı kabul etmek istiyorsak problem kuralına göre şöyle bir tolerans seçilebilir:
assert abs(sonuc - beklenen) < 0.01Toleransın değeri “her zaman 0.01 kullanın” biçiminde evrensel bir kural değildir. Kabul edilebilir farkı problem alanı belirler. Ayrıca ekranda iki ondalık basamak göstermek, hesaplamayı otomatik olarak kesin ondalık aritmetiğe dönüştürmez.
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.
Buradaki global toplam satırı, fonksiyon içindeki atamanın yeni bir yerel toplam oluşturması yerine dışarıdaki küresel toplam değişkenini değiştirmesine izin verir. global bu örnekte önerilen çözüm kalıbı olarak değil, yan etkiyi görünür kılmak için kullanılıyor.
Bu derste hesaplama fonksiyonlarında global kullanmayı varsayılan çözüm olarak benimsemeyeceğiz. Bir fonksiyonun bilgiye ihtiyacı varsa onu parametreyle almak; sonuç üretiyorsa return ile döndürmek genellikle bağımlılıkları daha açık ve test edilebilir kılar.
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ı?
- 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?
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.
Tek sayfa özet
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.
- Yerel değişken fonksiyonun kendi kapsamında yaşar; dış bağımlılıkları parametrelerle açık hâle getirmek çoğu hesaplama fonksiyonunu daha anlaşılır yapar.
- Varsayılan parametre, argüman verilmediğinde kullanılacak açık bir varsayılan davranış sağlar.
global, fonksiyonun dışarıdaki küresel bir adı değiştirmesine izin verebilir; bu derste hesaplama fonksiyonlarında varsayılan çözüm olarak kullanılmaz.assert, geliştiricinin beklediği bir davranışı hızlıca doğrulamak için kullanılabilir.floatsonuçlarında tam eşitlik yerine problem alanına uygun toleransla karşılaştırma gerekebilir.- 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.
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.