Ana içeriğe geç
Kesit Python
Temel · Veri yapıları

İç içe veri yapıları

Liste içinde sözlük, sözlük içinde liste ve JSON benzeri derin yapılarda gezinir; güvenli erişim, güncelleme, gruplama, düzleştirme, tablo gibi yazdırma ve sığ/derin kopya tuzağını Anadolu Market verisiyle öğrenirsin.

Uygulamada aç, 17 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ç.

Gerçek veri düz değildir

Şimdiye kadar tek bir liste ya da tek bir sözlükle çalıştın. Gerçek işte veri neredeyse hiç böyle gelmez. Anadolu Market'in sipariş sisteminden gelen bir kayıt düşün: siparişin bir müşterisi var, müşterinin bir adresi var, siparişin içinde birden fazla ürün kalemi var, her kalemin adedi ve fiyatı var. Bu bilgi doğal olarak iç içe (nested) bir yapıdır: sözlüğün içinde sözlük, onun içinde liste, listenin içinde yine sözlük.

Web servislerinden (API) gelen JSON yanıtları, yapılandırma dosyaları, e-ticaret sepetleri, banka hesap hareketleri; hepsi bu biçimdedir. Bu derste iç içe yapıları okumayı, güvenle erişmeyi, değiştirmeyi ve analiz için düzleştirmeyi öğreneceksin. Listeler ve Sözlükler derslerinde gördüğün her şey burada birleşiyor.

Kayıt listesi: liste içinde sözlük

En yaygın yapı budur: her eleman bir kayıt (satır), her kayıt bir sözlük (sütun adı → değer). Bir Excel tablosunun Python karşılığı diye düşünebilirsin.

siparisler = [
    {"no": "AM-101", "musteri": "Ayşe", "sehir": "İzmir", "tutar": 1499.9},
    {"no": "AM-102", "musteri": "Mehmet", "sehir": "Ankara", "tutar": 250.0},
    {"no": "AM-103", "musteri": "Zeynep", "sehir": "İzmir", "tutar": 89.5},
]
print(len(siparisler), "sipariş")
print(siparisler[0]["musteri"])          # ilk kaydın müşterisi
for s in siparisler:
    if s["sehir"] == "İzmir":
        print(s["no"], s["tutar"])
toplam = 0
for s in siparisler:
    toplam += s["tutar"]
print("Toplam:", round(toplam, 2))

Okuma kuralı basit: önce köşeli parantezle hangi kayıt (indeks), sonra hangi alan (anahtar). siparisler[0]["musteri"] "0. kaydın musteri alanı" demektir. Döngüde ise s zaten tek bir sözlüktür; doğrudan s["tutar"] yazarsın.

Sözlük içinde liste

Ters yön de çok kullanılır: bir anahtarın altında birden fazla değer tutmak. Bir müşterinin sipariş numaraları, bir şehrin şube listesi, bir ürünün etiketleri.

musteri = {
    "no": "M-101",
    "ad": "Ayşe Yılmaz",
    "siparisler": ["AM-101", "AM-117"],
    "etiketler": ["sadık", "kampanya"],
}
musteri["siparisler"].append("AM-130")     # iç listeye ekle
print(musteri["siparisler"])
print(len(musteri["siparisler"]), "sipariş")
print("sadık" in musteri["etiketler"])
print(musteri["siparisler"][-1])           # son sipariş

musteri["siparisler"] bir liste döndürür; ona .append, len, in, dilim gibi tüm liste işlemlerini uygulayabilirsin. İç listeyi değiştirdiğinde sözlüğün içindeki liste değişir, çünkü ikisi aynı nesnedir.

Derin yapılarda gezinme: yolu adım adım yaz

Gerçek bir sipariş kaydı daha derindir. Aşağıdaki yapı, bir API'den gelen JSON yanıtının tipik görünümüdür.

siparis = {
    "no": "AM-205",
    "musteri": {
        "ad": "Ayşe Yılmaz",
        "adres": {"sehir": "İzmir", "ilce": "Konak"},
    },
    "kalemler": [
        {"urun": "Kahve makinesi", "adet": 1, "fiyat": 1499.9},
        {"urun": "Filtre kağıdı", "adet": 3, "fiyat": 45.0},
    ],
}
print(siparis["musteri"]["adres"]["sehir"])
print(siparis["kalemler"][1]["urun"])
print(type(siparis["kalemler"]), type(siparis["kalemler"][0]))
toplam = 0
for k in siparis["kalemler"]:
    toplam += k["adet"] * k["fiyat"]
