İçeriğe geç
academia.sh

Ders 05 / 16

Özelleşmiş Koleksiyonlar

Aynı gruplamayı setdefault ile yazmak 320, varsayılan sözlükle yazmak 100 kap var eder; bin öğelik akışta üç yazım da 1000 nesne üretir ama tutulan 1000'e karşı 5'tir ve döndürme listede üç kap kurarken kuyrukta hiç kurmaz.

İçindekiler

Önceki dersin son satırı kümenin yanıtlayamadığı soruyu yazdı: kaç kez. Küme yalnız üyelik tutar; sayıyı tutmaz, çünkü tutması gerekmez.

O sayıyı isteyen herkes aynı kalıbı elle yazar — bir sözlük açar, anahtar varsa artırır, yoksa birden başlatır. Aynı biçimde elle yazılan başka kalıplar da vardır: kayıtları anahtara göre kutulara ayırmak, bir akışın yalnız son birkaç öğesini elde tutmak, bir dizinin ucunu öbür ucuna taşımak. Bu ders o kalıpların standart kitaplıktaki hazır karşılıklarına bakar ve her birini elle yazılan eşdeğeriyle yan yana koyar. Ölçülecek iki sayı var: kaç satır ve kaç nesne.

Kalıbın İki Fiyatı

Bir kalıbın elle yazılmasının iki ayrı bedeli vardır ve bunlar bağımsızdır. Birincisi satırdır: yazılan, okunan, sınanan ve yanlış yazılabilen kod. İkincisi nesnedir: kalıbın çalışırken var ettiği ve tuttuğu şeyler. Bir yazım satırı azaltıp nesneyi artırabilir; ölçümdeki üç gruplama yazımından biri tam olarak bunu yapar.

Yığıt, kuyruk ve bağlı liste gibi yapıların kuramı Veri Yapıları kursunda kuruldu ve burada tekrarlanmaz. Burada ölçülen şey Python’un o kuyruğu hangi kapla verdiği ve elle yazılan karşılığına göre kaç satır ve kaç nesne tasarruf ettiğidir.

Satır sayısı elle sayılmaz: her yazım bir işlevin gövdesidir ve gövdenin boş olmayan satır sayısı işlevin kaynağından okunur. Sayı böylece bir iddia değil, koşumun bulgusu olur.

  • KL52 — Gövde satırı işlevin kaynağından okunur; tanım satırı ve boş satırlar sayılmaz. Her yazım aynı biçimde girintilenmiş ve aynı işi yapan bir işlevdir.
  • KL53 — Üç sayma yazımı ve üç gruplama yazımı aynı veriyle çalışır; veri 320 kayıt ve 100 ayrı anahtar taşır, bir anahtar bilerek daha sık geçer.
  • KL54 — Doğruluk sütunu, sonucun elle yazılan ilk yazımın sonucuyla karşılaştırılmasıdır; kâhin elle yazılan kalıptır.
  • KL55 — Gruplamada kutu bir nesnedir ve her kuruluşunda sayılır; sayılan şey kutular, içlerine konan kayıtlar değildir.
  • KL56 — Sayaç her yazımdan hemen önce sıfırlanır; verinin kurulması ölçüme girmez.
"""Sayma ve gruplama: elle yazilan kalip ile hazir karsiligi yan yana."""

import inspect
from collections import Counter, defaultdict

SAYAC = {"uretilen": 0}


class Oge:
    def __init__(self, deger):
        SAYAC["uretilen"] += 1
        self.deger = deger

    def __repr__(self):
        return f"Oge({self.deger})"


def sifirla():
    SAYAC["uretilen"] = 0


def uretilen():
    return SAYAC["uretilen"]


VERI = [f"a{i % 100:02d}" for i in range(300)] + ["a07"] * 20


def govde_satiri(f):
    """Islev govdesinin bos olmayan satir sayisi; tanim satiri sayilmaz."""
    satirlar = inspect.getsource(f).splitlines()[1:]
    return sum(1 for s in satirlar if s.strip())


