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öz dizimini yeniden öğrenmeyeceğiz. Bunun yerine üç soruya bakacağız: Bu yapılar veri yapısı olarak nasıl davranır? Hangi işlemler neden daha maliyetlidir? İki değişken aynı listeyi gösterdiğinde ne olur? Bu son duruma aliasing (aynı nesneye referans verme) denir.
Sonraki haftalarda yığın, kuyruk, arama ve sıralama konularını bu bölümdeki fikirlerin üzerine kuracağız.
1 Bu hafta neleri yapabilmelisiniz?
Bölümün sonunda:
listiletuplearasındaki farkı kullanım amacı açısından açıklayabilmeli,- Değiştirilebilir (mutable) ve değiştirilemez (immutable) nesneleri 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,
- İki değişken aynı listeyi gösterdiğinde (aliasing) programın ne yapacağını tahmin edebilmeli,
- Bağımsız bir kopya gerektiğinde
.copy()veya dilimleme kullanabilmeli, - İç içe listelerde sığ kopya (shallow copy) alındığında neyin ortak kaldığı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 satır size tanıdık gelecek:
puanlar = [70, 80, 90]
koordinat = (41.29, 36.33)Programlama Temelleri’nde bu yapıları oluşturmayı ve dolaşmayı öğrendiniz. Şimdi başka 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?
Veriye bir veri yapısı gözüyle bakmak bu tür sorularla başlar.
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:
Bir demetin elemanını ise aynı biçimde değiştiremeyiz:
koordinat = (41.29, 36.33)
# koordinat[0] = 40.0 # TypeErrorDeğiştirilemeyen şey, oluşturulmuş tuple nesnesinin elemanlarıdır. Değişken adı ise sonradan başka bir tuple nesnesini gösterebilir.
Örneğin:
Burada ilk tuple değişmedi; konum adı artık başka bir tuple nesnesini gösteriyor. Buna adı yeniden bağlamak denir.
4 Listeyi veri yapısı olarak düşünmek
Python listesini bir dinamik dizi (dynamic array) gibi düşünebilirsiniz: elemanlar sırayla yan yana durur, her birine indeksiyle ulaşılır ve dizi gerektiğinde büyür.
İndeks: 0 1 2 3
+------+------+------+------+
Değer: | 12 | 25 | 31 | 48 |
+------+------+------+------+
Bu düzenin büyük bir avantajı var: belirli bir indeksteki elemana doğrudan erişebiliriz.
İndeks erişimini sezgisel olarak O(1) kabul ederiz.
5 Listenin sonuna eklemek neden genellikle ucuzdur?
append() listenin sonuna yeni eleman ekler:
Python listesi dolduğunda kendine daha geniş bir alan ayırır. Böylece her append() çağrısında bütün listeyi baştan taşımak gerekmez. Bu yüzden sona ekleme ortalama (amortize) durumda O(1) sayılır.
Şimdilik “amortize” sözcüğünü şöyle düşünebilirsiniz:
Arada bir ekleme pahalıya gelebilir. Ama çok sayıda
append()çağrısının toplam maliyetini eleman sayısına bölersek eleman başına sabite yakın bir maliyet çıkar.
Ayrıntıyı ezberlemeniz gerekmez; şu sonucu hatırlamanız yeter:
Listenin sonuna ekleme genellikle verimlidir.
6 Listenin başına eklemek neden farklıdır?
Şimdi başa eleman ekleyelim:
Yeni elemanı ilk konuma koymak için mevcut elemanları birer konum kaydırmak 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 O(n)’dir: liste uzadıkça kaydırılacak eleman sayısı da artar.
7 İşlem maliyetlerini birlikte görelim
Aşağıdaki tablo, Python listesi için bu derste kullanacağımız maliyetleri ö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, her Python gerçekleştiriminde birebir geçerli bir performans garantisi değildir. Ders boyunca yapı seçerken dayanacağımız bir maliyet sezgisidir.
8 Deney: başa ve sona ekleme
Aşağıdaki kodu birkaç kez çalıştırın. Süreler her seferinde aynı çıkmayabilir; genel eğilime bakın.
Tarayıcıda çalışan Python, kesin performans ölçümü (benchmark) için uygun bir ortam değil. Yine de iki işlemin farklı davrandığını görmek için bu deney yeter.
9 Alıştırma: maliyeti tahmin edin
Aşağıdaki işlemlerin her biri O(1) mi, O(n) mi?
1. puanlar[5]
2. puanlar.append(80)
3. 80 in puanlar
4. puanlar.insert(0, 80)
5. puanlar.pop()
6. puanlar.pop(0)
Karar vermeden önce her işlemin kaç elemana dokunabileceğini düşünün.
10 Aliasing: iki ad, tek liste
Aliasing, iki farklı değişken adının aynı değiştirilebilir nesneyi göstermesidir. Bu bölümün en önemli kavramlarından biri budur.
Aşağıdaki kodu çalıştırmadan önce çıktıyı tahmin edin:
b = a yeni bir liste oluşturmaz. a ve b aynı liste nesnesini gösterir.
Bu yüzden b ile yapılan değişiklik a’dan da görülür.
11 Kimlik ve eşitlik
İçerikleri aynı olan iki liste farklı nesneler olabilir.
Burada iki ayrı soru soruluyor:
==→ içerikleri eşit mi?is→ aynı nesne mi?
is ile karşılaştırmayın
is, iki ifadenin aynı nesne olup olmadığını kontrol eder. Sayı ve string gibi değerleri karşılaştırırken == kullanın.
12 Alıştırma: aliasing sonucunu tahmin edin
Boşluğu doldurmadan önce sonucun ne olacağını düşünün. Bu alıştırmada yedek ile yapılan değişikliğin notlar listesini de etkilediğini göreceksiniz.
Yeni bir liste oluşturmayın; yedek adı da notlar değişkeninin gösterdiği listeyi göstersin.
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 oluşturmalıyız.
13.1 .copy() kullanmak
13.2 Dilimleme kullanmak
b = a[:]Bu da yeni bir liste oluşturur.
13.3 list() kullanmak
b = list(a)İç içe olmayan basit listelerde bu üç yoldan herhangi biriyle bağımsız bir kopya elde edersiniz.
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.
Okuması en kolay seçeneklerden biri yedek_notlar = notlar.copy() satırıdır.
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() yalnızca dıştaki listeyi kopyalar. Dış listenin elemanları da liste gibi değiştirilebilir nesnelerse bu iç nesneler iki kopya arasında ortak kalır.
Aşağıdaki kodu çalıştırmadan önce sonucu tahmin edin:
Bu tür kopyaya sığ kopya (shallow copy) denir.
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 konusuna bu hafta girmiyoruz
copy.deepcopy() iç içe bir yapıyı iç katmanlarıyla birlikte kopyalayabilir. Bu hafta .copy() çağrısının iç katmanları kopyalamadığını fark etmeniz yeter.
16 Fonksiyonlar ve mutable nesneler
Bir listeyi fonksiyona argüman olarak verdiğimizde Python listeyi kopyalamaz. Fonksiyon, çağıran kodla aynı listeyi kullanır.
Fonksiyonun yaptığı değişiklik, fonksiyonu çağıran kodun listesinde de görünür.
Fonksiyonun içinde parametreye yeni bir liste atamak ise dışarıdaki değişkeni etkilemez:
İlerleyen haftalarda veri yapılarıyla çalışan fonksiyonlar yazarken bu ayrım sık sık karşımıza çıkacak.
17 Demet ne zaman daha uygundur?
tuple’ı yalnızca “değiştirilemeyen liste” diye düşünmeyin. Birkaç değer birlikte tek ve yapısı sabit bir kayıt oluşturuyorsa tuple iyi bir seçim olabilir.
Örneğin bir koordinat:
veya bir RGB rengi:
kirmizi = (255, 0, 0)İki örnekte de değerlerin sırası ve sayısı bellidir. Program çalışırken bu yapılara eleman ekleyip çıkarmayız.
18 Liste mi, demet mi?
Karar verirken şu sorular işinize yarar:
| 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 tablo da kesin bir reçete değildir; kararı problemin kendisi belirler.
19 Kavram köşesi: bağlı liste fikri
Bu derste kendi Node ve LinkedList sınıflarımızı yazmayacağız. Yine de bağlı liste (linked list) fikri, farklı veri yapılarının maliyetlerinin neden farklı olduğunu anlamamıza yardım eder.
Dinamik dizide elemanların yan yana durduğunu düşünürüz:
[10][20][30][40]
Bağlı yapıda ise her eleman bir düğümde (node) durur ve her düğüm bir sonraki düğüme giden bağlantıyı tutar:
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 elemana indeksle doğrudan erişilir,
- Bağlı yapıda “17. elemana git” diye doğrudan atlanamaz; baştan başlayıp bağlantıları tek tek izlemek gerekir,
- Bağlı yapıda belirli konumlara ekleme ve silmenin maliyeti dinamik diziden farklı olabilir.
Bu bölümde bağlı liste kodlamanız beklenmiyor. Şunu görmeniz yeter: veri bellekte farklı düzenlenince aynı işlemin maliyeti değişebilir.
20 Hatalı yaklaşımı düzeltin
Bir öğrencinin programı bir listenin başından sürekli eleman çıkarıyor:
isler = ["A", "B", "C", "D"]
sonraki = isler.pop(0)Bu kod küçük listelerde çalışır ve yanlış değildir. Ama bu işlem binlerce kez yapılacaksa, listenin başından silmenin O(n) maliyeti sorun çıkarabilir.
Altıncı haftada collections.deque yapısını öğrenince bu örneğe geri döneceğiz. Şimdilik şunu aklınızda tutun:
Liste ile yazılan bir çözümün çalışması, listenin o problem için en uygun veri yapısı olduğunu göstermez.
21 Uygulama: aliasing hatasını düzeltin
Aşağıdaki program ürün listesini güncellemeden önce bir yedek almak istiyor, ama programcı gerçek bir kopya oluşturmamış.
Beklenen çıktı:
orijinal: ['kalem', 'defter', 'silgi']
yedek : ['kalem', 'silgi']
yedek = urunler.copy() en açık çözümlerden biridir.
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 == bvea is bhangi soruları sorar?list.append()ilelist.insert(0, x)işlemlerinin maliyeti neden farklıdır?.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?
- Bu derste sınıf yazmayacak olsak da bağlı liste fikrini öğrenmek neden işimize yarar?
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 ya da aliasing açısından nelere dikkat etmeniz gerektiğini bir cümleyle açıklayın.
24 Bu haftadan aklınızda kalsın
Listede her işlemin maliyeti aynı değildir.
İndeksleme ve sona ekleme ucuzdur; başa ekleme, baştan silme ve üyelik araması liste uzadıkça pahalılaşır. Liste değiştirilebilir olduğu için aynı listeyi birden fazla değişkenle paylaşmak da beklenmedik yan etkilere (side effect) yol açabilir.