Ana içeriğe geç
Kesit Python
Orta · Standart kütüphane

collections ve itertools: hazır veri yapıları ve yineleme araçları

Counter, defaultdict, deque, namedtuple ile veriyi daha az kodla tutar; chain, groupby, product, combinations, accumulate ve islice ile yinelemeyi tek geçişte kurarsın.

Uygulamada aç, 22 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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))
Erişim yan etkisidefaultdict'te bir anahtarı okumak bile onu oluşturur. Yalnızca "var mı?" diye bakmak istiyorsan "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)
OrderedDict notuPython 3.7'den beri normal 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 zamanYerine geçtiği elle yazılan kod
CounterSayım, en çok geçend[x] = d.get(x, 0) + 1
defaultdictGruplama, toplamaif k not in d: d[k] = []
dequeKuyruk, son N kayıtlst.pop(0), lst = lst[-N:]
namedtupleHafif, değişmez kayıtt[0], t[1] ile indeks erişimi
ChainMapKatmanlı 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_longest ve pairwisezip 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ş

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ı

FonksiyonNe 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şlerEksik ayları 0 ile hizalamak
takewhile(p, x) / dropwhile(p, x)Koşul bozulana kadar al / atlaDosya 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 tekrarlarVardiyaya 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"]))))
Yineleyici tek kullanımlıktırmap, 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.
batched yoksaPython 3.11 ve öncesinde parçalamayı (chunking) islice ile yaparsın: it = iter(veri), sonra while parca := list(islice(it, n)): .... Bu kalıp her sürümde çalışır.
Gerçek işteDepo sayımlarında "sistem stoğu ile raf sayımı farkı" Counter aritmetiğiyle iki satırda çıkar; muhasebe mutabakatında (reconciliation) aynı fikir iki kaynaktaki kayıtları karşılaştırmaya uygulanır. Toplu API çağrılarında servisler genellikle istek başına kayıt sınırı koyar; 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.
Uzmanlaş

İş 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.

Alıştırmalara geç (22)