flowchart LR
A[a] --> L["[10, 20, 30, 40]"]
B[b] --> L
Listeler, demetler, mutability ve aliasing
Programlama Temelleri dersinde list ve tuple yapılarını kullandınız. Bu bölümde sözdizimini yeniden öğrenmeyeceğiz. Bunun yerine bu yapıların veri yapısı olarak nasıl davrandığına, hangi işlemlerin neden daha maliyetli olduğuna ve değiştirilebilir nesnelerde ortaya çıkan aliasing sorununa odaklanacağız.
Bu bölüm, sonraki haftalarda yığın, kuyruk, arama ve sıralama konularını anlamak için önemli bir temel oluşturur.
1 Bu hafta neleri yapabilmelisiniz?
Bölümün sonunda:
listiletuplearasındaki farkı kullanım amacı açısından açıklayabilmeli,- mutable ve immutable kavramlarını ayırt edebilmeli,
- liste indeksleme, sona ekleme, başa ekleme ve silme işlemlerinin maliyetlerini sezgisel düzeyde karşılaştırabilmeli,
- Python listesinin bir dinamik dizi gibi düşünülebileceğini açıklayabilmeli,
- iki değişkenin aynı listeyi göstermesi durumunu yani aliasing davranışını tahmin edebilmeli,
- bağımsız bir kopya gerektiğinde
.copy()veya dilimleme kullanabilmeli, - iç içe listelerde sığ kopya (shallow copy) sınırını gözlemleyebilmeli,
- bağlı liste fikrini Python listesiyle kavramsal düzeyde karşılaştırabilmelisiniz.
2 Liste ve demeti neden tekrar ele alıyoruz?
Aşağıdaki iki kod parçası size tanıdık gelmelidir:
puanlar = [70, 80, 90]
koordinat = (41.29, 36.33)İlk derste bu yapıların nasıl oluşturulduğunu ve nasıl dolaşıldığını öğrendiniz. Şimdi daha farklı sorular soracağız:
puanlar[2]erişimi neden hızlıdır?puanlar.append(95)ilepuanlar.insert(0, 95)neden aynı maliyette değildir?yedek = puanlargerçekten bir yedek oluşturur mu?- Bir veriyi neden bazen
tupleolarak tutmak isteriz?
Bu sorular veri yapısı bakış açısının başlangıcıdır.
3 Mutable ve immutable
Python’da bazı nesnelerin içeriği oluşturulduktan sonra değiştirilebilir, bazılarınınki değiştirilemez.
- mutable (değiştirilebilir):
list,dict,set - immutable (değiştirilemez):
tuple,str,int,float
Bir listenin elemanını değiştirebiliriz:
#| edit: false
#| completion: false
puanlar = [70, 80, 90]
puanlar[0] = 75
puanlar.append(95)
print(puanlar)
Bir demetin elemanını ise aynı biçimde değiştiremeyiz:
koordinat = (41.29, 36.33)
# koordinat[0] = 40.0 # TypeErrorBir değişken adı daha sonra başka bir tuple nesnesine bağlanabilir. Değiştirilemeyen şey, oluşturulmuş tuple nesnesinin kendi elemanlarıdır.
Örneğin:
#| edit: false
#| completion: false
konum = (41.29, 36.33)
print("önce:", konum)
konum = (40.98, 37.88)
print("sonra:", konum)
Burada ilk tuple değişmedi; konum adı başka bir tuple nesnesine yeniden bağlandı.
4 Listeyi veri yapısı olarak düşünmek
Python listesi, sezgisel olarak bir dinamik dizi (dynamic array) gibi düşünülebilir. Elemanlar indekslerle erişilebilen sıralı bir yapı içinde tutulur.
İndeks: 0 1 2 3
+------+------+------+------+
Değer: | 12 | 25 | 31 | 48 |
+------+------+------+------+
Bu düzen bize çok önemli bir avantaj sağlar: belirli bir indeksteki elemana doğrudan erişebiliriz.
#| edit: false
#| completion: false
veriler = [12, 25, 31, 48, 52, 67]
print(veriler[0])
print(veriler[4])
İndeks erişimini sezgisel olarak O(1) kabul ederiz.
5 Listenin sonuna eklemek neden genellikle ucuzdur?
append() listenin sonuna yeni eleman ekler:
#| edit: false
#| completion: false
kuyruk_gibi = ["A", "B", "C"]
kuyruk_gibi.append("D")
print(kuyruk_gibi)
Python listesi gerektiğinde daha geniş bir alan ayırabilir. Her append() işleminde bütün listeyi baştan taşımak gerekmez. Bu yüzden sona ekleme ortalama/amortize durumda O(1) olarak değerlendirilir.
Şimdilik “amortize” sözcüğünü şöyle düşünebilirsiniz:
Bazı tekil eklemeler daha pahalı olabilir; fakat çok sayıda
append()işleminin toplam maliyeti ele alındığında eleman başına maliyet sabite yakın davranır.
Bu ayrıntıyı ezberlemek yerine, şu pratik sonucu hatırlamak yeterlidir:
Listenin sonuna ekleme genellikle verimlidir.
6 Listenin başına eklemek neden farklıdır?
Şimdi başa eleman ekleyelim:
#| edit: false
#| completion: false
veriler = [20, 30, 40, 50]
veriler.insert(0, 10)
print(veriler)
Yeni eleman ilk konuma geldiğinde mevcut elemanların konumlarının değişmesi gerekir.
Önce:
[20][30][40][50]
10 başa eklenecek:
→ 20 sağa
→ 30 sağa
→ 40 sağa
→ 50 sağa
Sonra:
[10][20][30][40][50]
Bu yüzden başa ekleme genel olarak O(n) davranışı gösterir.
7 İşlem maliyetlerini birlikte görelim
Aşağıdaki tablo Python listesi için bu derste kullanacağımız temel sezgiyi özetler:
| İşlem | Sezgisel maliyet |
|---|---|
liste[i] |
O(1) |
liste.append(x) |
amortize O(1) |
liste.pop() |
O(1) |
x in liste |
O(n) |
liste.insert(0, x) |
O(n) |
liste.pop(0) |
O(n) |
Bu tablo tüm Python uygulamalarının her ayrıntısını açıklayan mutlak bir performans garantisi değildir. Ders boyunca yapısal karar verebilmek için kullandığımız maliyet sezgisidir.
8 Deney — başa ve sona ekleme
Aşağıdaki kodu birkaç kez çalıştırın. Süreler aynı çıkmayabilir; önemli olan genel eğilimdir.
#| edit: false
#| completion: false
import time
n = 50_000
tekrar = 1_000
liste = list(range(n))
baslangic = time.perf_counter()
for i in range(tekrar):
liste.append(i)
sona_ekleme = time.perf_counter() - baslangic
liste = list(range(n))
baslangic = time.perf_counter()
for i in range(tekrar):
liste.insert(0, i)
basa_ekleme = time.perf_counter() - baslangic
print("Sona ekleme:", round(sona_ekleme, 6), "s")
print("Başa ekleme:", round(basa_ekleme, 6), "s")
Tarayıcı içindeki çalışma ortamı kesin benchmark için uygun değildir. Buna rağmen işlemlerin neden farklı davrandığını görmek için yararlı bir deneydir.
9 Alıştırma — maliyeti tahmin edin
Aşağıdaki işlemleri O(1) veya O(n) olarak sınıflandırmaya çalışın:
1. puanlar[5]
2. puanlar.append(80)
3. 80 in puanlar
4. puanlar.insert(0, 80)
5. puanlar.pop()
6. puanlar.pop(0)
Yanıtlarınızı vermeden önce her işlemde kaç elemanın etkilenebileceğini düşünün.
10 Aliasing: iki ad, tek liste
Bu bölümün en kritik kavramlarından biri aliasingdir. İki farklı değişken adı aynı mutable nesneyi gösterebilir.
Aşağıdaki kodu çalıştırmadan önce çıktıyı tahmin edin:
#| edit: false
#| completion: false
a = [10, 20, 30]
b = a
b.append(40)
print("a =", a)
print("b =", b)
b = a yeni bir liste üretmez. a ve b aynı liste nesnesine erişir.
Dolayısıyla b üzerinden yapılan değişiklik a üzerinden de görülür.
11 Kimlik ve eşitlik
İki listenin içeriği aynı olabilir ama bunlar farklı nesneler olabilir.
#| edit: false
#| completion: false
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print("a == b:", a == b)
print("a is b:", a is b)
print("a == c:", a == c)
print("a is c:", a is c)
Burada iki ayrı soru vardır:
==→ içerik/değer bakımından eşit mi?is→ aynı nesne mi?
is ile genel değer karşılaştırması yapmayın
is, nesne kimliğini kontrol eder. Sayı ve string gibi değerleri karşılaştırmak için genel olarak == kullanılır.
12 Alıştırma — aliasing sonucunu tahmin edin
Aşağıdaki kodda boşluğu doldurmadan önce sonucun ne olacağını düşünün. Amaç yedek üzerinden yapılan değişikliğin notlar listesini de etkilediğini gözlemlemektir.
#| exercise: hafta02-aliasing
#| completion: false
#| persist: true
notlar = [60, 70, 80]
yedek = ______
yedek[0] = 100
print("notlar:", notlar)
print("yedek :", yedek)
#| exercise: hafta02-aliasing
#| check: true
import ast
try:
tree = ast.parse(user_code)
assignments = [n for n in ast.walk(tree) if isinstance(n, ast.Assign)]
aliases = False
for node in assignments:
targets = [t.id for t in node.targets if isinstance(t, ast.Name)]
if "yedek" in targets and isinstance(node.value, ast.Name) and node.value.id == "notlar":
aliases = True
correct = aliases
except SyntaxError:
correct = False
feedback = (
{"correct": True, "message": "yedek ve notlar aynı listeyi gösteriyor. Değişikliğin iki ad üzerinden de görünmesine dikkat edin."}
if correct
else {"correct": False, "message": "Aliasing oluşturmak için yedek = notlar yazın."}
)
feedback
Yeni bir liste üretmeyin; yedek adını doğrudan notlar değişkeninin gösterdiği nesneye bağlayın.
Solution.
notlar = [60, 70, 80]
yedek = notlar
yedek[0] = 100
print("notlar:", notlar)
print("yedek :", yedek)13 Gerçek bir kopya nasıl oluşturulur?
Bağımsız bir liste istiyorsak yeni bir liste nesnesi üretmeliyiz.
13.1 .copy() kullanmak
#| edit: false
#| completion: false
a = [1, 2, 3]
b = a.copy()
b.append(4)
print("a:", a)
print("b:", b)
print("aynı nesne mi?", a is b)
13.2 Dilimleme kullanmak
b = a[:]Bu da yeni bir liste oluşturur.
13.3 list() kullanmak
b = list(a)Bu üç yaklaşım tek katmanlı basit listelerde bağımsız bir dış liste elde etmek için kullanılabilir.
14 Alıştırma — gerçek yedek oluşturun
Aşağıdaki programda yedek_notlar değişkeni bağımsız bir liste olmalıdır. yedek_notlar[1] değiştirildiğinde notlar değişmemelidir.
#| exercise: hafta02-kopya
#| completion: false
#| persist: true
notlar = [50, 65, 70, 90]
yedek_notlar = ______
yedek_notlar[1] = 100
print("orijinal:", notlar)
print("yedek :", yedek_notlar)
#| exercise: hafta02-kopya
#| check: true
import ast
try:
tree = ast.parse(user_code)
correct = False
for node in ast.walk(tree):
if isinstance(node, ast.Assign):
targets = [t.id for t in node.targets if isinstance(t, ast.Name)]
if "yedek_notlar" not in targets:
continue
v = node.value
if isinstance(v, ast.Call) and isinstance(v.func, ast.Attribute) and v.func.attr == "copy":
correct = True
elif isinstance(v, ast.Call) and isinstance(v.func, ast.Name) and v.func.id == "list":
correct = True
elif isinstance(v, ast.Subscript) and isinstance(v.slice, ast.Slice):
correct = True
except SyntaxError:
correct = False
feedback = (
{"correct": True, "message": "Bağımsız dış liste oluşturuldu. Şimdi iki çıktının farklı olduğunu doğrulayın."}
if correct
else {"correct": False, "message": "notlar.copy(), list(notlar) veya notlar[:] ile yeni liste oluşturun."}
)
feedback
En okunur seçeneklerden biri yedek_notlar = notlar.copy() ifadesidir.
Solution.
notlar = [50, 65, 70, 90]
yedek_notlar = notlar.copy()
yedek_notlar[1] = 100
print("orijinal:", notlar)
print("yedek :", yedek_notlar)15 Sığ kopyanın sınırı
.copy() yeni bir dış liste oluşturur. Fakat dış listenin içindeki elemanlar da mutable nesnelerse bu iç nesneler paylaşılmaya devam edebilir.
Aşağıdaki kodu çalıştırmadan önce sonucu tahmin edin:
#| edit: false
#| completion: false
siniflar = [
["Ali", "Ayşe"],
["Deniz", "Ece"],
]
yedek = siniflar.copy()
yedek[0].append("Mert")
print("siniflar:", siniflar)
print("yedek :", yedek)
print("dış liste aynı mı?", siniflar is yedek)
print("ilk iç liste aynı mı?", siniflar[0] is yedek[0])
Bu davranışın adı sığ kopya (shallow copy)dır.
flowchart LR
A[siniflar] --> O1[Dış liste 1]
B[yedek] --> O2[Dış liste 2]
O1 --> I1["['Ali','Ayşe','Mert']"]
O1 --> I2["['Deniz','Ece']"]
O2 --> I1
O2 --> I2
Dış listeler farklıdır; iç listeler aynı nesnelerdir.
deepcopy bu haftanın ana konusu değildir
copy.deepcopy() gibi araçlar iç içe yapıların bağımsız kopyalarını oluşturabilir. Ancak burada asıl hedef, .copy() işleminin her seviyede tamamen bağımsız bir yapı oluşturmadığını fark etmektir.
16 Fonksiyonlar ve mutable nesneler
Bir listeyi fonksiyona verdiğimizde Python listeyi otomatik olarak kopyalamaz. Fonksiyon aynı nesneye erişebilir.
#| edit: false
#| completion: false
def puan_ekle(puanlar, yeni_puan):
puanlar.append(yeni_puan)
notlar = [70, 80]
puan_ekle(notlar, 90)
print(notlar)
Fonksiyonun yaptığı değişiklik çağıranın listesinden de görünür.
Buna karşılık fonksiyon içinde değişkeni yeni bir listeye yeniden bağlamak çağıranın değişkenini yeniden bağlamaz:
#| edit: false
#| completion: false
def degistir(veriler):
veriler = [999]
print("fonksiyon içinde:", veriler)
sayilar = [1, 2, 3]
degistir(sayilar)
print("fonksiyon dışında:", sayilar)
Bu ayrım, ilerleyen haftalarda veri yapıları üzerinde fonksiyon tasarlarken sık sık karşımıza çıkacaktır.
17 Demet ne zaman daha anlamlıdır?
tuple yalnızca “değiştirilemeyen liste” olarak düşünülmemelidir. Bir grup değerin tek ve sabit yapılı bir kayıt gibi kullanılacağı durumlarda anlamlı olabilir.
Örneğin bir koordinat:
#| edit: false
#| completion: false
konum = (41.2867, 36.33)
enlem, boylam = konum
print("Enlem:", enlem)
print("Boylam:", boylam)
veya bir RGB rengi:
kirmizi = (255, 0, 0)Burada “bu üç değerin sırası ve bütünlüğü belli, günlük işlem sırasında eleman ekleyip çıkarmayacağız” fikri vardır.
18 Liste mi, demet mi?
Aşağıdaki karar soruları yardımcı olabilir:
| Soru | Eğilim |
|---|---|
| Eleman ekleyip çıkaracak mıyım? | list |
| Elemanları yerinde değiştirecek miyim? | list |
| Yapı sabit kalmalı mı? | tuple düşünülebilir |
| Kayıt küçük ve sabit konumlu alanlardan mı oluşuyor? | tuple düşünülebilir |
Bu da mutlak bir reçete değildir; problem bağlamı önemlidir.
19 Kavram köşesi: bağlı liste fikri
Python’da kendi Node ve LinkedList sınıflarımızı yazmak bu dersin çekirdek hedefi değildir. Yine de bağlı liste (linked list) fikri, farklı veri yapılarının neden farklı maliyetlere sahip olduğunu anlamamıza yardımcı olur.
Dinamik dizide elemanları yan yana konumlandırılmış gibi düşünürüz:
[10][20][30][40]
Bağlı yapıda ise her düğüm bir sonraki düğüme bağlantı taşır:
flowchart LR
N1[10] --> N2[20]
N2 --> N3[30]
N3 --> N4[40]
N4 --> X[None]
Bu fark bazı işlemleri kolaylaştırırken bazılarını zorlaştırır:
- dinamik dizide indeksle erişim güçlüdür,
- bağlı yapıda doğrudan “17. elemana git” işlemi doğal değildir,
- bağlı yapıların belirli konumlardaki ekleme/silme davranışı farklı olabilir.
Bu bölümde bağlı liste kodlamanız beklenmez. Amaç, verinin bellekte/düğümlerde farklı düzenlenmesinin işlem maliyetini değiştirebildiğini kavramaktır.
20 Hatalı yaklaşımı düzeltin
Bir öğrenci, listenin başından sürekli eleman çıkarması gereken bir işlem yazıyor:
isler = ["A", "B", "C", "D"]
sonraki = isler.pop(0)Bu kod küçük listelerde çalışır ve yanlış değildir. Fakat işlem binlerce kez yapılacaksa listenin başından silmenin O(n) davranışı sorun olabilir.
Altıncı haftada collections.deque yapısını öğreneceğiz ve bu senaryoyu tekrar değerlendireceğiz. Şimdilik önemli olan şudur:
Çalışan bir liste çözümü görmek, onun problem için en uygun veri yapısı olduğunu kanıtlamaz.
21 Uygulama — aliasing hatasını düzeltin
Aşağıdaki program bir ürün listesini güncellemeden önce yedeklemek istiyor. Ancak programcı gerçek bir kopya oluşturmamış.
#| exercise: hafta02-yedek-hatasi
#| completion: false
#| persist: true
urunler = ["kalem", "defter", "silgi"]
yedek = urunler
# Yalnızca bu satırın üstündeki kodu düzelterek
# yedek değişkenini bağımsız bir liste yapın.
yedek.remove("defter")
print("orijinal:", urunler)
print("yedek :", yedek)
Beklenen davranış:
orijinal: ['kalem', 'defter', 'silgi']
yedek : ['kalem', 'silgi']
#| exercise: hafta02-yedek-hatasi
#| check: true
import ast
try:
tree = ast.parse(user_code)
correct = False
for node in ast.walk(tree):
if not isinstance(node, ast.Assign):
continue
targets = [t.id for t in node.targets if isinstance(t, ast.Name)]
if "yedek" not in targets:
continue
v = node.value
if isinstance(v, ast.Call) and isinstance(v.func, ast.Attribute) and v.func.attr == "copy":
correct = True
elif isinstance(v, ast.Call) and isinstance(v.func, ast.Name) and v.func.id == "list":
correct = True
elif isinstance(v, ast.Subscript) and isinstance(v.slice, ast.Slice):
correct = True
except SyntaxError:
correct = False
feedback = (
{"correct": True, "message": "Yedek artık bağımsız bir dış liste. remove() orijinal listeyi değiştirmemeli."}
if correct
else {"correct": False, "message": "yedek = urunler doğrudan alias oluşturur. Yeni bir liste üretin."}
)
feedback
yedek = urunler.copy() en açık çözümlerden biridir.
Solution.
urunler = ["kalem", "defter", "silgi"]
yedek = urunler.copy()
yedek.remove("defter")
print("orijinal:", urunler)
print("yedek :", yedek)22 Kendinizi kontrol edin
listvetuplearasındaki temel davranış farkı nedir?- Mutable bir nesne ile immutable bir nesne arasındaki farkı örnekle açıklayın.
a = [1, 2]veb = aifadelerinden sonra kaç ayrı liste nesnesi vardır?a == bilea is bfarklı hangi soruları sorar?list.append()ilelist.insert(0, x)neden farklı maliyetlere sahiptir?.copy()neden iç içe listelerde her şeyi tamamen bağımsız hâle getirmeyebilir?- Python listesini dinamik dizi olarak düşünmek hangi işlemlerin maliyetini açıklamamıza yardımcı olur?
- Bağlı liste fikri neden bu derste sınıf yazmadan da öğrenmeye değerdir?
23 Bölüm sonu mini görev
Bir müzik uygulamasında şarkılar bir listede tutuluyor:
sarkilar = ["A", "B", "C", "D"]Aşağıdaki dört gereksinimi ayrı ayrı değerlendirin:
- Beşinci şarkıya indeksle erişmek.
- Listenin sonuna yeni şarkı eklemek.
- Listenin başına her saniye yeni şarkı eklemek.
- Listeyi düzenlemeden önce bağımsız bir yedek oluşturmak.
Her madde için hangi Python işlemini kullanacağınızı ve maliyet/aliasing açısından dikkat edilmesi gereken noktayı bir cümleyle açıklayın.
24 Bu haftadan aklınızda kalsın
Python listesi yalnızca “birden fazla değer tutan yapı” değildir.
İndeksleme, ekleme, silme ve üyelik işlemlerinin farklı maliyetleri vardır. Ayrıca mutable olduğu için aynı listeyi birden fazla değişken üzerinden paylaşmak beklenmedik yan etkilere yol açabilir.