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
isile 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
isile 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:
setdefaultile 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
KeyErrorverir, 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.