Döngü yazmak, kalıp tanımaktır
Döngüler dersinde for, while, range, enumerate, zip, break ve continue'yu öğrendin. Bu ders bir adım geri çekilip şu soruyu soruyor: deneyimli bir geliştirici yeni bir problemi gördüğünde döngüyü nasıl bu kadar hızlı yazıyor? Cevap: yazdığı döngülerin büyük çoğunluğu bir avuç kalıbın (pattern) tekrarıdır. "Kaç tane?" sorusu sayaçtır, "toplam ne?" biriktiricidir, "en pahalısı hangisi?" en iyi değer kalıbıdır, "var mı?" aramadır.
Her kalıbın üç parçası vardır: döngüden önce başlangıç değeri, döngünün içinde güncelleme kuralı, döngüden sonra sonucu kullanma. Hataların çoğu bu üçünden birinde yapılır; özellikle başlangıç değerinde.
| Soru | Kalıp | Başlangıç | Güncelleme |
|---|---|---|---|
| Kaç tane? | Sayaç | sayac = 0 | sayac += 1 (koşullu) |
| Toplam/çarpım ne? | Biriktirici | 0 ya da 1, "", [] | toplam += x |
| En büyüğü hangisi? | En iyi değer | İlk eleman | Daha iyisi gelince değiştir |
| Var mı, nerede? | Doğrusal arama | bulundu = False | Bulunca bayrak + break |
| Hepsi mi uyuyor? | Doğrulama | hepsi = True | Uymayan gelince False + break |
| Önceki güne göre? | Komşu karşılaştırma | İlk eleman ya da indeks 1 | x ile onceki |
| Her değerden kaç tane? | Frekans | {} | d[x] = d.get(x, 0) + 1 |
| Hedefe kaç adımda? | Koşula kadar tekrar | Durum + adım sayacı | while koşulu |
Sayaç ve biriktirici: tek geçişte birden fazla sonuç
Bu ikisini zaten biliyorsun. Burada kazanman gereken alışkanlık: aynı veriyi birkaç kez gezmek yerine tek geçişte birden fazla sayaç ve biriktirici tutmak.
islemler = [1200, -350, 4800, -90, -1500, 600, 0, 75] # Ilgaz Bank hesap hareketleri
giris_toplam = 0
cikis_toplam = 0
cikis_sayisi = 0
buyuk_islem = 0 # |tutar| >= 1000
for t in islemler:
if t > 0:
giris_toplam += t
elif t < 0:
cikis_toplam += -t
cikis_sayisi += 1
if abs(t) >= 1000:
buyuk_islem += 1
print("Giriş:", giris_toplam, "Çıkış:", cikis_toplam)
print("Çıkış adedi:", cikis_sayisi, "Büyük işlem:", buyuk_islem)
print("Net:", giris_toplam - cikis_toplam)
Biriktiricinin başlangıç değeri işleme göre değişir: toplamada 0, çarpmada 1 (0 ile başlarsan sonuç hep 0 olur), metin birleştirmede "", liste toplamada [].
En büyük ve en küçük: max kullanmadan
Gerçek kodda max ve min kullanırsın; ama kalıbı bilmek iki sebeple önemli. Birincisi, çoğu zaman yalnız değeri değil kaydın kendisini ya da konumunu istersin. İkincisi, "en iyi" tanımı karmaşıklaşabilir: en yüksek tutarlı ama iptal edilmemiş sipariş, eşitlikte en eski olan gibi.
bakiyeler = [-250.0, -1200.5, -40.0, -980.0] # hepsi negatif!
# YANLIŞ başlangıç: 0 hiçbir negatif değerden küçük değil
en_buyuk_yanlis = 0
for b in bakiyeler:
if b > en_buyuk_yanlis:
en_buyuk_yanlis = b
print("Yanlış sonuç:", en_buyuk_yanlis)
# DOĞRU: ilk elemanla başla, konumu da tut
en_kucuk = bakiyeler[0]
en_kucuk_i = 0
for i, b in enumerate(bakiyeler):
if b < en_kucuk:
en_kucuk = b
en_kucuk_i = i
print("En düşük bakiye:", en_kucuk, "konum:", en_kucuk_i)
float("-inf") / float("inf"). Boş liste ihtimali varsa önce onu kontrol et: if not bakiyeler: return None.Eşitlik durumunda hangi kaydın seçileceği karşılaştırma operatörüne bağlıdır: < ilk görüleni, <= son görüleni tutar. Bu küçük fark, raporlarda "neden bu müşteri çıktı" sorusunun cevabıdır.
Doğrusal arama ve bayrak değişkeni
"Bu listede şu koşulu sağlayan var mı, varsa ilki hangisi?" Elemanları baştan sona tek tek kontrol etmeye doğrusal arama (linear search) denir. Sonucu tutmak için bir bayrak (flag) değişkeni kullanırsın; aradığını bulunca da break ile çıkarsın, çünkü geri kalanı kontrol etmek boşa iştir.
siparisler = [("AM-1", 120), ("AM-2", 4500), ("AM-3", 80), ("AM-4", 9000)]
esik = 4000
bulundu = False
for no, tutar in siparisler:
if tutar > esik:
bulundu = True
ilk_buyuk = no
break
if bulundu:
print("İlk büyük sipariş:", ilk_buyuk)
else:
print("Büyük sipariş yok")
# Aynı iş for-else ile: else, döngü break OLMADAN biterse çalışır
for no, tutar in siparisler:
if tutar > 10000:
print("Çok büyük sipariş:", no)
break
else:
print("10000 üstü sipariş yok")
Bayrağın tersi doğrulama kalıbıdır: "hepsi uyuyor mu?" sorusunda hepsi = True ile başlar, uymayan ilk elemanda False yapıp çıkarsın. Bu iki kalıbın hazır karşılıkları any() ve all()'dur; ama hangi elemanın uyduğunu ya da uymadığını öğrenmen gerekiyorsa döngü yazarsın.
return et, döngü bitince "bulunamadı" değerini (None ya da -1) döndür. Erken dönüş (early return), bayrak ve break'in en okunur hâlidir.Komşu elemanları karşılaştırmak
Zaman serilerinde (günlük satış, aylık bakiye, saatlik sıcaklık) asıl soru çoğu zaman bir değerin bir öncekine göre ne olduğudur: arttı mı, azaldı mı, kaç gün üst üste arttı? Bunun için her adımda iki elemana birden bakarsın.
satislar = [120, 135, 135, 150, 90, 110, 130, 160] # günlük satış adedi
# Yöntem 1: indeksle, 1'den başla
artis = 0
for i in range(1, len(satislar)):
if satislar[i] > satislar[i - 1]:
artis += 1
print("Artış günü:", artis)
# Yöntem 2: zip ile (önceki, şimdiki) çiftleri
for onceki, simdi in zip(satislar, satislar[1:]):
if simdi < onceki:
print(f"Düşüş: {onceki} → {simdi} ({simdi - onceki})")
# Seri (streak): en uzun üst üste artış
seri = en_uzun = 0
for onceki, simdi in zip(satislar, satislar[1:]):
if simdi > onceki:
seri += 1
en_uzun = max(en_uzun, seri)
else:
seri = 0
print("En uzun artış serisi:", en_uzun, "gün")
zip(satislar, satislar[1:]) güzel bir hiledir: listeyi bir kaydırılmış kopyasıyla eşler ve n elemandan n-1 komşu çifti verir. Döngünün ilk elemandan değil ikinciden başladığına dikkat et; ilk günün "önceki günü" yoktur. İndeks yönteminde range(len(satislar)) yazıp satislar[i - 1] kullanırsan ilk turda satislar[-1], yani son eleman okunur ve hata vermeden yanlış sonuç alırsın.
Frekans ve gruplama
Sözlükler dersinde sayım kalıbını, İç içe veri yapıları dersinde gruplamayı gördün. Kalıp olarak hatırla: frekans, anahtar başına bir sayaç; gruplama, anahtar başına bir liste biriktiricisi. İkisi birleşince "en sık" sorusu iki adımlı olur: önce say, sonra en iyi değer kalıbıyla en büyüğü bul.
satilan = ["çay", "kahve", "çay", "şeker", "kahve", "çay", "su"]
frekans = {}
for urun in satilan:
frekans[urun] = frekans.get(urun, 0) + 1
en_cok, en_cok_adet = None, 0
for urun, adet in frekans.items():
if adet > en_cok_adet:
en_cok, en_cok_adet = urun, adet
print(frekans)
print("En çok satan:", en_cok, en_cok_adet)
İç içe döngü: tablolar ve çiftler
Dış döngünün her turunda iç döngü baştan sona çalışır. İki tipik kullanım: satır-sütun tablosu üretmek ve bir listedeki tüm çiftleri denemek. Çiftlerde aynı çifti iki kez (a-b ve b-a) ve elemanı kendisiyle eşlememek için iç döngü i + 1'den başlar.
for i in range(1, 4):
satir = ""
for j in range(1, 6):
satir += f"{i * j:4}"
print(satir)
fiyatlar = [150, 350, 200, 50]
butce = 400
for i in range(len(fiyatlar)):
for j in range(i + 1, len(fiyatlar)):
if fiyatlar[i] + fiyatlar[j] == butce:
print("Bütçeye tam uyan ikili:", fiyatlar[i], fiyatlar[j])
İç içe döngüde iş sayısı çarpılır: 1000 elemanlı listede tüm çiftler yaklaşık yarım milyon karşılaştırmadır. Bu, Uzmanlaş bölümündeki karmaşıklık konusunun girişidir.
while: koşula kadar tekrar
Kaç tur süreceğini önceden bilmediğin durumlarda while kullanırsın. Ilgaz Bank'ın klasik müşteri sorusu: "Hesabımda 10.000 TL var, aylık %3 faiz alırsam 15.000 TL'ye kaç ayda ulaşırım?"
bakiye = 10_000
aylik_faiz = 0.03
hedef = 15_000
ay = 0
while bakiye < hedef:
bakiye *= 1 + aylik_faiz
ay += 1
print(f"{ay} ayda hedefe ulaşılır, bakiye: {bakiye:.2f} TL")
# Güvenlik sınırı: faiz 0 olursa döngü sonsuza gider
bakiye, ay = 10_000, 0
while bakiye < hedef and ay < 600:
bakiye *= 1 + 0.0
ay += 1
if bakiye < hedef:
print("Hedefe ulaşılamadı, döngü", ay, "ayda durduruldu")
while döngüsünün üç sorusunu her zaman cevapla: koşul ne, döngü içinde koşulu hedefe yaklaştıran satır hangisi, ve hedefe asla ulaşılamazsa ne olur? Son soru için bir üst sınır (en fazla 600 ay gibi) koymak, üretim kodunda hayat kurtarır.
Döngüde liste değiştirme tuzağı
Gezdiğin listeden eleman silmek, Python'daki en sinsi hatalardan biridir: hata vermez, sessizce eleman atlar. Sebep: for döngüsü içeride bir indeks sayacı tutar; sen bir elemanı silince sonrakiler bir sola kayar ve sayaç bir sonrakini atlar.
siparisler = ["iptal", "iptal", "aktif", "iptal", "aktif"]
for s in siparisler:
if s == "iptal":
siparisler.remove(s)
print("Yanlış:", siparisler) # bir iptal kaldı!
siparisler = ["iptal", "iptal", "aktif", "iptal", "aktif"]
temiz = []
for s in siparisler:
if s != "iptal":
temiz.append(s)
print("Doğru (yeni liste):", temiz)
siparisler = ["iptal", "iptal", "aktif", "iptal", "aktif"]
for s in siparisler[:]: # kopya üzerinde gez, orijinalden sil
if s == "iptal":
siparisler.remove(s)
print("Doğru (kopya üzerinde):", siparisler)
En temiz yol, istediğin elemanlarla yeni bir liste kurmaktır (Orta seviyede bunu tek satırlık comprehension ile yapacaksın). Orijinal listeyi mutlaka yerinde değiştirmen gerekiyorsa kopyası üzerinde gez ya da listeyi sondan başa doğru gez. Aynı kural sözlükler için daha da serttir: gezerken sözlüğe anahtar ekleyip silmek RuntimeError verir.
Döngü değişmezi (loop invariant)
Doğru döngü yazmanın en güçlü düşünme aracı, her turun sonunda doğru kalan cümledir. En büyük bulma kalıbında değişmez şudur: "en_buyuk, şimdiye kadar görülen elemanların en büyüğüdür." Başlangıçta doğru olmalı (ilk elemanla başlatmanın sebebi), her tur korumalı ve döngü bitince sonucu vermeli. Hata ayıklarken "bu cümle hangi turda bozuldu?" diye sormak, rastgele print eklemekten çok daha hızlıdır.
Karmaşıklık: kaç adım?
| Kalıp | Adım sayısı | 1 milyon eleman |
|---|---|---|
| Tek geçiş (sayaç, toplam, en büyük, arama) | n | Göz açıp kapayıncaya kadar |
| Erken çıkışlı arama | En iyi 1, en kötü n | Bulunacak yere bağlı |
| Tüm çiftler (iç içe döngü) | n × n / 2 | Yarım trilyon: kullanılamaz |
| Sözlük/küme ile eşleme | n | Hızlı |
"Toplamı bütçeye eşit iki ürün" problemini iç içe döngü yerine tek geçişte çözebilirsin: gördüğün fiyatları bir kümede tut, her yeni fiyatta butce - fiyat kümede var mı diye bak. Bu, mülakatların en klasik sorusudur (two sum) ve kalıp birleştirmenin gücünü gösterir.
def butce_ikilisi(fiyatlar, butce):
gorulen = set()
for f in fiyatlar:
if butce - f in gorulen:
return (butce - f, f)
gorulen.add(f)
return None
print(butce_ikilisi([150, 350, 200, 50], 400))
print(butce_ikilisi([10, 20], 100))
Kalıptan yerleşik fonksiyona
Kalıbı tanıdıktan sonra kısaltmasını kullanmak profesyonel koddur: sayaç → sum(1 for x in v if ...), biriktirici → sum, en iyi → max(v, key=...), arama → next((x for x in v if ...), None), doğrulama → all, frekans → collections.Counter, komşu çiftleri → itertools.pairwise. Bunların çoğunu Orta seviyede göreceksin. Ama kısaltma çalışmadığında ya da kural karmaşıklaştığında açık döngüye dönebilmek, kalıbı bilmekle mümkün.