Fonksiyonlar, kapsam ve modüller
C’de programı fonksiyonlara bölmeyi öğrendiniz. Nesne tabanlı programlamada da bu beceriye ihtiyacınız olacak, çünkü ileride yazacağımız sınıfların içindeki metotlar da birer fonksiyondur. Bu hafta Python’da fonksiyon yazacak ve kodu dosyalara böleceğiz. Söz dizimi C’ye yakın. Bölümde en çok C’den farklı davranan yerlerde duracağız: parametrenin varsayılan değeri, fonksiyonun dışındaki bir ada atama ve fonksiyona verilen listenin değişmesi.
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ı
print()ilereturnfarkı ve örtükNone- Varsayılan parametre ve anahtar kelime argümanı
- Değiştirilebilir (mutable) varsayılan değer tuzağı ve
Nonenöbetçisi - Yerel ad ile dış kapsamdaki ad,
UnboundLocalError - Değiştirilebilir parametrede yan etki
- Docstring ile davranış sözleşmesi
__name__ == "__main__"kalıbının ne işe yaradığı
- Değişken sayıda argüman (
*args,**kwargs) - Lambda ifadeleri ve closure
- Paket yayımlama (
pip install, PyPI)
Bu başlıklar konunun devamıdır. İleride karşınıza çıkar, ama bu derste ezberlemeniz beklenmiyor.
Not programının beş işini C’de nasıl yapardınız?
Bir not programını fonksiyonlara ve dosyalara bölüyorsunuz. Program şu beş işi yapacak:
- Bir öğrencinin ortalamasını hesaplamak ve sonucu başka bir hesapta kullanmak
- Geçme sınırını çoğu derste 50 almak, bazı derslerde 60 verebilmek
- Programın kaç öğrencinin ortalamasını hesapladığını saymak
- Bir öğrencinin not listesine yeni not ekleyen bir fonksiyon yazmak
- Bu fonksiyonları ayrı bir dosyaya koyup başka bir programda da kullanmak
Programlama Temelleri’nde C ile yazdığınız programları düşünün. Bu işlerin her birini C’de nasıl yapardınız? Okumaya devam etmeden önce her iş için bir satır yazın. Python bilgisi gerekmiyor: “iki ayrı fonksiyon yazarım” da bir cevap.
Bölümde bu beş işin Python’da nasıl yazıldığını göreceğiz. Bölümün sonunda listenize döneceğiz.
Fonksiyon tanımlamak: def
Ortalamayı programın birkaç yerinde hesaplayacaksanız bu kodu bir fonksiyona koyarsınız. Python’da fonksiyon def ile tanımlanır. C’deki gibi dönüş türü ve parametre türleri yazılmaz. def satırı : ile biter, gövdeyi girinti belirler. Hücre ne yazar: 80 mi, 80.0 mı?
first ve second parametrelerdir, çağrıdaki 70 ve 90 argümanlardır. 1. haftada gördüğünüz gibi / her zaman float döndürür.
C’de bir fonksiyonu çağırmadan önce onu tanımlamanız ya da prototipini yazmanız gerekirdi. Python’da prototip yoktur, ama def satırı fonksiyonu çağıran satırdan önce çalışmış olmalıdır. Python dosyayı yukarıdan aşağı çalıştırdığı için fonksiyonlar dosyanın başına yazılır.
Fonksiyonun adına bakan kişi, işin nasıl yapıldığından önce ne yapıldığını anlamalıdır. Bu yüzden calculate_average (ortalama hesapla), process ya da do_it gibi belirsiz adlardan daha açıklayıcıdır.
print() ekrana yazar, return çağırana verir
C’de ortalamayı printf ile yazdıran void bir fonksiyonun sonucunu bir değişkene atamaya kalkarsanız derleyici hata verirdi. Python’da bu atama hata vermez. Aşağıdaki iki fonksiyondan biri sonucu yazdırıyor, öteki döndürüyor. Ekranda kaç satır görünür? first: satırında ne yazar?
return içermeyen bir fonksiyonun çağrısı None verir. 3. haftada .sort() çağrısının döndürdüğü None da buydu: Python’da “değer yok” anlamına gelen özel değer. Ekrana yazılan bir sayıyı programın geri kalanında kullanamazsınız. Sonucu başka bir hesapta kullanacaksanız return yazın.
first = show_square(5) satırı çalışırken gövdedeki print() ekrana 25 yazar. Fonksiyon bir değer döndürmediği için first adı None’a bağlanır ve first: None görünür. get_square ise ekrana bir şey yazmaz, 25 değerini döndürür. Son satır second: 25 olur. Ekranda toplam üç satır vardır.
Varsayılan parametre ve anahtar kelime argümanı
C’de bir parametreye varsayılan değer veremezdiniz: geçme sınırını her çağrıda yazar ya da iki ayrı fonksiyon tanımlardınız. Python’da parametreye tanımda bir değer verebilirsiniz. Üç satır ne yazar?
limit=50 bir varsayılan değerdir: çağrıda limit verilmezse 50 kullanılır. Üçüncü çağrıdaki score=70 ve limit=60 ise anahtar kelime argümanıdır, yani değer parametrenin adıyla birlikte verilir. Birden çok isteğe bağlı parametre olduğunda hangi değerin hangi parametreye gittiği böylece çağrı satırından okunur.
Değiştirilebilir varsayılan değer tuzağı
Varsayılan değer bir liste olursa ne olur? Aşağıdaki fonksiyon, liste verilmediğinde boş bir listeyle başlayacak gibi görünüyor. İki çağrı ne yazar?
Varsayılan [] her çağrıda yeniden oluşturulmaz. Python onu def satırı çalışırken bir kez oluşturur ve sonraki çağrılar aynı listeyi paylaşır. C’deki yerel dizi her çağrıda yeniden kurulurdu. Burada öyle olmaz.
İlk çağrı varsayılan listeye "A" ekler ve ['A'] yazar. İkinci çağrı yeni bir liste almaz, aynı listeye "B" ekler ve ['A', 'B'] yazar.
Sorunu önlemek için şu kalıp kullanılır. Bu kez iki çağrı ne yazar?
Burada None, “çağıran liste vermedi” anlamına gelen bir işarettir. Bu işi gören değere nöbetçi (sentinel) denir. Liste artık gövdede oluşturulduğu için her çağrı kendi listesini alır.
7. haftada sınıflarla çalışırken benzer bir sorunla karşılaşacağız. Onun nedeni de aynı: değiştirilebilir bir nesneyi farkında olmadan birden çok yerde ortak kullanmak.
Yerel kapsam, global ad ve UnboundLocalError
Kaç öğrencinin ortalamasının hesaplandığını saymak için C’de çoğu zaman global bir değişken tutar, fonksiyonun içinde count++ yazardınız. Python’da da fonksiyon dışarıdaki bir adı okuyabilir. Hücre ne yazar? Son satırın başındaki # işaretini silip yeniden çalıştırırsanız ne olur?
tax fonksiyonun içinde oluşturulduğu için yerel bir addır. C’deki yerel değişkenler gibi yalnızca o fonksiyonun içinde kullanılabilir, dışarıdan okumaya çalışırsanız NameError alırsınız. rate ise fonksiyonun dışında tanımlıdır ve fonksiyon onu dış kapsamdan (scope) okur. Kapsam, bir adın programın hangi bölümünde geçerli olduğudur.
Okumak sorun çıkarmıyor. Şimdi sayacı fonksiyonun içinde artıralım. increase() çağrılınca ilk satırdaki print(count) ne yazar?
Python bir adın yerel olup olmadığına satır satır değil, fonksiyonun tamamına bakarak karar verir. Gövdede count’a atama olduğu için count baştan yerel sayılır. print satırı çalıştığında bu yerel count’a henüz değer verilmemiştir ve Python UnboundLocalError verir.
Dıştaki count’u değiştirmek istiyorsanız bunu global ile açıkça söylemeniz gerekir:
count = 10
def increase():
global count
count += 1Ama her sorunu global ile çözerseniz hangi fonksiyonun hangi değişkeni değiştirdiğini izlemek zorlaşır. Bu derste mümkün olduğunca girdiyi parametreyle alıp sonucu return ile vereceğiz.
Değiştirilebilir parametre ve yan etki
C’de not dizisine eleman ekleyen bir fonksiyona diziyi ve eleman sayısının adresini verirdiniz. Dizi fonksiyona adresiyle gittiği için fonksiyon asıl diziyi değiştirirdi. Python’da listeyi fonksiyona verince ne olur? add_score hiçbir şey döndürmüyor. numbers değişir mi?
Çağrı sırasında scores parametresi, çağıranın verdiği listenin kendisine bağlanır. Python listeyi kendiliğinden kopyalamaz. append() bu ortak listeyi değiştirdiği için numbers da yeni içeriği gösterir. 3. haftadaki second = first deneyiyle aynı durum: iki ad, bir liste. Bir fonksiyonun, dönüş değeri dışında dışarıda bıraktığı böyle bir değişikliğe yan etki (side effect) denir.
C’de int parametre kopyalanırdı ve fonksiyondaki değişiklik dışarıya yansımazdı. Python’da fonksiyona bir sayı verelim. add_one gövdesinde number += 1 yazıyor. Son satır ne yazar?
Dışarıdaki sayı C’deki gibi değişmez, ama nedeni farklıdır. int değiştirilemez bir nesnedir. number += 1 o nesneyi değiştirmez, yerel number adını yeni bir sayı nesnesine bağlar. total hâlâ eski nesneyi gösterir. Bir adı başka bir nesneye bağlamaya 6. haftada ayrıntılı olarak döneceğiz.
Bu davranışı “Python her şeyi referansla geçirir” diye genellemek yanıltıcı olabilir. Bu derste şöyle düşüneceğiz: her ad bir nesneye bağlanır, fonksiyon çağrılınca parametre adı da verilen nesneye bağlanır. Değiştirilebilir bir nesne yerinde değiştirilirse, o nesneye bağlı bütün adlar değişikliği görür.
Yan etki bazen istenir, bazen beklenmedik bir hataya yol açar. Çağıranın listesi değişmesin istiyorsak yeni bir liste döndürürüz. İki liste + ile birleştirilince yeni bir liste oluşur, iki taraf da değişmez. İki satır ne yazar?
Fonksiyonu çağıran kişi, listesinin değişip değişmeyeceğini nereden bilir? Bunu fonksiyonun sözleşmesi söyler: fonksiyonun ne aldığını, ne döndürdüğünü ve neyi değiştirdiğini anlatan tanım.
Docstring: davranış sözleşmesini koda yazmak
C’de fonksiyonun ne yaptığını prototipin üstüne yazdığınız bir yorumla anlatırdınız. Python’da bu açıklamanın belli bir yeri var. Docstring, fonksiyon tanımının hemen altına üç tırnak içinde yazılan açıklama metnidir. Kısa bir docstring, fonksiyonun ne beklediğini, ne döndürdüğünü, varsa önemli yan etkilerini ve hangi durumda hata verdiğini yazar:
def calculate_discount(price, rate=0.10):
"""Pozitif fiyat için indirimli tutarı döndürür.
price negatifse ValueError yükseltir. Girdileri değiştirmez.
"""
if price < 0:
raise ValueError("Fiyat negatif olamaz")
return price * (1 - rate)help(calculate_discount) yazarsanız Python bu metni ekranda gösterir. Buradaki raise satırı, fiyat negatifse hatayı çağırana bildirir. raise, try ve except arasındaki ilişkiyi 5. haftada ayrıntılı olarak göreceğiz.
Fonksiyonları sorumluluğa göre ayırmak
Bir fonksiyon hem veri alır, hem doğrular, hem hesaplar, hem dosyaya yazar, hem de ekrana yazarsa onu test etmek ve değiştirmek zorlaşır.
Örneğin calculate_discount yalnızca indirimi hesaplar ve fiyatın negatif olmadığını denetler. Kullanıcıdan fiyatı alan kod başka bir fonksiyonda durabilir.
Modüller: kodu dosyalara ayırmak
C’de bir fonksiyonu başka bir dosyada kullanmak için prototipini .h dosyasına yazar, onu #include ile ekler ve iki .c dosyasını birlikte derlerdiniz. Python’da başlık dosyası yoktur. Her .py dosyası bir modüldür: başka bir dosya onun içindeki fonksiyonları import ile içe aktarıp çağırabilir. Örneğin:
# pricing.py
def final_price(price, tax_rate):
return price * (1 + tax_rate)Aynı klasördeki app.py dosyasında:
from pricing import final_price
print(final_price(100, 0.20))from pricing import final_price satırı pricing.py dosyasını bulur, çalıştırır ve içindeki final_price fonksiyonunu app.py içinde kullanılabilir hâle getirir. import pricing yazarsanız fonksiyonu pricing.final_price(100, 0.20) diye çağırırsınız.
Kod hücreleri tek dosyayla çalıştığı için modül örneklerini kendi bilgisayarınızda deneyin. 1. haftada kurduğunuz VS Code’da bir klasör açın, iki dosyayı bu klasöre kaydedin ve app.py dosyasını çalıştırın. Windows’ta terminale py app.py yazabilirsiniz. Programı çalıştırdıktan sonra klasörde __pycache__ adlı bir dizin belirir. Python, içe aktardığı modülün bytecode’unu (1. haftada gördüğümüz ara kodu) orada saklar. Bu dizini silebilirsiniz, Python gerektiğinde yeniden oluşturur.
__name__ == "__main__" ne işe yarar?
C’de fonksiyonları ayrı bir .c dosyasına taşıdığınızda o dosyaya main() yazmazdınız, çünkü bir programda tek bir main() bulunur. Python’da import, modülün bütün satırlarını bir kez çalıştırır. pricing.py dosyasının en dışına print(final_price(100, 0.20)) gibi bir deneme satırı yazarsanız, bu satır modülü içe aktaran her programda da çalışır.
Bunu önlemek için Python’ın her modülde tuttuğu __name__ adı kullanılır. Bir dosya doğrudan çalıştırıldığında __name__ değeri "__main__" olur. Başka bir dosyadan içe aktarıldığında ise modülün adını alır (örneğin "pricing").
# pricing.py
def final_price(price, tax_rate):
return price * (1 + tax_rate)
def main():
print(final_price(100, 0.20))
if __name__ == "__main__":
main()Bu kalıpla fonksiyon tanımları bir yanda, yalnızca dosya doğrudan çalıştırıldığında işleyen kod öbür yanda durur. py pricing.py deneme satırını çalıştırır. app.py ise final_price fonksiyonunu alır, ama main() çalışmaz. 13. haftada birkaç dosyadan oluşan bir OOP programında bu kalıbı yeniden kullanacağız.
Soruya dönelim: beş iş Python’da
Bölümün başında not programının beş işini C’de nasıl yapacağınızı sormuştuk. Python’daki karşılıkları şöyle:
| İş | C’de | Python’da | Dikkat |
|---|---|---|---|
| Ortalamayı hesaplayıp başka hesapta kullanmak | Fonksiyon yazar, sonucu return ile döndürürsünüz |
def calculate_average(first, second): ve return |
Yalnız print() eden fonksiyonun çağrısı None verir |
| Sınırı çoğu derste 50, bazılarında 60 almak | Varsayılan değer yoktur, sınırı her çağrıda yazarsınız | def is_successful(score, limit=50): |
Varsayılan değer olarak liste vermeyin, None nöbetçisini kullanın |
| Hesaplanan ortalamaları saymak | Global bir sayaç tutar, fonksiyonda count++ yazarsınız |
global count ya da sayıyı parametreyle alıp return ile vermek |
global yazmadan dıştaki ada atama yapmak UnboundLocalError verir |
| Not listesine not eklemek | Diziyi ve eleman sayısının adresini verirsiniz, fonksiyon asıl diziyi değiştirir | Fonksiyonda scores.append(value) |
Python listeyi kopyalamaz, çağıranın listesi değişir. Bunu docstring’e yazın |
| Fonksiyonları ayrı dosyada kullanmak | Prototipi .h, gövdeyi .c dosyasına yazar, #include eder ve birlikte derlersiniz |
pricing.py ve from pricing import final_price |
import modülün bütün satırlarını çalıştırır. Deneme kodunu if __name__ == "__main__": altına yazın |
Listenizi tabloyla karşılaştırın. return iki dilde de aynı işi görür. Varsayılan değer C’de yoktu, Python’da var. Son üç satırda ise C alışkanlığı sizi yanıltabilir. C’de global değişkeni fonksiyonun içinden doğrudan değiştirebilirdiniz, Python bunun için global ister. C’de dizi fonksiyona adresiyle, int ise kopya olarak giderdi. Python’da her parametre çağıranın verdiği nesneye bağlanır. Listenin değişip sayının değişmemesi, listenin değiştirilebilir, int’in değiştirilemez olmasından gelir. Sonraki haftalarda fonksiyonlara listelerin yerine kendi yazdığımız nesneleri vereceğiz. Dördüncü satırdaki kural onlar için de geçerli.
Alıştırma: return ekle
Beklenen çıktı 0 ve 8’dir.
def clamp_zero(value):
if value < 0:
return 0
return valueAlıştırma: Yan etkiyi tahmin et
Önce çıktıyı tahmin edin, sonra hücreyi çalıştırın.
- Çıktıyı neden böyle tahmin ettiniz? Hangi satır listeyi değiştiriyor?
- Fonksiyonun çağıranın listesini değiştirmesini istemeseydik hangi yaklaşımı kullanabilirdik?
- Fonksiyonun listeyi değiştirdiği adından anlaşılıyor mu, yoksa bunu docstring’e de yazmak gerekir mi?
Alıştırma: Varsayılan listeyi düzelt
Aşağıdaki fonksiyonu, her çağrı kendi ayrı listesini döndürecek biçimde düzeltin:
def collect(value, values=[]):
values.append(value)
return valuesNone nöbetçisini kullanın ve buna neden gerek olduğunu bir cümleyle açıklayın.
Sıra sizde: Not yardımcı modülü
Kendi bilgisayarınızda aynı klasöre iki dosya kaydedin. grades.py dosyasında şu iki fonksiyonu tanımlayın ve her birine kısa bir docstring ekleyin:
average(scores)is_successful(score, limit=50)
app.py dosyasında bu fonksiyonları import ile içe aktarın ve birkaç örnek veriyle çağırın. Argümanları hem sırasıyla (konumsal argüman) hem de parametre adıyla (anahtar kelime argümanı) verin. Ardından grades.py dosyasına, dosya doğrudan çalıştırıldığında birkaç örnek sonuç yazdıran bir main() fonksiyonu ekleyin.
Çalışır kod görevi: app.py dosyasını Windows’ta py app.py, macOS’ta python3 app.py ile çalıştırın. app.py içine en az üç assert satırı ekleyin. assert satırı bir koşulu denetler: koşul doğruysa hiçbir şey yapmaz, yanlışsa programı AssertionError ile durdurur. Örneğin assert is_successful(50) satırı sınır değerini denetler. average ve is_successful için hem olağan değerleri hem sınır değerlerini deneyin.
Tek sayfa özet
- Python’da fonksiyon
defile tanımlanır. Parametre ve dönüş türü yazılmaz, gövdeyi girinti belirler. returnsonucu çağırana verir,print()yalnızca ekrana yazar.returniçermeyen fonksiyonun çağrısıNoneverir.- Varsayılan değeri olan parametre çağrıda yazılmayabilir. Anahtar kelime argümanı, değeri parametrenin adıyla verir.
- Varsayılan değer
defsatırı çalışırken bir kez oluşturulur. Liste gerekiyorsa varsayılanıNoneyapın, listeyi gövdede oluşturun. - Fonksiyon dıştaki bir adı okuyabilir, ama gövdede o ada atama yaparsa Python adı yerel sayar.
UnboundLocalErrorçoğu zaman bundan çıkar. Dıştaki adı değiştirmek içinglobalgerekir. - Fonksiyon çağrılınca parametre adı verilen nesneye bağlanır. Fonksiyon listeyi yerinde değiştirirse çağıran da değişikliği görür.
intgibi değiştirilemez bir değerde+=yalnız yerel adı yeni bir nesneye bağlar. - Docstring, fonksiyonun ne alıp ne döndürdüğünü ve neyi değiştirdiğini kodun hemen yanında anlatır.
- Her
.pydosyası bir modüldür.importmodülün bütün satırlarını bir kez çalıştırır. Deneme koduif __name__ == "__main__":altına yazılır.
Bu bölümün kazanımları
Bu bölümü bitiren öğrenci:
print()ilereturnfarkını gösterir ve değer döndürmeyen çağrınınNoneverdiğini bilir.- Varsayılan parametre ve anahtar kelime argümanını uygun durumda kullanır.
- Değiştirilebilir varsayılan değer tuzağını tanır ve
Nonenöbetçisiyle önler. - Yerel ad ile dış kapsamdaki adı ayırır ve
UnboundLocalErrornedenini açıklar. - Fonksiyona verilen değiştirilebilir nesnenin yan etkiyle değişebileceğini gösterir.
- Docstring ile bir fonksiyonun davranış sözleşmesini yazar.
__name__ == "__main__"kalıbının fonksiyon tanımlarını ana akıştan neden ayırdığını açıklar.