print("Sipariş toplamı:", round(toplam, 2))

Derin bir değere ulaşmak için yolu soldan sağa okursun: siparis → ["musteri"] (sözlük) → ["adres"] (sözlük) → ["sehir"] (metin). Her adımda elindeki şeyin türünü bil: sözlükse anahtar, listeyse indeks kullanırsın. Kaybolduğunda bir ara adımı print(type(...)) ile yazdır; bu, deneyimli geliştiricilerin de en çok kullandığı yöntemdir.

İfadeElindeki şeySonraki adım
siparissözlükanahtar: ["kalemler"]
siparis["kalemler"]listeindeks: [1] ya da for
siparis["kalemler"][1]sözlükanahtar: ["urun"]
siparis["kalemler"][1]["urun"]metinmetin işlemleri

Güvenli erişim: .get zinciri

Gerçek veride alanlar eksik olabilir: bazı müşterilerin adresi girilmemiştir, bazı siparişlerde indirim alanı yoktur. Köşeli parantez eksik anahtarda KeyError verir ve tüm rapor durur. Sözlükler dersinde get ile varsayılan değeri gördün; iç içe yapıda bunu zincirlersin.

musteriler = [
    {"ad": "Ayşe", "adres": {"sehir": "İzmir"}},
    {"ad": "Mehmet"},                       # adres yok
    {"ad": "Zeynep", "adres": {}},          # adres var, şehir yok
]
for m in musteriler:
    sehir = m.get("adres", {}).get("sehir", "Bilinmiyor")
    print(m["ad"], "→", sehir)

Püf noktası ara adımdaki varsayılan: m.get("adres", {}). Adres yoksa boş sözlük döner ve ikinci .get onun üzerinde güvenle çalışır. Varsayılanı yazmazsan None döner ve None.get(...) AttributeError verir. Listede eksik olabilecek bir indeks için de benzer bir kontrol yaparsın: kalemler[0] if kalemler else None.

get zinciri her şeyi çözmezZincirin ara adımı bazen anahtar eksikliği değil None değeri olarak gelir: {"adres": None}. Bu durumda .get("adres", {}) yine None döndürür, çünkü anahtar vardır. Kirli veride (m.get("adres") or {}).get("sehir") yazmak daha dayanıklıdır: or, None'ı da boş sözlükle değiştirir.

Çok derin yollar için küçük bir yardımcı fonksiyon yazmak işini kolaylaştırır. Yolu bir liste olarak verirsin; fonksiyon her adımda bir sonraki katmana iner, bir adım bulunamazsa varsayılanı döndürür.

def derin_al(veri, yol, varsayilan=None):
    for adim in yol:
        try:
            veri = veri[adim]
        except (KeyError, IndexError, TypeError):
            return varsayilan
    return veri

siparis = {"musteri": {"adres": {"sehir": "İzmir"}}, "kalemler": [{"urun": "Çay"}]}
print(derin_al(siparis, ["musteri", "adres", "sehir"]))
print(derin_al(siparis, ["kalemler", 0, "urun"]))
print(derin_al(siparis, ["kalemler", 5, "urun"], "yok"))
print(derin_al(siparis, ["musteri", "telefon"], "yok"))

Buradaki try/except yapısını Hatalar dersinde ayrıntılı göreceksin; şimdilik "bu adım patlarsa varsayılanı döndür" diye oku.

İç içe yapıyı güncellemek

Okuduğun yolun aynısıyla yazabilirsin. Son adıma atama yaparsan değer değişir; iç listeye append yaparsan eleman eklenir.

siparis = {
    "no": "AM-205",
    "durum": "hazırlanıyor",
    "musteri": {"ad": "Ayşe", "adres": {"sehir": "İzmir"}},
    "kalemler": [{"urun": "Çay", "adet": 2, "fiyat": 120.0}],
}
siparis["durum"] = "kargoda"
siparis["musteri"]["adres"]["ilce"] = "Bornova"          # yeni alan
siparis["kalemler"][0]["adet"] += 1                       # adet 3 oldu
siparis["kalemler"].append({"urun": "Bardak", "adet": 6, "fiyat": 25.0})
siparis.setdefault("notlar", []).append("Kapıya bırak")   # liste yoksa oluştur
print(siparis["musteri"]["adres"])
print(len(siparis["kalemler"]), siparis["kalemler"][0]["adet"])
print(siparis["notlar"])

