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 parametreyi, dönüş değerini, kapsamı, varsayılan değeri, docstring’i ve modülleri kısaca toparlayacağız. Bir yandan da her şeyi tek büyük programa yazmak yerine işi küçük fonksiyonlara bölme alışkanlığını pekiştireceğiz.

Bu bölümün kapsamı

NoteSınavda sorulur
  • print() ile return farkı ve örtük None
  • Varsayılan parametre ve anahtar kelime argümanı
  • Mutable varsayılan parametre tuzağı ve None nö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; ileride karşınıza çıkar ama bu derste ezberlemeniz beklenmiyor.

Fonksiyon verilen listeyi mi değiştirmeli, yeni liste mi döndürmeli?

Bir listeye not ekleyen fonksiyonu iki biçimde yazabilirsiniz: verilen listeyi yerinde değiştirir ya da notu eklenmiş yeni bir liste döndürür. İkisi de çalışır, ama fonksiyonu çağıran kod için sonuç farklıdır: birinde eldeki liste değişir, ötekinde olduğu gibi kalır. Bu bölüm boyunca şunu soracağız: bu fonksiyon, onu çağıran koda ne söz veriyor?

Fonksiyon: adlandırılmış davranış

first ve second parametrelerdir; çağrıdaki 70 ve 90 argümanlardı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.

Varsayılan parametre ve anahtar kelime argümanı

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.

Mutable varsayılan parametre tuzağı

Şu kod ilk bakışta sorunsuz görünür:

def add_item(item, items=[]):
    items.append(item)
    return items

Ama varsayılan [] her çağrıda yeniden oluşturulmaz. Fonksiyon tanımlanırken bir kez oluşturulur ve sonraki çağrılar aynı listeyi paylaşır. Sorunu önlemek için şu kalıp yaygın olarak kullanılır:

Burada None, “çağıran liste vermedi” anlamına gelen bir işarettir. Bu işi gören değere nöbetçi (sentinel) denir.

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

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. rate ise fonksiyonun dışında tanımlıdır ve fonksiyon onu dış kapsamdan (scope) okur.

Ancak fonksiyon içinde aynı ada atama yaparsanız Python bu adı o fonksiyonda yerel kabul eder:

count = 10

def increase():
    print(count)
    count = count + 1

Bu fonksiyon çağrıldığında UnboundLocalError oluşur. Gövdede counta atama olduğu için Python countu baştan yerel sayar, ama print satırı çalıştığında bu yerel counta henüz değer verilmemiştir.

Dıştaki countu değiştirmek istiyorsanız bunu global ile açıkça söylemeniz gerekir:

count = 10

def increase():
    global count
    count += 1

Ama 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

Aşağıdaki örneği çalıştırmadan önce numbers listesinin değişip değişmeyeceğini tahmin edin.

Bir fonksiyona verilen değiştirilebilir nesnenin fonksiyon içinde değiştirilmesinin çağıran tarafta yan etki oluşturmasını gösteren diyagram.

Değiştirilebilir parametre ve yan etki

Ç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. 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. Yan etki bazen istenir, bazen beklenmedik bir hataya yol açar. Hangisi olduğunu fonksiyonun sözleşmesi, yani ne alıp ne döndürdüğünü ve neyi değiştirdiğini anlatan tanımı açıkça söylemelidir.

Note“Pass-by-reference” diye ezberlemeyin

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.

Çağıranın listesi değişmesin istiyorsak yeni bir liste döndürebiliriz:

Docstring: davranış sözleşmesini koda yazmak