def elle_say(veri):
    sayim = {}
    for k in veri:
        if k in sayim:
            sayim[k] += 1
        else:
            sayim[k] = 1
    return sayim


def get_ile_say(veri):
    sayim = {}
    for k in veri:
        sayim[k] = sayim.get(k, 0) + 1
    return sayim


def sayacla(veri):
    return Counter(veri)


def elle_grupla(veri):
    kutular = {}
    for i, k in enumerate(veri):
        if k not in kutular:
            kutular[k] = Oge([])
        kutular[k].deger.append(i)
    return kutular


def setdefault_ile_grupla(veri):
    kutular = {}
    for i, k in enumerate(veri):
        kutular.setdefault(k, Oge([])).deger.append(i)
    return kutular


def varsayilan_sozlukle_grupla(veri):
    kutular = defaultdict(lambda: Oge([]))
    for i, k in enumerate(veri):
        kutular[k].deger.append(i)
    return kutular


BEKLENEN_SAYIM = elle_say(VERI)
BEKLENEN_GRUP = {k: v.deger for k, v in elle_grupla(VERI).items()}

print(f"{'saymanın yazımı':<24s}{'gövde satırı':>13s}{'var edilen':>11s}"
      f"{'sonuç doğru':>12s}")
for ad, yontem in (("elle sınama", elle_say), ("get ile", get_ile_say),
                   ("sayaç", sayacla)):
    sifirla()
    sonuc = yontem(VERI)
    print(f"{ad:<24s}{govde_satiri(yontem):>13d}{uretilen():>11d}"
          f"{str(dict(sonuc) == BEKLENEN_SAYIM):>12s}")

print()
print(f"{'gruplamanın yazımı':<24s}{'gövde satırı':>13s}{'var edilen':>11s}"
      f"{'sonuç doğru':>12s}")
for ad, yontem in (("elle sınama", elle_grupla),
                   ("setdefault ile", setdefault_ile_grupla),
                   ("varsayılan sözlük", varsayilan_sozlukle_grupla)):
    sifirla()
    sonuc = yontem(VERI)
    dogru = {k: v.deger for k, v in sonuc.items()} == BEKLENEN_GRUP
    print(f"{ad:<24s}{govde_satiri(yontem):>13d}{uretilen():>11d}"
          f"{str(dogru):>12s}")

print()
print(f"veri {len(VERI)} kayıt | ayrı anahtar {len(BEKLENEN_SAYIM)} | "
      f"görülen sayılar {sorted(set(BEKLENEN_SAYIM.values()))}")
print(f"sayacın en çok geçen ikisi: {Counter(VERI).most_common(2)}")
saymanın yazımı          gövde satırı var edilen sonuç doğru
elle sınama                         7          0        True
get ile                             4          0        True
sayaç                               1          0        True

gruplamanın yazımı       gövde satırı var edilen sonuç doğru
elle sınama                         6        100        True
setdefault ile                      4        320        True
varsayılan sözlük                   4        100        True

veri 320 kayıt | ayrı anahtar 100 | görülen sayılar [3, 23]
sayacın en çok geçen ikisi: [('a07', 23), ('a00', 3)]

Satır Ucuz, Nesne Değil

Üst tablo yalnız satır tarafını ölçüyor ve tek yönlü. Elle sınamalı sayma 7 satır, get ile yazımı 4, sayaç (Counter) ise 1 satır. Üçü de doğru sonucu veriyor ve üçü de 0 nesne var ediyor — sayılar tam sayıdır, kap değildir. Kazanılan şey yalnız satırdır; ama satır da bir maliyettir, çünkü anahtarın ilk görülüşünü ayrı ele almak bir dal ve yanlış yazılabilecek bir yer daha demektir.