setdefault(anahtar, []) çok kullanışlıdır: anahtar yoksa boş liste koyup onu döndürür, varsa mevcut listeyi döndürür. Böylece "liste var mı yok mu" kontrolünü tek satırda geçersin.

Gruplama: şehre göre sipariş listesi

Sözlükler dersinde sayım kalıbını gördün: her şehirde kaç sipariş var. Gruplama bir adım ötesidir: her şehrin siparişlerinin kendisini bir listede toplarsın. Sonuç sözlük içinde liste yapısıdır.

siparisler = [
    {"no": "AM-101", "sehir": "İzmir", "tutar": 1499.9},
    {"no": "AM-102", "sehir": "Ankara", "tutar": 250.0},
    {"no": "AM-103", "sehir": "İzmir", "tutar": 89.5},
    {"no": "AM-104", "sehir": "Bursa", "tutar": 3200.0},
]
gruplar = {}
for s in siparisler:
    gruplar.setdefault(s["sehir"], []).append(s["no"])
print(gruplar)
for sehir, nolar in gruplar.items():
    print(sehir, len(nolar), nolar)

Aynı işi if s["sehir"] not in gruplar: gruplar[s["sehir"]] = [] diye iki satırla da yazabilirsin; setdefault bunun kısaltmasıdır. Orta seviyede collections.defaultdict(list) ile daha da kısalacak. Sık yapılan hata, gruplar[sehir] = [no] yazıp her seferinde listeyi sıfırlamaktır.

Düzleştirme: iç içe veriyi tabloya çevirmek

Analiz ve raporlama için çoğu zaman düz (flat) bir tablo istersin: her satır bir kalem, her sütun bir alan. Bu, pandas'a ya da Excel'e aktarmanın da ön adımıdır. İç içe döngüyle dış kaydın bilgisini her iç satıra taşırsın.

siparisler = [
    {"no": "AM-101", "sehir": "İzmir",
     "kalemler": [{"urun": "Çay", "adet": 2, "fiyat": 120.0},
                  {"urun": "Bardak", "adet": 6, "fiyat": 25.0}]},
    {"no": "AM-102", "sehir": "Ankara",
     "kalemler": [{"urun": "Kahve", "adet": 1, "fiyat": 350.0}]},
]
satirlar = []
for s in siparisler:
    for k in s["kalemler"]:
        satirlar.append({
            "no": s["no"], "sehir": s["sehir"], "urun": k["urun"],
            "tutar": k["adet"] * k["fiyat"],
        })
print(len(satirlar), "satır")
for r in satirlar:
    print(r)

Tablo gibi yazdırmak

Düz satırları ekranda okunur göstermek için f-string hizalaması kullan: :<12 sola, :>10 sağa yaslar, :.2f iki ondalık verir. Metinler sola, sayılar sağa hizalanır; muhasebe raporlarının da kuralı budur.

satirlar = [
    {"no": "AM-101", "urun": "Çay", "tutar": 240.0},
    {"no": "AM-101", "urun": "Bardak", "tutar": 150.0},
    {"no": "AM-102", "urun": "Kahve", "tutar": 350.0},
]
print(f"{'Sipariş':<8} {'Ürün':<10} {'Tutar':>8}")
print("-" * 28)
for r in satirlar:
    print(f"{r['no']:<8} {r['urun']:<10} {r['tutar']:>8.2f}")
print("-" * 28)
print(f"{'TOPLAM':<19} {sum(r['tutar'] for r in satirlar):>8.2f}")
Tırnak karışıklığıf-string içinde sözlük anahtarı yazarken dıştaki tırnakla aynı tırnağı kullanma: f"{r['no']}" doğru, eski sürümlerde f"{r["no"]}" SyntaxError verir. Python 3.12 bunu kabul etse de karışık tırnak alışkanlığı her sürümde çalışır. Biçimlendirmenin tüm ayrıntısını String biçimleme dersinde göreceksin.

Bir yapının içini hızlıca görmek için iki araç daha var: pprint.pprint(veri) iç içe yapıyı girintili yazdırır, json.dumps(veri, indent=2, ensure_ascii=False) JSON görünümü verir. Hata ayıklarken ikisi de hayat kurtarır.

