Neden önemli?
Anadolu Market'in raporlama ekibinde çalıştığını düşün. Her gün "hangi şehirden kaç sipariş geldi", "son 5 işlem neydi", "ürün çiftlerinin hangisi birlikte alınıyor" gibi sorular geliyor. Bunların hepsini sözlük ve for döngüsüyle yazabilirsin; ama Python'ın standart kütüphanesindeki collections ve itertools modülleri aynı işi daha kısa, daha hızlı ve daha az hatayla yapar. Bu ders, "elle yazdığın yardımcı kodun" yerine hazır ve test edilmiş araçları koymayı öğretir.
collections modülü
Counter: sayım yapan sözlük
Counter (sayaç), bir yinelenebilirdeki (iterable) elemanları sayar ve eleman: adet sözlüğü üretir. En çok geçenleri most_common(n) ile alırsın; olmayan anahtar için hata değil 0 döner.
from collections import Counter
siparis_sehirleri = ["İstanbul", "Ankara", "İstanbul", "İzmir", "Ankara", "İstanbul", "Bursa"]
sayac = Counter(siparis_sehirleri)
print(sayac)
print("En çok 2 şehir:", sayac.most_common(2))
print("Adana siparişi:", sayac["Adana"]) # KeyError yok, 0 döner
# Counter'lar toplanabilir
dun = Counter({"İstanbul": 3, "Ankara": 1})
bugun = Counter({"İstanbul": 2, "İzmir": 4})
print("İki gün toplam:", dun + bugun)
print("Toplam sipariş:", sum(sayac.values()))
defaultdict: eksik anahtarı kendisi üreten sözlük
Klasik gruplama kodunda if sehir not in gruplar: gruplar[sehir] = [] satırı hep tekrar eder. defaultdict(list) bu satırı kaldırır: olmayan bir anahtara eriştiğinde fabrika fonksiyonunu (list, int, set...) çağırıp varsayılan değeri yerleştirir.
from collections import defaultdict
islemler = [
("TR01", 250.0), ("TR02", 80.0), ("TR01", 120.5), ("TR03", 999.0), ("TR02", 15.0),
]
hesap_toplam = defaultdict(float)
hesap_hareket = defaultdict(list)
for hesap, tutar in islemler:
hesap_toplam[hesap] += tutar # ilk erişimde 0.0 ile başlar
hesap_hareket[hesap].append(tutar) # ilk erişimde [] ile başlar
print(dict(hesap_toplam))
print(dict(hesap_hareket))
print("TR09 toplam:", hesap_toplam["TR09"]) # dikkat: erişim anahtarı OLUŞTURUR
print("Anahtarlar:", list(hesap_toplam))
"TR09" in d kullan ya da d.get("TR09") çağır; köşeli parantezle okuma.deque: iki uçlu kuyruk
Listenin başından eleman çıkarmak (pop(0)) tüm elemanları kaydırdığı için yavaştır. deque (double-ended queue) iki uçtan da O(1) ekleme/çıkarma yapar. maxlen verirsen "son N kayıt" penceresi elde edersin: yeni eleman gelince en eski kendiliğinden düşer.
from collections import deque
son_islemler = deque(maxlen=3)
for tutar in [100, 250, 80, 999, 42]:
son_islemler.append(tutar)
print("Pencere:", list(son_islemler))
kuyruk = deque(["müşteri-1", "müşteri-2"])
kuyruk.append("müşteri-3") # sağa ekle
kuyruk.appendleft("acil") # sola ekle
print(kuyruk.popleft(), "işleme alındı")
print("Kalan:", list(kuyruk))
kuyruk.rotate(1) # döndür
print("Döndürülmüş:", list(kuyruk))
namedtuple: adlandırılmış demet
Bir işlemi ("TR01", 250.0, "havale") diye tutarsan üç ay sonra islem[2] ne demekti diye bakarsın. namedtuple demete alan adı verir: islem.tur. Değiştirilemez (immutable) ve hafiftir; sözlükten az yer kaplar. Daha fazla özellik (varsayılan değer, metot) istersen dataclass dersine dön.
from collections import namedtuple
Islem = namedtuple("Islem", ["hesap", "tutar", "tur"])
i1 = Islem("TR01", 250.0, "havale")
i2 = Islem(hesap="TR02", tutar=80.0, tur="kart")
print(i1.hesap, i1.tutar, i1.tur)
print(i1[0], "=", i1.hesap) # indeksle de çalışır
hesap, tutar, tur = i2 # paketten çıkarma
print(tutar)
print(i1._asdict()) # sözlüğe çevir
print(i1._replace(tutar=300.0)) # yeni kopya (orijinal değişmez)
dict ekleme sırasını korur; bu yüzden OrderedDict çoğu kodda gerekmez. Hâlâ iki farkı var: eşitlik karşılaştırmasında sırayı dikkate alır ve move_to_end() metodu vardır (LRU önbellek (cache) gibi yapılarda kullanılır). Eski kodda görürsen şaşırma.| Araç | Ne zaman | Yerine geçtiği elle yazılan kod |
|---|---|---|
| Counter | Sayım, en çok geçen | d[x] = d.get(x, 0) + 1 |
| defaultdict | Gruplama, toplama | if k not in d: d[k] = [] |
| deque | Kuyruk, son N kayıt | lst.pop(0), lst = lst[-N:] |
| namedtuple | Hafif, değişmez kayıt | t[0], t[1] ile indeks erişimi |
| ChainMap | Katmanlı ayar (varsayılan + kullanıcı) | {**varsayilan, **kullanici} |
itertools modülü
itertools fonksiyonları tembel (lazy) çalışır: sonuç listesini bellekte kurmaz, istendikçe üretir. Bu yüzden büyük verilerle ve üreteçlerle (generator) iyi anlaşır. Sonucu görmek için list() ile sarman gerekir.
chain ve islice
chain(a, b) birden çok yinelenebiliri tek akış gibi gezer; chain.from_iterable iç içe listeyi düzleştirir. islice(it, start, stop, step) ise üreteçlerde dilimleme yapar; listeye çevirmeden "ilk 3" almak için kullanılır.
from itertools import chain, islice, count
subat = ["S-101", "S-102"]
mart = ["S-201", "S-202", "S-203"]
print(list(chain(subat, mart)))
print(list(chain.from_iterable([subat, mart, ["S-301"]])))
# Sonsuz sayaçtan ilk 4 sipariş numarası
numaralar = (f"S-{n}" for n in count(1000))
print(list(islice(numaralar, 4)))
print(list(islice(mart, 1, None))) # 1. indeksten sona
groupby: ardışık grupları ayırma
groupby SQL'deki GROUP BY'a benzer ama bir tuzağı var: yalnızca ardışık eşit anahtarları gruplar. Bu yüzden önce aynı anahtara göre sorted() yapmalısın. Grup nesnesi tembel bir yineleyicidir (iterator); bir kez tüketilir.
from itertools import groupby
siparisler = [
{"sehir": "Ankara", "tutar": 120}, {"sehir": "İzmir", "tutar": 300},
{"sehir": "Ankara", "tutar": 80}, {"sehir": "İstanbul", "tutar": 50},
]
anahtar = lambda s: s["sehir"]
for sehir, grup in groupby(sorted(siparisler, key=anahtar), key=anahtar):
grup = list(grup) # tüketmeden önce listeye al
print(sehir, len(grup), "sipariş, toplam", sum(s["tutar"] for s in grup))
# Sıralamadan: Ankara iki ayrı gruba bölünür!
print([k for k, _ in groupby(siparisler, key=anahtar)])
product, combinations, permutations
from itertools import product, combinations, permutations
bedenler = ["S", "M"]
renkler = ["kırmızı", "siyah", "beyaz"]
print("Varyant sayısı:", len(list(product(bedenler, renkler))))
print(list(product(bedenler, renkler))[:3])
sepet = ["laptop", "mouse", "çanta", "kulaklık"]
print("İkili kombinasyon:", list(combinations(sepet, 2))[:4]) # sıra önemsiz
print("Sıralı ikili:", list(permutations(sepet[:3], 2))) # sıra önemli
print("Kendisiyle çarpım:", list(product("01", repeat=2)))
accumulate: kümülatif değerler
accumulate birikimli toplam üretir; func ile başka işlem (örneğin max) verebilirsin. Banka bakiyesi, yılbaşından bugüne satış, tepe değer gibi raporlar için birebir.
from itertools import accumulate
import operator
hareketler = [1000, -250, 400, -1200, 300]
print("Bakiye:", list(accumulate(hareketler)))
print("Bakiye (başlangıç 500):", list(accumulate(hareketler, initial=500)))
print("Şimdiye kadarki tepe:", list(accumulate(hareketler, max)))
print("Çarpımsal:", list(accumulate([1.1, 1.2, 0.9], operator.mul)))
zip en kısa listede durur; eşit olmayan uzunluklarda itertools.zip_longest(a, b, fillvalue=None) kullan. Ardışık çiftler (bugün-dün farkı gibi) için itertools.pairwise(lst) (3.10+) vardır: [(1,2),(2,3),...].Birlikte kullanım: küçük bir rapor
from collections import Counter, defaultdict, namedtuple
from itertools import accumulate
Satis = namedtuple("Satis", "gun kategori tutar")
veri = [
Satis("Pzt", "elektronik", 1500), Satis("Pzt", "giyim", 300),
Satis("Sal", "elektronik", 700), Satis("Sal", "kitap", 120),
Satis("Çar", "giyim", 450),
]
print("Kategori adedi:", Counter(s.kategori for s in veri))
gunluk = defaultdict(int)
for s in veri:
gunluk[s.gun] += s.tutar
print("Günlük:", dict(gunluk))
print("Kümülatif:", list(accumulate(gunluk.values())))
Derinleş: sayaç aritmetiği, katmanlı ayarlar ve itertools'un geri kalanı
Counter, defaultdict, deque ve temel itertools fonksiyonlarını tanıdın. Bu bölümde iş hayatında sık kullanılıp derslerde az anlatılan parçaları ekliyoruz: Counter ile stok farkı çıkarmak, iç içe defaultdict, ChainMap ile katmanlı ayarlar, namedtuple'ın yardımcı metotları ve pairwise, batched, zip_longest, takewhile gibi araçlar. Öncelik kuyruğu (heapq) "Veri yapıları", reduce ve partial "Fonksiyonel programlama" dersinde.
1. Counter aritmetiği: stok sayımı farkı
Counter'lar birer çoklu küme (multiset) gibi davranır. + toplar, - çıkarır ve sıfır ya da negatif sonuçları atar; & ortak en küçük, | en büyük adetleri verir. Negatifleri korumak istersen yerinde çalışan subtract() kullanırsın. total() (3.10+) tüm adetlerin toplamıdır.
from collections import Counter
sistem = Counter({"koli": 40, "bant": 12, "etiket": 5})
sayim = Counter({"koli": 37, "bant": 12, "etiket": 9, "makas": 1})
print("eksik:", sistem - sayim) # sistemde olup rafta olmayan
print("fazla:", sayim - sistem) # rafta olup sistemde olmayan
print("ortak:", sistem & sayim)
fark = Counter(sistem)
fark.subtract(sayim) # negatifler korunur
print("net :", dict(fark))
print("toplam adet:", sayim.total(), "| çeşit:", len(sayim))
print(sorted(Counter("abaca").elements()))
2. İç içe defaultdict
"Şehir → kategori → ciro" gibi iki seviyeli toplamalarda iç içe if'ler yerine defaultdict(lambda: defaultdict(float)) yazarsın. Fabrika çağrılabilir olmalıdır; defaultdict(defaultdict(float)) hatalıdır çünkü dış sözlüğe bir nesne verir, fabrika değil. JSON'a ya da teste vermeden önce düz dict'e çevir.
from collections import defaultdict
satislar = [("Ankara", "Elektronik", 1200.0), ("İzmir", "Gıda", 85.5),
("Ankara", "Gıda", 40.0), ("Ankara", "Elektronik", 300.0)]
ciro = defaultdict(lambda: defaultdict(float))
for sehir, kategori, tutar in satislar:
ciro[sehir][kategori] += tutar
duz = {s: dict(k) for s, k in ciro.items()}
print(duz)
print(ciro["Bursa"]["Gıda"], "Bursa" in ciro) # okumak bile anahtar oluşturur!
3. ChainMap: katmanlı ayarlar
Bir uygulamanın ayarları genellikle üç katmandan gelir: kod içi varsayılanlar, ortam (sunucu) ayarları ve kullanıcının komut satırı seçenekleri. ChainMap(kullanici, ortam, varsayilan) bunları kopyalamadan tek sözlük gibi gösterir: okumada soldan sağa ilk bulunan kazanır, yazma ve silme yalnız ilk sözlüğe yapılır. Katmanlar sonradan değişirse ChainMap bunu anında yansıtır; {**a, **b} birleştirmesi ise bir kopyadır.
from collections import ChainMap
varsayilan = {"para_birimi": "TRY", "sayfa": 20, "dil": "tr"}
ortam = {"sayfa": 50}
kullanici = {"dil": "en"}
ayar = ChainMap(kullanici, ortam, varsayilan)
print(ayar["para_birimi"], ayar["sayfa"], ayar["dil"])
ayar["sayfa"] = 10 # yalnız kullanici'ya yazılır
print(kullanici, ortam)
ortam["zaman_asimi"] = 30 # sonradan eklenen katman verisi görünür
print(ayar["zaman_asimi"], dict(ayar))
print([k for k in varsayilan if ayar[k] != varsayilan[k]]) # değişen ayarlar
4. namedtuple'ın yardımcıları
namedtuple değiştirilemez; bir alanı "güncellemek" için _replace(alan=...) yeni bir kayıt döndürür. _asdict() sözlüğe, _fields alan adlarına, _make(iterable) tam alan sayısındaki bir diziden kayıt üretir (varsayılanları kullanmaz; eksik sütunlu satırda Urun(*satir) yaz). defaults= ile sondaki alanlara varsayılan verilir. Alt çizgi burada "özel" demek değildir; alan adlarıyla çakışmasın diye konmuştur.
from collections import namedtuple
import csv, io
Urun = namedtuple("Urun", ["sku", "ad", "fiyat", "stok"], defaults=[0])
metin = "K-1,Kulaklık,750\nF-2,Fare,300\n"
urunler = [Urun(*r) for r in csv.reader(io.StringIO(metin))] # stok varsayılan 0
print(Urun._make(["M-3", "Monitör", "4200", "2"])) # _make: tüm alanlar
print(urunler[0])
zamli = [u._replace(fiyat=round(float(u.fiyat) * 1.1, 2)) for u in urunler]
print(zamli[1]._asdict(), Urun._fields)
5. itertools'un geri kalanı
| Fonksiyon | Ne yapar | İş örneği |
|---|---|---|
pairwise(x) (3.10+) | Ardışık çiftler: (a,b), (b,c)... | Günlük değişim, iki olay arası süre |
batched(x, n) (3.12+) | n'lik demet parçaları | API'ye 100'erli gönderim |
zip_longest(a, b, fillvalue=) | Kısa olanı doldurarak eşler | Eksik ayları 0 ile hizalamak |
takewhile(p, x) / dropwhile(p, x) | Koşul bozulana kadar al / atla | Dosya başlığını atlamak |
compress(x, maske) | Maskede doğru olanları seç | Onaylı kayıtları süzmek |
starmap(f, demetler) | f(*demet) uygular | (fiyat, adet) çiftlerinden tutar |
cycle(x) | Sonsuz döngüyle tekrarlar | Vardiyaya sırayla personel atamak |
from itertools import pairwise, batched, zip_longest, takewhile, dropwhile, compress, starmap, cycle
kapanis = [100, 104, 101, 110]
print([b - a for a, b in pairwise(kapanis)])
print(list(batched(["S1", "S2", "S3", "S4", "S5"], 2)))
print(list(zip_longest(["Oca", "Şub", "Mar"], [120, 95], fillvalue=0)))
satirlar = ["# rapor", "# v2", "TR01;100", "# not", "TR02;50"]
print(list(dropwhile(lambda s: s.startswith("#"), satirlar)))
print(list(takewhile(lambda s: s.startswith("#"), satirlar)))
print(list(compress(["A", "B", "C"], [True, False, True])))
print(list(starmap(lambda f, a: f * a, [(10.0, 3), (2.5, 4)])))
print(list(zip(["Pzt", "Sal", "Çar", "Per"], cycle(["Ayşe", "Can"]))))
map, filter, üreteçler ve itertools sonuçları bir kez tüketilir. Aynı sonucu iki kez dolaşacaksan önce list()'e çevir ya da itertools.tee kullan. İkinci sum(...)'ın sessizce 0 döndürmesi raporlarda en sinsi hatalardandır.islice ile yaparsın: it = iter(veri), sonra while parca := list(islice(it, n)): .... Bu kalıp her sürümde çalışır.batched ile parçalayıp her parçayı ayrı gönderirsin. CLI araçlarında ayarlar varsayılan → yapılandırma dosyası → ortam değişkeni → komut satırı katmanlarından gelir; ChainMap bu öncelik sırasını kodda açıkça gösterir.İş hayatında nerede karşına çıkar?
Günlük (log) analizinde Counter ile en çok hata veren uç noktayı bulmak, deque(maxlen=1000) ile "son 1000 istek" üzerinden hareketli ortalama hesaplamak, defaultdict(set) ile "her müşteri hangi ürünleri aldı" tablosunu kurmak çok yaygın kalıplardır. Pazar sepeti (market basket) analizine giriş düzeyinde combinations(sepet, 2) ile ürün çiftlerini sayıp Counter ile birleştirince "birlikte alınan ürünler" raporu elde edersin.
Performans tarafında: Counter ve defaultdict C düzeyinde yazılmıştır; elle yazdığın get döngüsünden genelde hızlıdır. itertools tembel olduğu için milyonlarca satırı diske yazmadan akış halinde işleyebilirsin; pandas yüklemeden önce denenecek ilk araç budur. Ancak groupby için önce sıralama gerekir; sıralama O(n log n) olduğundan büyük veride tek defaultdict geçişi (O(n)) daha ucuza gelebilir. Aracı seçerken "sıralı sonuç mu, yoksa toplam mı istiyorum?" sorusunu sor.
Son bir not: namedtuple okunurluğu artırır ama değiştirilemez; kaydı güncellemek istiyorsan _replace yeni nesne döndürür. Mutasyon gerekiyorsa @dataclass; bellek kritikse @dataclass(slots=True) tercih et.