Sayacın verdiği fazladan şey son satırda görünüyor: en çok geçen anahtarları sıralı vermek. Elle yazımda bu, sözlüğü değere göre sıralayıp ilk ikisini almak demektir. Hazır kabın sunduğu şey yalnız kalıbın kendisi değil, etrafındaki işlerdir.

Alt tablo asıl ölçümdür ve iki sütun ters yönde çalışıyor. Elle sınamalı gruplama 6 satır ve 100 kutu var ediyor: her ayrı anahtar için bir kez. setdefault ile yazım 4 satıra iniyor — ama 320 kutu var ediyor. Fark yazımın kendisindedir: setdefault bir çağrıdır ve çağrının argümanı, sonuç kullanılacak mı diye bakılmadan, çağrıdan önce değerlendirilir. Anahtar zaten varsa var edilen kutu hiç kullanılmaz ve oracıkta atılır. Üç yüz yirmi kayıt için üç yüz yirmi kutu kurulur, iki yüz yirmisi boşuna.

Varsayılan sözlük (defaultdict) aynı 4 satırla 100 kutuya iniyor. Çünkü orada kutuyu kuran şey çağıran taraf değil, kabın kendisidir: anahtar gerçekten eksikse üretici çağrılır, değilse çağrılmaz. Aynı satır sayısı, üçte birinden az nesne.

Buradan çıkan kural yazıma değil, değerlendirme anına bakar: bir varsayılan değer argüman olarak yazıldığında her çağrıda var edilir, üretici olarak verildiğinde yalnız gerektiğinde. İki yazım aynı uzunlukta ve aynı sonuçtadır; ayrıldıkları yer bu kursun ölçtüğü eksendir.

Akışın Yalnız Sonunu Tutmak

İkinci ölçüm kabın tutma tarafına bakar. Bin öğelik bir akışta her öğe var edilir — bundan kaçış yoktur, çünkü öğeleri üreten şey kap değildir. Değişebilen tek şey, kabın bunların kaçını elde tuttuğudur. Ölçüme giren üçüncü kap çift uçlu kuyruktur (deque): iki ucundan da öğe alınıp verilebilen, kurulurken bir uzunluk sınırı verilebilen bir sıralı kap.

  • KL57 — Üç yazım da aynı bin öğelik akışı kurar; öğeler ölçüm sırasında var edilir ve sayaç her yazımdan önce sıfırlanır.
  • KL58 — Pencere beş öğedir ve son öğe her üç yazımda da yazdırılarak üçünün de akışı sonuna kadar tükettiği doğrulanır.
  • KL59 — Döndürme ölçümünde kurulan kap sayısı koddaki kap kuran ifadelerden okunur: iki dilim ve bir birleşim üç kap eder, yerinde döndürme hiç kap kurmaz. Kap sayısı ayrıca is ile doğrulanır.
  • KL60 — İki döndürme sonucu aynı sırada olup olmadığıyla karşılaştırılır; yazımların eşdeğerliği bu satırla gösterilir.
  • KL61 — Öğelerin paylaşımı, sonuçtaki her öğenin havuzda is ile aranmasıyla sınanır.
"""Cift uclu kuyruk: sinirli pencere ve dondurme, elle yazilan karsiligiyla."""

import inspect
from collections import deque

SAYAC = {"uretilen": 0}


class Oge:
    def __init__(self, deger):
        SAYAC["uretilen"] += 1
        self.deger = deger

    def __repr__(self):
        return f"Oge({self.deger})"


def sifirla():
    SAYAC["uretilen"] = 0


def uretilen():
    return SAYAC["uretilen"]


def govde_satiri(f):
    satirlar = inspect.getsource(f).splitlines()[1:]
    return sum(1 for s in satirlar if s.strip())


AKIS, PENCERE = 1000, 5


def listeyle_kirpmasiz():
    kap = []
    for i in range(AKIS):
        kap.append(Oge(i))
    return kap


