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:

  • list ile tuple arası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) ile puanlar.insert(0, 95) neden aynı maliyette değildir?
  • yedek = puanlar gerçekten bir yedek oluşturur mu?
  • Bir veriyi neden bazen tuple olarak 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   # TypeError
NoteTuple değişmez, ama ad başka tuple’ı gösterebilir

Değ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)
Warning

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.

flowchart LR
    A[a] --> L["[10, 20, 30, 40]"]
    B[b] --> L

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?
ImportantDeğerleri 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.

Notedeepcopy 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.
Important

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

  1. list ve tuple arasındaki temel davranış farkı nedir?
  2. Mutable bir nesne ile immutable bir nesne arasındaki farkı örnekle açıklayın.
  3. a = [1, 2] ve b = a ifadelerinden sonra kaç ayrı liste nesnesi vardır?
  4. a == b ve a is b hangi soruları sorar?
  5. list.append() ile list.insert(0, x) işlemlerinin maliyeti neden farklıdır?
  6. .copy() neden iç içe listelerde her şeyi tamamen bağımsız hâle getirmeyebilir?
  7. Python listesini dinamik dizi olarak düşünmek hangi işlemlerin maliyetini açıklamamıza yardımcı olur?
  8. 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:

  1. Beşinci şarkıya indeksle erişmek.
  2. Listenin sonuna yeni şarkı eklemek.
  3. Listenin başına her saniye yeni şarkı eklemek.
  4. 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

Important

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.

Back to top