Fonksiyonun adı her ayrıntıyı anlatmaz. 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 üretir. Girdileri değiştirmez.
    """
    if price < 0:
        raise ValueError("Fiyat negatif olamaz")
    return price * (1 - rate)

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

Her Python dosyası bir modül olarak kullanılabilir: başka bir dosya onun içindeki fonksiyonları import ile alıp çağırabilir. Örneğin:

# pricing.py
def final_price(price, tax_rate):
    return price * (1 + tax_rate)

Başka dosyada:

from pricing import final_price

print(final_price(100, 0.20))

Bu sayfadaki kod hücrelerinde ayrı dosya oluşturmak her zaman kolay değil. Bu yüzden modül örneklerini bu hafta çoğunlukla çalıştırmadan okuyacağız. Siz de 1. haftada kurduğunuz Python ve VS Code ile kendi bilgisayarınızda iki dosya açıp deneyin.

__name__ == "__main__" ne işe yarar?

Bir dosya doğrudan çalıştırıldığında __name__ değeri "__main__" olur. Başka bir dosyadan import edildiğinde ise modülün adını alır (örneğin "pricing").

def main():
    print("Program başladı")

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. Dosyayı import eden başka bir dosya fonksiyonları alır, ama main() çalışmaz. 13. haftada birkaç dosyadan oluşan bir OOP programında bu kalıbı yeniden kullanacağız.

Alıştırma: return ekle

Beklenen çıktı 0 ve 8’dir.

def clamp_zero(value):
    if value < 0:
        return 0
    return value

Alıştırma: Yan etkiyi tahmin et

  1. Çıktıyı tahmin edin.
  2. Fonksiyonun çağıranın listesini değiştirmesini istemeseydik hangi yaklaşımı kullanabilirdik?
  3. Fonksiyonun listeyi değiştirdiği adından anlaşılıyor mu, yoksa bunu docstring’e de yazmak gerekir mi?

Alıştırma: Mutable varsayılanı 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 values

None 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ü

Yerel ortamınızda iki dosya oluşturun:

grades.py içinde:

  • average(scores)
  • is_successful(score, limit=50)

fonksiyonlarını tanımlayın. Her fonksiyona kısa docstring ekleyin.

app.py içinde bu fonksiyonları import edin 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: py app.py / python3 app.py ile programı çalıştırın; en az üç assert ile average ve is_successful için normal ve sınır durumlarını doğrulayın.

Sınav provası

Önce kendi cevabınızı seçin; sonra cevap anahtarında her şıkkın neden doğru veya yanlış olduğunu okuyun. Çeldiriciler uydurma değil, bu konuda gerçekten yapılan hatalardır. Sınav maddeleri de bu mantıkla yazılır.

Madde 1. Aşağıdaki program ne yazdırır?

def show_square(x):
    print(x * x)

first = show_square(5)
print("first:", first)
  • A. Önce 25, sonra first: 25
  • B. Önce 25, sonra first: None
  • C. Yalnızca first: 25
  • D. Yalnızca first: None
  • E. Hata verir; dönüş değeri olmayan bir çağrı bir değişkene atanamaz

Madde 2. Aşağıdaki program ne yazdırır?

def is_successful(score, limit=50):
    return score >= limit

print(is_successful(70))
print(is_successful(70, 80))
  • A. True ve True
  • B. True ve None
  • C. False ve False
  • D. Hata verir; varsayılanı olan parametre çağrıda verilemez
  • E. True ve False

Madde 3. Aşağıdaki fonksiyon iki kez çağrıldığında ne olur?

def add_item(item, items=[]):
    items.append(item)
    return items

print(add_item("A"))
print(add_item("B"))
  • A. ['A', 'B'] ve ['A', 'B']
  • B. İki çağrı da [] döndürür
  • C. ['A'] ve ['B']; her çağrıda yeni bir liste oluşturulur
  • D. ['A'] ve ['A', 'B']; varsayılan liste paylaşılır
  • E. İkinci çağrı TypeError verir

Madde 4. Mutable varsayılan parametre tuzağından kaçınmak için hangi kalıp kullanılır?

  • A. Varsayılanı () yapmak
  • B. Varsayılanı [] bırakıp gövdenin başında items.clear() çağırmak
  • C. Varsayılanı None yapmak ve gövdede if items is None: items = [] yazmak
  • D. Fonksiyonu her çağrıdan önce yeniden tanımlamak
  • E. Parametreyi tümüyle kaldırıp global bir liste kullanmak

Madde 5. Aşağıdaki fonksiyon çağrıldığında ne olur?

count = 10

def increase():
    print(count)
    count = count + 1

increase()
  • A. UnboundLocalError; atama yüzünden count yerel sayılır
  • B. 11 yazdırır
  • C. Hata vermez; dıştaki count değeri 11 olur
  • D. 10 yazdırır; count dıştan okunur ve sonra yerel olur
  • E. NameError; count tanımlı değildir

Madde 6. Aşağıdaki program ne yazdırır?

def add_score(scores, value):
    scores.append(value)

numbers = [70, 80]
add_score(numbers, 90)
print(numbers)
  • A. None
  • B. [70, 80, 90]; scores aynı listeye bağlanır
  • C. [90]
  • D. Hata verir; fonksiyon dışındaki liste değiştirilemez
  • E. [70, 80]; fonksiyon kendi kopyası üzerinde çalışır

Madde 7. Çağıranın listesini değiştirmeden yeni bir liste döndürmek için gövdeye hangisi yazılmalıdır?

  • A. return scores + [value]
  • B. return list(value)
  • C. return scores yazıp çağıranın kopya almasını beklemek
  • D. scores.append(value) ve ardından return scores
  • E. scores += [value] ve ardından return scores

Madde 8. Bir fonksiyonun docstring’i öncelikle neyi belirtmelidir?

  • A. Gövdedeki her satırın ne yaptığını sırayla
  • B. Fonksiyonun hangi modülde durduğunu
  • C. Beklenen girdiyi, dönüş değerini ve önemli yan etki ile hata koşullarını
  • D. Fonksiyonun kaç satır olduğunu ve kim tarafından yazıldığını
  • E. Kullanılan algoritmanın karmaşıklık analizini

Madde 9. if __name__ == "__main__": bloğu ne işe yarar?

  • A. Hataları yakalayıp programın çökmesini engeller
  • B. Modülün yalnızca bir kez import edilmesini sağlar
  • C. Programın main adlı bir fonksiyon içermesini zorunlu kılar
  • D. Ana akışı yalnız dosya doğrudan çalıştırıldığında başlatır
  • E. Dosyanın çalıştırılabilir olmasını sağlar

Madde 1. Doğru: B. İlk satırı fonksiyonun içindeki print() çağrısı yazdırır. Fonksiyon return içermediği için dönüş değeri Nonedır.

  • A yanlış: Fonksiyon 25 değerini ekrana yazar ama döndürmez; first adına bir sonuç gelmez.
  • C yanlış: Fonksiyon gövdesindeki print() de çalışır.
  • D yanlış: Çağrı sırasında gövdedeki print() çıktısı da görünür.
  • E yanlış: Atama geçerlidir; atanan değer Nonedır.

Madde 2. Doğru: E. İlk çağrıda varsayılan 50 kullanılır ve 70 >= 50 doğrudur. İkincide sınır 80 olur ve 70 >= 80 yanlıştır.

  • A yanlış: İkinci çağrıda verilen argüman varsayılanın yerini alır.
  • B yanlış: Fonksiyon her iki çağrıda da bir karşılaştırma sonucu döndürür.
  • C yanlış: İlk çağrıda sınır 50’dir ve koşul sağlanır.
  • D yanlış: Çağrı geçerlidir; varsayılan yalnızca değer verilmediğinde kullanılır.

Madde 3. Doğru: D. Varsayılan değer fonksiyon tanımlanırken bir kez oluşturulur ve çağrılar arasında paylaşılır. Her çağrı aynı listeyi kullanır.

  • A yanlış: Paylaşımı fark eden öğrenci iki çıktının da son durumu göstereceğini düşünebilir. Ama ilk çağrının çıktısı o andaki içeriği gösterir; B henüz eklenmemiştir.
  • B yanlış: Varsayılan değer [] olduğu için sonucun da boş liste olacağı sanılabilir. Ama append listeye gerçekten eleman ekler.
  • C yanlış: C’de yerel değişkenler her çağrıda yeniden kurulduğu için bu sonuç beklenir. Ama varsayılan liste her çağrıda yeniden oluşturulmaz; tanım anında bir kez oluşturulur.
  • E yanlış: Aynı listeye ikinci kez dokunmanın bir çakışma yaratacağı düşünülebilir. Çağrı geçerlidir; sorun, aynı listenin hata vermeden paylaşılmasıdır.

Madde 4. Doğru: C. Böylece liste her çağrıda yeniden oluşur; paylaşım ortadan kalkar.

  • A yanlış: Demet değiştirilemez olduğu için append çağrısı hata verir.
  • B yanlış: Çağıranın gönderdiği listeyi de boşaltır; yeni bir yan etki yaratır.
  • D yanlış: Böyle bir çözüm uygulanamaz; sorun, varsayılan listenin ne zaman oluşturulduğudur.
  • E yanlış: Paylaşımı daha da büyütür; hangi kodun listeyi değiştirdiğini izlemek zorlaşır.

Madde 5. Doğru: A. Python bir adın yerel olup olmadığına fonksiyonun tamamına bakarak karar verir. Gövdede counta atama olduğu için ad yerel kabul edilir, ama print anında henüz değeri yoktur.

  • B yanlış: Fonksiyon sayacı artırdığı için sonuç 11 gibi görünür. Ama print satırı atamadan önce gelir; üstelik hata verdiği için hiçbir şey yazdıramaz.
  • C yanlış: C’de küresel değişken fonksiyon içinden doğrudan değiştirilebildiği için bu beklenir. Python’da dış adı değiştirmek için global bildirimi gerekirdi.
  • D yanlış: Satırlar sırayla çalıştığı için adın önce dıştan okunduğu düşünülür. Ama Python bu kararı satır satır vermez; fonksiyon derlenirken bir kez verir.
  • E yanlış: Ad bulunamayınca akla ilk gelen hata budur. Ama count dışarıda tanımlıdır; sorun, yerel counta henüz değer verilmemiş olmasıdır.

Madde 6. Doğru: B. Parametre adı, çağıranın verdiği liste nesnesine bağlanır; Python argümanı kopyalamaz. append ortak listeyi değiştirdiği için çağıran da yeni içeriği görür.

  • A yanlış: add_score bir değer döndürmediği için None beklenebilir. Ama yazdırılan, fonksiyonun dönüş değeri değil, numbers listesidir.
  • C yanlış: Fonksiyona yalnız 90 gönderildiği için sonucun o olduğu sanılabilir. Ama append listeyi sıfırlamaz; sona ekler.
  • D yanlış: Dışarıdaki verinin fonksiyon içinden korunduğu sanılabilir. Böyle bir kısıt yoktur. Fonksiyon dışarıdaki listeyi değiştirebildiği için yan etki olup olmayacağına fonksiyonu yazan karar verir.
  • E yanlış: Kopya oluşturulmaz. Bu beklenti, C’deki “fonksiyona değerin kopyası gider” alışkanlığından gelir.

Madde 7. Doğru: A. + yeni bir liste oluşturur; scores olduğu gibi kalır.

  • B yanlış: scores içindeki değerleri yok sayar ve tek değeri listeye çevirmeye çalışır.
  • C yanlış: Kopya almayı çağırana bırakır; fonksiyon hâlâ aynı listeyi döndürür.
  • D yanlış: scores yerinde değişir; yan etki ortadan kalkmaz.
  • E yanlış: Listelerde += mevcut listeyi yerinde uzatır; scores değişir.

Madde 8. Doğru: C. Docstring fonksiyonun sözleşmesidir; fonksiyonu çağıran kişinin bilmesi gerekenleri yazar.

  • A yanlış: Bu, kodu bir kez daha anlatmak olur; üstelik kod değişince eskir.
  • B yanlış: Bu bilgi zaten dosya yapısından okunur.
  • D yanlış: Fonksiyonu çağıran kişi davranışı anlamak için bu bilgiye ihtiyaç duymaz.
  • E yanlış: Yararlı olabilir ama sözleşmenin temel parçası değildir.

Madde 9. Doğru: D. Doğrudan çalıştırmada __name__ değeri "__main__" olur; import edildiğinde modül adını alır ve blok çalışmaz. Tanımlar ile ana akış böyle ayrılır.

  • A yanlış: if bloğu programı saran bir koruma gibi görünebilir. Hata yönetimiyle ilgisi yoktur; try/except ayrı bir konudur.
  • B yanlış: Blok import ile ilgili olduğu için import sayısını denetlediği düşünülebilir. Python bir modülü zaten bir kez yükler; bunun bu blokla ilgisi yoktur.
  • C yanlış: C’de her programın bir main() fonksiyonu olduğu için bu blok o kuralın Python karşılığı sanılabilir. Böyle bir zorunluluk yoktur; blok içine herhangi bir kod yazılabilir.
  • E yanlış: Blok ana akışı başlattığı için dosyayı çalıştırılabilir yapan şey sanılabilir. Python dosyaları bu blok olmadan da çalıştırılabilir.

Tek sayfa özet

  • Fonksiyon kod tekrarını azaltır; üstelik bir işe ad verir.
  • return sonucu çağırana verir; print yalnızca ekrana yazar.
  • Varsayılan parametreler ve anahtar kelime argümanları fonksiyonu çağırmayı kolaylaştırır. Varsayılan değer olarak liste gibi değiştirilebilir bir nesne vermeyin.
  • Fonksiyon dıştaki bir adı okuyabilir, ama gövdede o ada atama yaparsa Python adı yerel sayar. UnboundLocalError çoğu zaman bundan çıkar.
  • Fonksiyon çağrılınca parametre adı verilen nesneye bağlanır. Fonksiyon bu nesneyi yerinde değiştirirse çağıran da değişikliği görür.
  • Docstring, fonksiyonun ne alıp ne döndürdüğünü ve neyi değiştirdiğini kodun hemen yanında anlatır.
  • Modüller programı dosyalara böler; böylece hangi dosyanın hangi işi yaptığı belli olur.

Bu bölümün kazanımları

Bu bölümü bitiren öğrenci:

  • print() ile return farkını gösterir ve değer döndürmeyen çağrının None verdiğini bilir.
  • Varsayılan parametre ve anahtar kelime argümanını uygun durumda kullanır.
  • Mutable varsayılan parametre tuzağını tanır ve None nöbetçisiyle önler.
  • Yerel ad ile dış kapsamdaki adı ayırır ve UnboundLocalError nedenini 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.
Back to top