def listeyle_kirparak():
    kap = []
    for i in range(AKIS):
        kap.append(Oge(i))
        if len(kap) > PENCERE:
            del kap[0]
    return kap


def kuyrukla():
    kap = deque(maxlen=PENCERE)
    for i in range(AKIS):
        kap.append(Oge(i))
    return kap


print(f"{'yazım':<24s}{'gövde satırı':>13s}{'var edilen':>11s}"
      f"{'tutulan':>8s}{'son öğe':>10s}")
for ad, yontem in (("liste, kırpmasız", listeyle_kirpmasiz),
                   ("liste, elle kırpma", listeyle_kirparak),
                   ("çift uçlu kuyruk", kuyrukla)):
    sifirla()
    kap = yontem()
    print(f"{ad:<24s}{govde_satiri(yontem):>13d}{uretilen():>11d}"
          f"{len(kap):>8d}{str(kap[-1]):>10s}")

print()
HAVUZ = [Oge(i) for i in range(5)]
K = 2

liste = list(HAVUZ)
sifirla()
sol, sag = liste[K:], liste[:K]
dondurulmus = sol + sag
print(f"listeyle döndürme: var edilen {uretilen()} | kurulan kap 3 "
      f"(iki dilim ve birleşim)")
print(f"  sonuç {dondurulmus} | dondurulmus is liste -> "
      f"{dondurulmus is liste}")

kuyruk = deque(HAVUZ)
once = kuyruk
sifirla()
kuyruk.rotate(-K)
print(f"kuyrukla döndürme: var edilen {uretilen()} | kurulan kap 0")
print(f"  sonuç {list(kuyruk)} | kuyruk is once -> {kuyruk is once}")
print(f"  iki sonuç aynı sırada -> {list(kuyruk) == dondurulmus}")
print(f"  öğeler paylaşılıyor -> "
      f"{all(any(x is y for y in HAVUZ) for x in kuyruk)}")
yazım                    gövde satırı var edilen tutulan   son öğe
liste, kırpmasız                    4       1000    1000  Oge(999)
liste, elle kırpma                  6       1000       5  Oge(999)
çift uçlu kuyruk                    4       1000       5  Oge(999)

listeyle döndürme: var edilen 0 | kurulan kap 3 (iki dilim ve birleşim)
  sonuç [Oge(2), Oge(3), Oge(4), Oge(0), Oge(1)] | dondurulmus is liste -> False
kuyrukla döndürme: var edilen 0 | kurulan kap 0
  sonuç [Oge(2), Oge(3), Oge(4), Oge(0), Oge(1)] | kuyruk is once -> True
  iki sonuç aynı sırada -> True
  öğeler paylaşılıyor -> True

Var Edilen Aynı, Tutulan Ayrı

Üç satırın “var edilen” sütunu aynı: 1000. Bu sütunun değişmemesi ölçümün en önemli bulgusudur. Kabı değiştirmek, üretilen nesne sayısını değiştirmez — öğeleri kap üretmiyor. Kabın seçtiği tek şey, üretilenlerin kaçının elde kalacağıdır.

“Tutulan” sütunu ayrılıyor: kırpmasız liste 1000 öğeyi tutuyor, elle kırpılan liste 5, çift uçlu kuyruk 5. Üçü de akışı sonuna kadar tüketiyor ve üçünün de son öğesi aynı. Yalnız son beş öğe gerekiyorsa kırpmasız liste dokuz yüz doksan beş nesneyi hiçbir gerekçe olmadan elde tutuyor demektir.

Satır sütunu ikinci farkı veriyor: elle kırpma 6 satır ister, kuyruk 4. Aradaki iki satır bir koşul ve bir silmedir. Elle yazımda pencere sınırı döngünün içinde her turda sınanan bir şeydir; kuyrukta kabın tanımının parçasıdır.

