Veri Yapıları Karşılaştırması
Python'da dört temel veri yapısı öğrendin: list, tuple, set ve dict. Her birinin güçlü ve zayıf yanları var. Doğru veri yapısını seçmek, kodunun hem performansını hem okunabilirliğini dramatik şekilde etkiler. Bu derste hepsini bir araya getirip karşılaştıracağız ve "ne zaman hangisini kullanmalıyım" sorusuna net cevaplar vereceğiz.
Bir alet çantası düşün: Çekiç, tornavida, pense, testere — hepsi aletler ama hepsi farklı işler için. Vidayı çekiçle çakmaya çalışırsan, işi bitirirsin belki ama hem vida zarar görür hem sen yorulursun. Doğru alet doğru iş için. Veri yapıları da öyle — her birinin parladığı bir alan var.
Büyük Karşılaştırma Tablosu
| Özellik | List | Tuple | Set | Dict |
|---|---|---|---|---|
| Sıralı | ✅ Evet | ✅ Evet | ❌ Hayır | ✅ Evet (3.7+) |
| Mutable | ✅ Evet | ❌ Hayır | ✅ Evet | ✅ Evet |
| Duplicate | ✅ İzin verir | ✅ İzin verir | ❌ Yok | ❌ Key yok* |
| İndeksleme | ✅ a[0] | ✅ a[0] | ❌ Yok | ✅ d["key"] |
| Hashable | ❌ Hayır | ✅ Evet** | ❌ Hayır | ❌ Hayır |
| Arama hızı | O(n) | O(n) | O(1) | O(1) key |
| Ekleme hızı | O(1) sona | — | O(1) | O(1) |
| Silme hızı | O(n) | — | O(1) | O(1) |
| Bellek | Orta | Düşük | Yüksek | Yüksek |
| Söz dizimi | [1, 2] | (1, 2) | {1, 2} | {"a": 1} |
| Boş | [] | () | set() | {} |
\* Dict key'leri benzersiz, value'lar tekrar edebilir \** Tüm elemanları hashable ise
Big-O: Zaman Karmaşıklığı Detayları
List
Erişim (indeks): O(1) — a[i] anında
Arama (in): O(n) — en kötü durumda tüm listeyi tarar
Append (sona ekle): O(1) — amortized (bazen O(n) resize)
Insert (başa ekle): O(n) — tüm elemanlar kaydırılır
Delete (indeks): O(n) — kaydırma gerekir
Delete (değer): O(n) — önce ara, sonra kaydır
Sort: O(n log n) — TimsortTuple
Erişim (indeks): O(1) — a[i] anında
Arama (in): O(n) — tüm elemanları kontrol eder
Oluşturma: O(n) — ama list'ten ~5-10x hızlı (constant factor)Set
Arama (in): O(1) — hash tablosu
Ekleme (add): O(1) — amortized
Silme (remove): O(1) — amortized
Union (|): O(len(a) + len(b))
Intersection (&): O(min(len(a), len(b)))
Difference (-): O(len(a))Dict
Erişim (key): O(1) — hash tablosu
Arama (in): O(1) — sadece key'lerde
Ekleme: O(1) — amortized
Silme: O(1) — amortized
Keys/Values/Items: O(1) — view döndürür (lazy)
İterasyon: O(n)Somut Performans Karşılaştırması
import time
N = 1_000_000
# Oluşturma
data = list(range(N))
# Arama performansı
hedef = N - 1 # En kötü durum
# List
start = time.perf_counter()
hedef in data
list_time = time.perf_counter() - start
# Set
data_set = set(data)
start = time.perf_counter()
hedef in data_set
set_time = time.perf_counter() - start
# Dict
data_dict = {x: True for x in data}
start = time.perf_counter()
hedef in data_dict
dict_time = time.perf_counter() - start
print(f"List arama: {list_time:.6f}s") # ~0.01s
print(f"Set arama: {set_time:.8f}s") # ~0.0000001s
print(f"Dict arama: {dict_time:.8f}s") # ~0.0000001s
print(f"\nSet, listeden {list_time/set_time:.0f}x hızlı!")Ne Zaman Hangi Yapı? Karar Ağacı
Hangi veri yapısını seçeceğini belirlemek için şu soruları sırayla sor:
1. Anahtar-değer çifti mi?
EVET → dict kullan
# İsim-not eşleştirmesi
notlar = {"Ahmet": 85, "Ayşe": 92}2. Elemanlar benzersiz olmalı mı?
EVET → set kullan
# Benzersiz IP adresleri
aktif_ipler = {"192.168.1.1", "10.0.0.1", "192.168.1.1"}
# Otomatik: {'192.168.1.1', '10.0.0.1'}3. Veri değişecek mi?
HAYIR → tuple kullan
# Koordinat — değişmemeli
konum = (41.01, 28.97)4. Sıralı ve değişken koleksiyon mu?
EVET → list kullan
# Yapılacaklar listesi — ekleme/çıkarma var
gorevler = ["alışveriş", "spor", "ders"]
gorevler.append("temizlik")Karar Ağacı Diyagramı
Başla
│
Key-value mi?
/ \
Evet Hayır
│ │
dict Benzersiz mi?
/ \
Evet Hayır
│ │
set Değişecek mi?
/ \
Evet Hayır
│ │
list tupleDaha Detaylı Senaryolar
# Sıklık sayımı → Counter (dict alt sınıfı)
from collections import Counter
frekans = Counter(["a", "b", "a", "c", "a"])
# FIFO kuyruğu → deque (list yerine!)
from collections import deque
kuyruk = deque(["a", "b", "c"])
kuyruk.append("d") # Sağa ekle
kuyruk.appendleft("z") # Sola ekle
kuyruk.popleft() # Soldan çıkar — O(1)!
# Sabit anahtar seti → frozenset
IZINLER = frozenset({"read", "write", "execute"})
# Birden fazla config katmanı → ChainMap
from collections import ChainMap
varsayilan = {"tema": "light", "dil": "tr"}
kullanici = {"tema": "dark"}
config = ChainMap(kullanici, varsayilan)collections Modülü: Gelişmiş Veri Yapıları
Python'un collections modülü, temel veri yapılarının özelleştirilmiş versiyonlarını sunar.
deque (Double-Ended Queue)
Liste iki uçtan ekleme/çıkarma için optimize değildir — başa ekleme O(n). deque iki uçtan da O(1):
from collections import deque
# Oluşturma
d = deque([1, 2, 3, 4, 5])
# İki uçtan ekleme — O(1)
d.append(6) # Sağa: [1, 2, 3, 4, 5, 6]
d.appendleft(0) # Sola: [0, 1, 2, 3, 4, 5, 6]
# İki uçtan çıkarma — O(1)
d.pop() # Sağdan: 6
d.popleft() # Soldan: 0
# Döndürme (rotate)
d = deque([1, 2, 3, 4, 5])
d.rotate(2) # Sağa 2 adım: [4, 5, 1, 2, 3]
d.rotate(-2) # Sola 2 adım: [1, 2, 3, 4, 5]
# Maksimum boyut sınırı (circular buffer)
son_5 = deque(maxlen=5)
for i in range(10):
son_5.append(i)
print(son_5) # deque([5, 6, 7, 8, 9], maxlen=5)
# Eski elemanlar otomatik düşer!Ne zaman deque kullan?
Baştan ekleme/çıkarma sık yapılıyorsa (list'te O(n), deque'te O(1))
Son N elemanı tutmak istiyorsan (maxlen)
BFS (Breadth-First Search) algoritmasında kuyruk olarak
# List vs Deque performans
import timeit
def list_sol_ekleme():
l = []
for i in range(10000):
l.insert(0, i)
def deque_sol_ekleme():
d = deque()
for i in range(10000):
d.appendleft(i)
print(f"List: {timeit.timeit(list_sol_ekleme, number=10):.3f}s")
print(f"Deque: {timeit.timeit(deque_sol_ekleme, number=10):.3f}s")
# List: ~1.5s | Deque: ~0.005s — 300x fark!OrderedDict
Python 3.7+ sonrasında dict ekleme sırasını garanti eder, o zaman neden OrderedDict var?
from collections import OrderedDict
# Normal dict'ten farkı: karşılaştırma sırayı dikkate alır
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
print(d1 == d2) # True — normal dict sırayı karşılaştırmaz
od1 = OrderedDict(a=1, b=2)
od2 = OrderedDict(b=2, a=1)
print(od1 == od2) # False — OrderedDict sıra önemli!
# move_to_end: Elemanı sona veya başa taşı
od = OrderedDict(a=1, b=2, c=3)
od.move_to_end("a") # Sona taşı
print(list(od.keys())) # ['b', 'c', 'a']
od.move_to_end("a", last=False) # Başa taşı
print(list(od.keys())) # ['a', 'b', 'c']ChainMap
Birden fazla dictionary'yi zincirleme arar — kopyalamadan:
from collections import ChainMap
# Config katmanları: kullanıcı > proje > global
global_config = {"tema": "light", "dil": "tr", "font": "Arial", "size": 14}
proje_config = {"tema": "dark", "font": "Fira Code"}
kullanici_config = {"size": 16}
config = ChainMap(kullanici_config, proje_config, global_config)
print(config["size"]) # 16 (kullanıcı)
print(config["tema"]) # dark (proje)
print(config["dil"]) # tr (global)
print(config["font"]) # Fira Code (proje)
# Tüm benzersiz key'ler
print(list(config.keys())) # Tüm katmanlardaki tüm key'lerChainMap gerçek birleştirme yapmaz — her aramada zinciri baştan tarar. Bu, dinamik config sistemlerinde çok kullanışlı: bir katmanı değiştirdiğinde hemen yansır.
Counter (Hatırlatma)
from collections import Counter
kelimeler = "the quick brown fox jumps over the lazy dog the fox".split()
c = Counter(kelimeler)
print(c.most_common(3)) # [('the', 3), ('fox', 2), ('quick', 1)]
# Aritmetik
c1 = Counter(a=4, b=2, c=0, d=-2)
c2 = Counter(a=1, b=2, c=3)
print(c1 + c2) # Counter({'a': 5, 'b': 4, 'c': 3})
print(c1 - c2) # Counter({'a': 3}) — 0 ve negatifler atılırarray Modülü: Numpy'a Köprü
Python'un array modülü, C tipi dizi sunar — tüm elemanlar aynı tipte olmalı. List'ten daha az bellek kullanır ama çok da kullanışlı değildir. Asıl güç numpy'dadır.
import array
# Tip kodu ile oluştur
# 'i' = signed int, 'f' = float, 'd' = double
int_dizisi = array.array('i', [1, 2, 3, 4, 5])
float_dizisi = array.array('d', [1.0, 2.5, 3.7])
print(int_dizisi) # array('i', [1, 2, 3, 4, 5])
print(float_dizisi[0]) # 1.0
# Tip kısıtlaması
# int_dizisi.append(3.14) # TypeError — sadece int!
int_dizisi.append(6)
print(int_dizisi) # array('i', [1, 2, 3, 4, 5, 6])Bellek Karşılaştırması
import sys
import array
# 1000 int
liste = list(range(1000))
arr = array.array('i', range(1000))
print(f"List: {sys.getsizeof(liste):,} byte") # ~8,856 byte
print(f"Array: {sys.getsizeof(arr):,} byte") # ~4,064 byte
# array yaklaşık yarı bellek kullanıyor!Numpy'a Geçiş
# array modülü → sınırlı
# Sayısal hesaplamalar için numpy çok daha güçlü
# import numpy as np
# arr = np.array([1, 2, 3, 4, 5])
# print(arr * 2) # [2, 4, 6, 8, 10] — element-wise!
# print(arr.mean()) # 3.0
# print(arr.std()) # 1.414...
# print(arr.reshape(5, 1)) # 5x1 matris
# numpy'ı ileriki derslerde detaylı göreceğizarray modülü nadiren doğrudan kullanılır. Ama "neden list yerine numpy array?" sorusunun temeli burada: tip kısıtlaması sayesinde bellek tasarrufu ve C-seviyesi hız.
Mutable vs Immutable
Bu ayrım Python'un en temel kavramlarından biri ve veri yapısı seçimini doğrudan etkiler:
Mutable (Değiştirilebilir)
# List — mutable
liste = [1, 2, 3]
liste[0] = 99 # ✅ Değiştirilebilir
liste.append(4) # ✅ Eklenebilir
# Dict — mutable
d = {"a": 1}
d["b"] = 2 # ✅ Eklenebilir
d["a"] = 99 # ✅ Değiştirilebilir
# Set — mutable
s = {1, 2, 3}
s.add(4) # ✅ Eklenebilir
s.remove(1) # ✅ SilinebilirImmutable (Değiştirilemez)
# Tuple — immutable
t = (1, 2, 3)
# t[0] = 99 # ❌ TypeError
# t.append(4) # ❌ AttributeError
# String — immutable
s = "merhaba"
# s[0] = "M" # ❌ TypeError
yeni = "M" + s[1:] # ✅ Yeni string oluştur
# frozenset — immutable
fs = frozenset({1, 2, 3})
# fs.add(4) # ❌ AttributeError
# int, float, bool — immutable
x = 5
x = x + 1 # Yeni nesne oluşturur, eski 5'i değiştirmezNeden Önemli?
# 1. Dict key ve set elemanı olabilme
d = {(1, 2): "tuple OK"} # ✅ Immutable → hashable
# d = {[1, 2]: "list NO"} # ❌ Mutable → unhashable
# 2. Fonksiyon argümanı güvenliği
def islem(veri):
veri.append(99) # Orijinali de değiştirir!
original = [1, 2, 3]
islem(original)
print(original) # [1, 2, 3, 99] — Dikkat!
# Tuple olsaydı bu mümkün değildi
def guvenli_islem(veri):
# veri.append(99) # AttributeError — tuple immutable!
pass
# 3. Thread safety
# Immutable nesneler thread-safe — yarış koşulu yokKarşılaştırma Tablosu
| Tip | Mutable | Hashable | Dict Key? | Set Eleman? |
|---|---|---|---|---|
int | ❌ | ✅ | ✅ | ✅ |
float | ❌ | ✅ | ✅ | ✅ |
str | ❌ | ✅ | ✅ | ✅ |
bool | ❌ | ✅ | ✅ | ✅ |
tuple | ❌ | ✅* | ✅* | ✅* |
frozenset | ❌ | ✅ | ✅ | ✅ |
list | ✅ | ❌ | ❌ | ❌ |
dict | ✅ | ❌ | ❌ | ❌ |
set | ✅ | ❌ | ❌ | ❌ |
\* Tüm elemanları da hashable ise
Bellek Kullanımı Karşılaştırması
import sys
# Aynı veriyi farklı yapılarda tut
n = 1000
veriler_liste = list(range(n))
veriler_tuple = tuple(range(n))
veriler_set = set(range(n))
veriler_dict = {i: i for i in range(n)}
print(f"List: {sys.getsizeof(veriler_liste):>8,} byte")
print(f"Tuple: {sys.getsizeof(veriler_tuple):>8,} byte")
print(f"Set: {sys.getsizeof(veriler_set):>8,} byte")
print(f"Dict: {sys.getsizeof(veriler_dict):>8,} byte")Tipik sonuçlar (1000 eleman):
List: 8,856 byte
Tuple: 8,048 byte
Set: 32,984 byte
Dict: 36,968 byteDikkat et: Set ve dict, list/tuple'dan 4x daha fazla bellek kullanır. Bu hash tablosu yapısının maliyeti — hızlı arama (O(1)) için ödenen bedel.
Elemanların Bellek Kullanımı
import sys
# Temel tipler
print(f"int (0): {sys.getsizeof(0)} byte")
print(f"int (1): {sys.getsizeof(1)} byte")
print(f"int (büyük): {sys.getsizeof(10**100)} byte")
print(f"float: {sys.getsizeof(3.14)} byte")
print(f"bool: {sys.getsizeof(True)} byte")
print(f"str (boş): {sys.getsizeof('')} byte")
print(f"str (5 harf): {sys.getsizeof('merhb')} byte")
print(f"None: {sys.getsizeof(None)} byte")⚠️ Dikkat:
sys.getsizeof()sadece nesnenin kendi boyutunu verir, içindeki nesnelerin boyutunu hesaplamaz. Gerçek bellek kullanımını ölçmek içinpymplerveyatracemallocmodüllerini kullan:
>
```python import tracemalloc tracemalloc.start()
>
buyuk_liste = [list(range(100)) for _ in range(1000)]
>
current, peak = tracemalloc.get_traced_memory() print(f"Şu an: {current / 1024:.1f} KB") print(f"Zirve: {peak / 1024:.1f} KB") tracemalloc.stop() ```
Pratik Karşılaştırma: Aynı Problem, Farklı Yapılar
Problem: Benzersiz Kelime Sayımı
metin = "python güzel python harika güzel bir dil python harika bir güzel dil"
kelimeler = metin.split()
# List ile — O(n²) zaman
benzersiz_list = []
for kelime in kelimeler:
if kelime not in benzersiz_list: # O(n) arama her seferinde
benzersiz_list.append(kelime)
print(f"Benzersiz (list): {benzersiz_list}")
# Set ile — O(n) zaman
benzersiz_set = set(kelimeler) # O(1) ekleme ve dedup
print(f"Benzersiz (set): {benzersiz_set}")
# Dict ile — O(n) zaman + frekans bilgisi
from collections import Counter
frekans = Counter(kelimeler)
print(f"Frekans (dict): {frekans}")Bu örnekte:
List: O(n²) — her kelime için tüm listeyi tarar
Set: O(n) — hash tablosu ile anında dedup
Counter/Dict: O(n) — dedup + sayım bilgisi
Problem: Öğrenci Verisi Saklama
# List of tuples — basit ama erişim indeksle
ogrenciler_lot = [
("Ahmet", 85, "A"),
("Ayşe", 92, "B"),
]
# ogrenciler_lot[0][1] → 85 (ama 1 ne anlama geliyor?)
# List of dicts — okunabilir
ogrenciler_lod = [
{"isim": "Ahmet", "not": 85, "sinif": "A"},
{"isim": "Ayşe", "not": 92, "sinif": "B"},
]
# ogrenciler_lod[0]["not"] → 85 (net!)
# Dict of dicts — key ile hızlı erişim
ogrenciler_dod = {
"Ahmet": {"not": 85, "sinif": "A"},
"Ayşe": {"not": 92, "sinif": "B"},
}
# ogrenciler_dod["Ahmet"]["not"] → 85 (O(1) erişim!)
# Named tuple — struct benzeri, immutable
from collections import namedtuple
Ogrenci = namedtuple("Ogrenci", "isim not_ sinif")
ogrenciler_nt = [
Ogrenci("Ahmet", 85, "A"),
Ogrenci("Ayşe", 92, "B"),
]
# ogrenciler_nt[0].not_ → 85 (okunabilir + immutable)Her yaklaşımın avantajı var — senaryona göre seç:
Sıra önemliyse: list of dicts
Key ile erişim gerekiyorsa: dict of dicts
Immutability istiyorsan: named tuple
Basit ve hafif: list of tuples
Dönüşüm Rehberi
Bir yapıdan diğerine kolayca geçiş yapabilirsin:
# List ↔ Tuple
liste = [1, 2, 3]
demet = tuple(liste) # (1, 2, 3)
liste2 = list(demet) # [1, 2, 3]
# List ↔ Set
liste = [1, 2, 2, 3, 3]
kume = set(liste) # {1, 2, 3} — duplicate temizlendi
liste2 = list(kume) # [1, 2, 3] — sıra garanti değil
# List ↔ Dict (iki yönlü)
ciftler = [("a", 1), ("b", 2)]
sozluk = dict(ciftler) # {'a': 1, 'b': 2}
ciftler2 = list(sozluk.items()) # [('a', 1), ('b', 2)]
# Dict → List (key'ler veya value'lar)
keyler = list(sozluk.keys()) # ['a', 'b']
degerler = list(sozluk.values()) # [1, 2]
# Set ↔ Frozenset
kume = {1, 2, 3}
donmus = frozenset(kume) # frozenset({1, 2, 3})
kume2 = set(donmus) # {1, 2, 3}
# Sırayı koruyarak list → benzersiz list
orijinal = [3, 1, 4, 1, 5, 9, 2, 6, 5]
benzersiz = list(dict.fromkeys(orijinal))
print(benzersiz) # [3, 1, 4, 5, 9, 2, 6]Sonuç: Hızlı Referans Kartı
📋 LIST — Sıralı, değişebilir, duplicate OK
En iyi: Sıralı koleksiyon, indeksle erişim, sıralama
Kaçın: Sık arama (in), başa ekleme
📌 TUPLE — Sıralı, değişmez, duplicate OK
En iyi: Sabit veri, dict key, fonksiyon return
Kaçın: Ekleme/çıkarma gereken yerler
🎯 SET — Sırasız, değişebilir, benzersiz
En iyi: Üyelik testi, dedup, küme operasyonları
Kaçın: İndeksle erişim, sıra gerektiren yerler
📖 DICT — Key-value, değişebilir, key benzersiz
En iyi: Lookup, mapping, config, cache
Kaçın: Sadece değer listesi gerektiğinde (list yeterli)Özet
List vs Tuple: Veri değişecekse list, değişmeyecekse tuple. Tuple daha hızlı, daha az bellek, hashable.
List vs Set: Sıra önemliyse ve duplicate izin veriliyorsa list; benzersizlik ve hızlı arama (O(1)) gerekiyorsa set.
Dict: Key-value mapping gereken her yerde birinci tercih. O(1) erişim,
defaultdictveCounterile genişletilebilir.collections modülü:
deque(iki uçtan O(1) ekleme/çıkarma),OrderedDict(sıra duyarlı karşılaştırma),ChainMap(config katmanları) özel senaryolarda standart yapılardan üstün.Bellek: Tuple < List < Set < Dict. Hash tablosu yapıları (set, dict) 3-4x daha fazla bellek kullanır — hız için ödenen bedel.
Karar ağacı: Key-value → dict, benzersizlik → set, değişmez → tuple, geri kalan → list. Basit ama etkili.
AI Asistan
Sorularını yanıtlamaya hazır