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.
| İfade | Elindeki şey | Sonraki adım |
|---|---|---|
siparis | sözlük | anahtar: ["kalemler"] |
siparis["kalemler"] | liste | indeks: [1] ya da for |
siparis["kalemler"][1] | sözlük | anahtar: ["urun"] |
siparis["kalemler"][1]["urun"] | metin | metin 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.
{"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}")
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öntem | Dış katman | İç katmanlar | Ne zaman |
|---|---|---|---|
b = a | Paylaşılır | Paylaşılır | Kopya istemiyorsan, aynı nesneye ikinci ad |
a.copy(), list(a), a[:] | Yeni | Paylaşılır | Yapı düzse (sayı, metin elemanlar) |
copy.deepcopy(a) | Yeni | Yeni | İç 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)].
.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.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.