Alt blok döndürmeyi ölçüyor ve iki sonucun aynı sırada olduğunu doğruluyor. Listeyle döndürme 0 öğe var ediyor ama 3 kap kuruyor: sağ parçanın dilimi, sol parçanın dilimi ve ikisinin birleşimi. Sonuç yeni bir kaptır ve dondurulmus is liste False. Kuyrukla döndürme 0 öğe ve 0 kap kuruyor; sonuç aynı kaptır ve kuyruk is once True. Öğeler her iki yazımda da havuzdan geliyor.

Üç kap ile sıfır kap arasındaki fark küçük görünür, ama döndürme bir döngü içindeyse her tur üç kap kurar. Elle yazılan kalıp çoğu zaman doğru sonucu verir; ödettiği şey sonucun kendisi değil, sonuca giderken kurulan ara kaplardır.

Eksik Anahtar ve Korunan Sayı

İki özelleşmiş kap da bir sözlüktür ve ikisi de eksik anahtarda sözlükten ayrılır — ama birbirlerinden de ayrılırlar. Üçüncü ölçüm köşeli ayraçla erişimi üç kapta yineler ve erişimden sonra kaç anahtar kaldığına bakar.

  • KL62 — Üç kap da tek bir anahtarla, aynı değerle kurulur; ölçülen tek şey var olmayan bir anahtara erişimin sonucudur.
  • KL63 — Anahtar sayısı erişimden sonra okunur; erişimin kabı değiştirip değiştirmediği bu sayıyla görünür.
  • KL64 — Sayaç aritmetiği iki küçük sayaçla yapılır; sonuçlar anahtara göre sıralanarak basılır, çünkü ölçülen şey sıra değil, korunan sayıdır.
"""Eksik anahtarda uc kap uc ayri sey yapar; ve sayac 'kac kez'i koruyarak hesaplar."""

from collections import Counter, defaultdict

SOZLUK = {"a07": 3}
VARSAYILAN = defaultdict(int, {"a07": 3})
SAYAC = Counter({"a07": 3})

print(f"{'kap':<20s}{'d[yok] sonucu':<16s}{'sonra kaç anahtar'}")
for ad, kap in (("sözlük", SOZLUK), ("varsayılan sözlük", VARSAYILAN),
                ("sayaç", SAYAC)):
    try:
        sonuc = str(kap["yok"])
    except KeyError as e:
        sonuc = type(e).__name__
    print(f"{ad:<20s}{sonuc:<16s}{len(kap)}")

print()
SOL = Counter("aabbbcc")
SAG = Counter("abbdd")
print(f"sol sayaç {dict(sorted(SOL.items()))}")
print(f"sağ sayaç {dict(sorted(SAG.items()))}")
print(f"{'işlem':<22s}{'sonuç'}")
for ad, sonuc in (("sayaç kesişimi", SOL & SAG), ("sayaç birleşimi", SOL | SAG),
                  ("sayaç farkı", SOL - SAG)):
    print(f"{ad:<22s}{dict(sorted(sonuc.items()))}")
print(f"{'küme kesişimi':<22s}{sorted(set(SOL) & set(SAG))}")
print(f"{'küme birleşimi':<22s}{sorted(set(SOL) | set(SAG))}")

print()
print(f"sayaç bir sözlüktür -> {isinstance(SOL, dict)} | "
      f"toplam kayıt {sum(SOL.values())} | ayrı anahtar {len(SOL)}")
print(f"kesişimin toplamı {sum((SOL & SAG).values())} | "
      f"küme kesişiminin öğe sayısı {len(set(SOL) & set(SAG))}")
kap                 d[yok] sonucu   sonra kaç anahtar
sözlük              KeyError        1
varsayılan sözlük   0               2
sayaç               0               1

sol sayaç {'a': 2, 'b': 3, 'c': 2}
sağ sayaç {'a': 1, 'b': 2, 'd': 2}
işlem                 sonuç
sayaç kesişimi        {'a': 1, 'b': 2}
sayaç birleşimi       {'a': 2, 'b': 3, 'c': 2, 'd': 2}
sayaç farkı           {'a': 1, 'b': 1, 'c': 2}
küme kesişimi         ['a', 'b']
küme birleşimi        ['a', 'b', 'c', 'd']