Sığ kopya, derin kopya tuzağı

Listeler dersinde atamanın kopya olmadığını gördün. İç içe yapıda bir adım daha var: .copy() yalnızca en dış katmanı kopyalar. Bu sığ kopyadır (shallow copy); içteki listeler ve sözlükler hâlâ paylaşılır.

import copy

katalog = {"Çay": {"fiyat": 120.0, "etiket": ["içecek"]}}
sig = katalog.copy()
derin = copy.deepcopy(katalog)

sig["Çay"]["fiyat"] = 99.0           # iç sözlük paylaşılıyor!
derin["Çay"]["etiket"].append("kampanya")

print(katalog["Çay"]["fiyat"])        # orijinal de değişti: 99.0
print(katalog["Çay"]["etiket"])       # orijinal etkilenmedi
print(sig["Çay"] is katalog["Çay"], derin["Çay"] is katalog["Çay"])
YöntemDış katmanİç katmanlarNe zaman
b = aPaylaşılırPaylaşılırKopya istemiyorsan, aynı nesneye ikinci ad
a.copy(), list(a), a[:]YeniPaylaşılırYapı düzse (sayı, metin elemanlar)
copy.deepcopy(a)YeniYeniİç içe yapıyı bağımsız değiştireceksen

Aynı tuzak [[0] * 3] * 3 ile tablo kurarken de karşına çıkar: üç satır aynı iç listedir, birini değiştirince hepsi değişir. Doğrusu her satırı ayrı oluşturmaktır: [[0] * 3 for _ in range(3)].

Gerçek işteBir veri analisti ya da backend geliştiricisi gününün önemli kısmını iç içe JSON'la geçirir: ödeme sağlayıcısının webhook mesajı, kargo firmasının takip yanıtı, CRM'den gelen müşteri kaydı. Tipik iş akışı hep aynıdır: yanıtı al, .get zinciriyle eksik alanlara dayanıklı oku, ihtiyacın olan alanları düz satırlara çevir, sonra raporla ya da veritabanına yaz. Kampanya fiyatı hesaplarken orijinal kataloğu sığ kopyayla bozmak ise canlı sistemde yaşanan klasik hatalardandır; fiyat ekranı birden yanlış göstermeye başlar.
Uzmanlaş

Ne kadar iç içe olmalı?

İç içelik veriyi doğal gösterir ama her katman okumayı zorlaştırır. Üç-dört katmandan derin yapılar genellikle tasarım kokusudur. Veritabanı mantığında veriyi normalize edersin: müşterileri ayrı bir sözlükte ({"M-101": {...}}), siparişleri ayrı bir listede tutar, siparişte yalnızca "musteri_no" saklarsın. Gerektiğinde sözlükten anında bulursun. Bu, SQL'deki JOIN'in Python karşılığıdır ve aynı müşteri bilgisinin yüz siparişte kopyalanmasını önler.

musteriler = {"M-1": {"ad": "Ayşe", "sehir": "İzmir"}, "M-2": {"ad": "Mehmet", "sehir": "Ankara"}}
siparisler = [{"no": "AM-1", "musteri_no": "M-1", "tutar": 300}, {"no": "AM-2", "musteri_no": "M-2", "tutar": 120}]
for s in siparisler:
    m = musteriler.get(s["musteri_no"], {})
    print(s["no"], m.get("ad", "?"), m.get("sehir", "?"), s["tutar"])

Sözlükten sınıfa

s["musteri"]["adres"]["sehir"] yazarken anahtarı yanlış yazarsan hata ancak çalışırken ortaya çıkar. Orta seviyede dataclass ile kayıtları sınıfa çevirip s.musteri.adres.sehir yazacak, editörün otomatik tamamlamasından ve tip denetiminden yararlanacaksın. Dış dünyadan gelen JSON'u sınıflara çevirmek için de pydantic gibi kütüphaneler kullanılır.

Bilinmeyen derinlik

Bazen yapının kaç katman olduğunu bilmezsin: klasör ağacı, kategori ağacı, organizasyon şeması. Bunları gezmek için ya bir yığın (stack) ile döngü kurarsın ya da fonksiyonun kendini çağırdığı özyinelemeyi (recursion) kullanırsın. Özyineleme dersinde iç içe listenin toplamını ve klasör ağacını tam bu tekniklerle gezeceksin.

Alıştırmalara geç (17)