sayaç bir sözlüktür -> True | toplam kayıt 7 | ayrı anahtar 3
kesişimin toplamı 3 | küme kesişiminin öğe sayısı 2

Üç kap üç ayrı şey yapıyor. Sözlük istisna veriyor ve kabı değiştirmiyor: anahtar sayısı 1’de kalıyor. Varsayılan sözlük 0 veriyor ve anahtar sayısı 2’ye çıkıyor — yani yalnız okuma niyetiyle yapılan bir erişim kaba kalıcı bir girdi eklemiş oluyor. Sayaç ise 0 veriyor ve anahtar sayısı 1’de kalıyor: eksik anahtara sıfır der, ama onu kaydetmez.

Bu ayrım gruplama ölçümünün doğrudan sonucudur. Eksik anahtarda üreticiyi çağırmak gruplamada aradığımız davranıştı; aynı davranış yalnız bakmak isteyen bir erişimde istenmeyen bir yan etkidir. Bir varsayılan sözlükte “bu anahtar var mı” diye köşeli ayraçla sorulmaz; üyelik sınamasıyla ya da get ile sorulur.

Alt blok önceki dersin kapanış sorusunu yanıtlıyor. Kümeye verilen veride “kaç kez” kaybolmuştu. Sayaç aynı hesapları yapıyor ve sayıyı koruyor: kesişim her anahtar için en küçük sayıyı, birleşim en büyük sayıyı, fark ise çıkarma sonucunu veriyor — sıfırın altına inen anahtar sonuçta hiç görünmüyor. Küme aynı iki veride yalnız anahtarları veriyor. Son satır farkı tek bir çiftle özetliyor: küme kesişiminde 2 öğe var, sayaç kesişiminin toplamı ise 3. Aynı iki anahtar, iki ayrı bilgi düzeyi.

Özet

  • Bir kalıbın iki ayrı fiyatı vardır — satır ve nesne — ve bunlar bağımsız değişebilir: setdefault ile yazım satırı 6’dan 4’e indirirken var edilen kabı 100’den 320’ye çıkarır.
  • Varsayılan bir argüman olarak yazıldığında her çağrıda var edilir; üretici olarak verildiğinde yalnız anahtar eksikse çağrılır — varsayılan sözlük aynı 4 satırla 100 kap kurar.
  • Sayma tarafında kazanç yalnız satırdadır: 7, 4 ve 1 satırlık üç yazım da aynı sonucu verir ve 0 nesne var eder.
  • Bin öğelik akışta üç yazım da 1000 nesne var eder; kap yalnız tutulanı belirler — kırpmasız listede 1000, elle kırpılan listede ve çift uçlu kuyrukta 5.
  • Döndürme listede 3 ara kap kurup yeni bir kap döndürür, kuyrukta 0 kap kurup aynı kabı yerinde çevirir; iki sonuç aynı sıradadır ve öğeler paylaşılır.
  • Eksik anahtarda sözlük KeyError verir, varsayılan sözlük 0 verip anahtarı ekler (1’den 2’ye), sayaç 0 verip eklemez; sayaç ayrıca kesişim ve farkı sayıları koruyarak hesaplar.

Sonraki Adım

Bu dersin döndürme ölçümünde iki dilim alındı ve sonuç yeni bir kap oldu. Dilim burada bir ayrıntı olarak geçti; oysa kendi başına bu kursun en çok yanlış anlaşılan işlemidir. Sonraki ders yalnız ona bakar ve tek bir soruyu sonuna kadar götürür: bir dilim alındığında ne kopyalanır? Kabın kendisi mi, içindekiler mi, yoksa ikisi de mi? Yanıt iç içe bir listede ölçülecek ve sayılar tek başına konuşacak.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat