İçeriğe geç
academia.sh

Ders 06 / 11

Veri Sınıfları

Dört kaynak satırlık kısa tanım elle yazılan on satırın yazdığı 4 özel yöntemin aynısını yazar; sıralama seçeneği 8'e, dondurma 6'ya, ikisi birlikte 10'a çıkarır ve dondurulan sınıfta beş işlemin 3'ü düşer.

İçindekiler

Önceki iki ders yazılan gövdeleri saydı: on bir sözdizim için on iki gövde, tek bir alanın denetimi için iki gövde daha. Oysa yazdığımız sınıfların çoğu aynı kalıbı tekrarlar. Birkaç alan taşırlar, kurulumda o alanları doldururlar, alanlarıyla karşılaştırılırlar ve alanlarıyla basılırlar. Bu kalıp her sınıfta elle yazıldığında hem uzar hem de her yazımda bir alanı unutma olasılığı taşır.

Standart kitaplık bu kalıp için kısa bir tanım biçimi sunar. Bu dersin sorusu kolaylık değil, ölçüdür: kısa tanım sınıfın sözlüğüne hangi özel yöntemleri gerçekten yazar, elle yazılmış karşılığıyla yan yana konduğunda hangileri eksik kalır, ve tanıma eklenen her seçenek sözlüğe kaç yeni ad koyar?

Kısa Tanım Ne Yazıyor

Beş sınıf aynı iki alanı taşır: bir dizgi ve bir tam sayı. ElleYazilan kurulum, temsil ve eşitlik gövdelerini elle yazar. Kalan dördü veri sınıfı (data class) tanımıdır: alanlar adlarıyla ve açıklamalarıyla bildirilir, gövdeler bildirilmez. Dört tanım birbirinden yalnız verilen seçeneklerle ayrılır — biri varsayılan, biri sıralama ister, biri dondurulmuştur, biri ikisini birden.

Ölçüm on özel yöntem adını her sınıfın kendi sözlüğünde arar. Atalardan gelen adlar sayıya girmez; sorulan şey kısa tanımın o sınıfa ne yazdığıdır.

  • SN62 — Beş sınıfın alanları aynıdır; adları, sıraları ve açıklamaları da aynıdır.
  • SN63 — “Kaynak satırı” satırı her tanımın kaç dolu kaynak satırı tuttuğunu sayar; boş satırlar sayılmaz, süsleyici satırı sayılır.
  • SN64 — Aranan on ad yalnız sınıfın kendi sözlüğünde aranır; saglayan kullanılmaz, çünkü ölçülen şey adın yazılıp yazılmadığıdır, nereden geldiği değil.
  • SN65 — “Karma değeri” satırı adın varlığına değil, değerinin boş olup olmadığına bakar; önceki dersin ölçtüğü boşaltma burada yeniden görünür.
"""Kisa tanim sinifin sozlugune hangi ozel yontemleri yaziyor."""

import inspect
from dataclasses import dataclass


class ElleYazilan:
    def __init__(self, ad, sayi):
        self.ad = ad
        self.sayi = sayi

    def __repr__(self):
        return f"ElleYazilan(ad={self.ad!r}, sayi={self.sayi!r})"

    def __eq__(self, o):
        if not isinstance(o, ElleYazilan):
            return NotImplemented
        return (self.ad, self.sayi) == (o.ad, o.sayi)


@dataclass
class Varsayilan:
    ad: str
    sayi: int


@dataclass(order=True)
class Sirali:
    ad: str
    sayi: int


@dataclass(frozen=True)
class Donmus:
    ad: str
    sayi: int


@dataclass(frozen=True, order=True)
class DonmusSirali:
    ad: str
    sayi: int


SINIFLAR = (ElleYazilan, Varsayilan, Sirali, Donmus, DonmusSirali)
ARANAN = ("__init__", "__repr__", "__eq__", "__hash__", "__lt__", "__le__",
          "__gt__", "__ge__", "__setattr__", "__delattr__")


def satir(sinif):
    """Tanimin kac dolu kaynak satiri tuttugu."""
    return sum(1 for s in inspect.getsource(sinif).splitlines() if s.strip())


print(f"{'özel yöntem':<13s}" + "".join(f"{s.__name__:>13s}" for s in SINIFLAR))
for ad in ARANAN:
    print(f"{ad:<13s}"
          + "".join(f"{('yazılı' if ad in s.__dict__ else '.'):>13s}"
                    for s in SINIFLAR))

print(f"{'toplam yazılı':<13s}"
      + "".join(f"{sum(1 for a in ARANAN if a in s.__dict__):>13d}"
                for s in SINIFLAR))
print(f"{'kaynak satırı':<13s}" + "".join(f"{satir(s):>13d}" for s in SINIFLAR))
print(f"{'karma değeri':<13s}"
      + "".join(f"{('yok' if s.__hash__ is None else 'var'):>13s}"
                for s in SINIFLAR))
özel yöntem    ElleYazilan   Varsayilan       Sirali       Donmus DonmusSirali
__init__            yazılı       yazılı       yazılı       yazılı       yazılı
__repr__            yazılı       yazılı       yazılı       yazılı       yazılı
__eq__              yazılı       yazılı       yazılı       yazılı       yazılı
__hash__            yazılı       yazılı       yazılı       yazılı       yazılı
__lt__                   .            .       yazılı            .       yazılı
__le__                   .            .       yazılı            .       yazılı
__gt__                   .            .       yazılı            .       yazılı
__ge__                   .            .       yazılı            .       yazılı
__setattr__              .            .            .       yazılı       yazılı
__delattr__              .            .            .       yazılı       yazılı
toplam yazılı            4            4            8            6           10
kaynak satırı           10            4            4            4            4
karma değeri           yok          yok          yok          var          var

İlk iki sütun karşılaştırmanın kendisidir. Elle yazılan tanım 10 kaynak satırında 4 ad yazıyor; kısa tanım 4 satırda aynı 4 adı yazıyor. Yazılan adlar birebir aynıdır — kısa tanım eksik bir şey bırakmıyor, fazladan bir şey de eklemiyor. Ölçünün asıl söylediği şudur: kısa tanımın kazandırdığı yalnız altı satır değildir. Elle yazılan sınıfta her alan adı üç gövdenin üçünde de yazılır — kurulumda, temsilde ve eşitlikte. Yeni bir alan eklendiğinde üç gövdenin üçü de güncellenmek zorundadır; kısa tanımda alan bir kez bildirilir. Unutulan bir güncellemeyle sessizce eksik karşılaştıran bir eşitlik gövdesi, bu kalıbın en pahalı hatasıdır ve hiçbir zaman istisna üretmez.

Dördüncü satır önceki dersin ölçtüğü düzeneği yeniden gösteriyor. Beş sınıfın beşinde de __hash__ adı sözlükte yazılıdır, ama alt satır ilk üçünde değerinin boş olduğunu söylüyor. Eşitliği yazan her sınıf karma yeteneğini kaybeder; kısa tanım da bu kuralın dışında değildir. Elle yazılan sınıf da aynı durumdadır ve aynı nedenle. Sonuç, veri sınıflarının en sık şaşırtan yanıdır: değerle karşılaştırılabilen bir nesne, dondurulmadıkça bir kümeye ya da sözlük anahtarına konamaz.

Seçenekler sözlüğe ne kattığını açıkça gösteriyor. Sıralama seçeneği 4 karşılaştırma yöntemi ekler ve toplamı 8’e çıkarır. Dondurma seçeneği yazma ve silme yollarını yakalayan 2 yöntem ekler, toplamı 6 yapar ve karma değerini geri getirir. İkisi birlikte verildiğinde toplam 10 olur — dört satırlık bir tanım, on gövdelik bir sınıf üretir.

Bu tablo kısa tanımın ne olduğunu da tanımlıyor: bir tanım değil, bir yazma işlemidir. Sınıf gövdesindeki alan bildirimleri okunur ve karşılık gelen gövdeler sınıfın sözlüğüne konur. Sözlüğe konan her ad, elle yazılabilecek olanın aynısıdır — bu yüzden kısa tanımla yazılmış bir sınıfta herhangi bir yöntemi elle yazmak da onu geçersiz kılar, çünkü gövdede zaten bulunan bir adın üzerine yazılmaz.

Yazılmayanları da saymak gerekir. Ölçüm on ad aradı ve dördüncü dersteki on bir sözdizimin çoğu bu listede yok: uzunluk, yineleme, üyelik, sıra erişimi ve bağlam sözdizimleri kısa tanımdan gelmez. Kısa tanım bir kayıt yazar, bir kap yazmaz. Bir veri sınıfının kap gibi davranması isteniyorsa gerekli gövdeler yine elle yazılır.

Alanların nasıl bulunduğu da bir noktayı açıkta bırakıyor. Kısa tanım hangi adların alan olduğunu, sınıf gövdesindeki açıklamalardan okur — ad: str yazımındaki tip adı alanın varlığını bildirir. Bu tip adının çalışma zamanında ne yaptığı ayrı bir sorudur ve bu kursun ikinci konusunda ölçülür; burada söylenebilecek olan, kısa tanımın onu yalnız bir işaret olarak kullandığıdır.

Değiştirilebilir Varsayılan

Kısa tanımın yalnız yazdıkları değil, reddettikleri de ölçülebilir. Python Temelleri kursu varsayılan değerin işlev tanımlanırken bir kez değerlendirildiğini ve değiştirilebilir bir varsayılanın çağrılar arasında paylaşıldığını ölçmüştü; o ölçüm tekrarlanmaz. Burada sorulan şey aynı yazımın iki tanım biçiminde ne ürettiğidir.

  • SN66 — Üç tanım aynı alanı aynı varsayılanla kurmayı dener; ilki elle yazılan sınıf, ötekiler kısa tanımdır.
  • SN67 — Kurulamayan tanım ünlem önekiyle istisnanın adını kaydeder ve o satırın ölçüm sütunları boş bırakılır.
  • SN68 — Kurulan her tanımdan üç örnek var edilir ve her biri kaba kendi sırasını ekler; paylaşım is ile gösterilir.
from dataclasses import field


class ElleKap:
    """Varsayilan deger bir kez degerlendirilir; uc ornek onu paylasir."""

    def __init__(self, ogeler=[]):
        self.ogeler = ogeler


def kisa_tanim(varsayilan):
    """Ayni varsayilani kisa tanimla kurmayi dener."""
    try:
        @dataclass
        class Gecici:
            ogeler: list = varsayilan
    except ValueError as e:
        return f"!{type(e).__name__}", None
    return "kuruldu", Gecici


TANIMLAR = (
    ("elle yazılan, varsayılan []", "kuruldu", ElleKap),
    ("kısa tanım, varsayılan []", *kisa_tanim([])),
    ("kısa tanım, üretici işlev", *kisa_tanim(field(default_factory=list))),
)

print(f"{'tanım':<28s} {'tanım anı':>16s} {'üç örneğin gördüğü':>20s} "
      f"{'kap paylaşılıyor':>17s}")
for ad, durum, sinif in TANIMLAR:
    if sinif is None:
        print(f"{ad:<28s} {durum:>16s} {'—':>20s} {'—':>17s}")
        continue
    ucu = [sinif() for _ in range(3)]
    for i, n in enumerate(ucu):
        n.ogeler.append(i)
    print(f"{ad:<28s} {durum:>16s} {str([len(n.ogeler) for n in ucu]):>20s} "
          f"{str(ucu[0].ogeler is ucu[2].ogeler):>17s}")
tanım                               tanım anı   üç örneğin gördüğü  kap paylaşılıyor
elle yazılan, varsayılan []           kuruldu            [3, 3, 3]              True
kısa tanım, varsayılan []         !ValueError                    —                 —
kısa tanım, üretici işlev             kuruldu            [1, 1, 1]             False

İlk satır bilinen sonucu veriyor: elle yazılan tanım kuruluyor, üç örnek tek kabı paylaşıyor ve her biri 3 öğe görüyor. Hata çalışma zamanında da ortaya çıkmıyor; program sessizce yanlış çalışıyor.

İkinci satır kısa tanımın farkıdır. Aynı yazım tanım anında ValueError ile düşüyor; sınıf hiç kurulamıyor ve hatanın ortaya çıkacağı yer, hatanın yazıldığı satırın kendisi oluyor. Üçüncü satır kabul edilen yazımı gösteriyor: varsayılan bir değer değil, her örnek için çağrılacak bir üretici işlev verildiğinde tanım kuruluyor ve üç örnek [1, 1, 1] görüyor — kap paylaşılmıyor.

Bu, ilk dersin sınıf gövdesindeki kap ölçümüyle aynı düzenektir ve üçüncü kez karşımıza çıkıyor. Kısa tanımın buradaki katkısı yeni bir kural getirmek değil, var olan kuralı erkene almaktır: bilinen bir yanlış yazımı çalışma zamanına bırakmak yerine tanım anında reddeder.

Reddin ne kadar dar olduğunu da yazmak gerekir. Kısa tanım yalnız bildiği birkaç değiştirilebilir tipi tanır; kendi yazdığımız değiştirilebilir bir sınıf varsayılan olarak verildiğinde tanım kurulur ve paylaşım ilk satırdaki sonucun aynısını verir. Yakalanan şey genel bir kural değil, sık yapılan bir yazımdır. Kuralın kendisi değişmemiştir: varsayılan değer tanım anında bir kez var edilir ve örneğe ait olması gereken her kap bir üretici işlevle bildirilir.

Dondurmanın Ölçüsü

Son seçenek nesnenin yazılabilirliğini kapatır. Değişmezliğin gerekçesi ve değer nesnesi kavramı Yazılım Tasarımı ve Mimari İlkeler müfredatında kuruldu; o tartışma tekrarlanmaz. Burada ölçülen şey tek bir seçeneğin kaç işlemi kapattığı ve karşılığında ne açtığıdır.

  • SN69 — İki sınıf aynı iki alanı taşır; tek farkları dondurulmuş olup olmamalarıdır.
  • SN70 — Beş işlem denenir ve her biri taze bir örnek üzerinde çalışır; işlemler birbirini etkilemez.
  • SN71 — Son satır aynı değerleri taşıyan iki ayrı nesneyi karşılaştırır; kimlik is ile gösterilir ve kimlik sayısı basılmaz.
from dataclasses import replace

ISLEMLER = (
    ("alana yazma", lambda n: setattr(n, "sayi", 9)),
    ("alan silme", lambda n: delattr(n, "sayi")),
    ("yeni alan açma", lambda n: setattr(n, "yeni", 1)),
    ("yerine yeni nesne", lambda n: replace(n, sayi=9)),
    ("kümeye koyma", lambda n: len({n})),
)


def dene(sinif, islev):
    try:
        islev(sinif("k", 1))
    except Exception as e:
        return f"!{type(e).__name__}"
    return "geçti"


print(f"{'işlem':<19s} {'Varsayilan':>21s} {'Donmus':>21s}")
for ad, islev in ISLEMLER:
    print(f"{ad:<19s} {dene(Varsayilan, islev):>21s} {dene(Donmus, islev):>21s}")

print()
for sinif in (Varsayilan, Donmus):
    dusen = sum(1 for _, f in ISLEMLER if dene(sinif, f).startswith("!"))
    print(f"{sinif.__name__}: beş işlemden düşen {dusen}, "
          f"geçen {len(ISLEMLER) - dusen}")

a, b = Donmus("k", 1), Donmus("k", 1)
print(f"ayrı iki nesne: a is b -> {a is b}, a == b -> {a == b}, "
      f"kümede kaç öğe -> {len({a, b})}")
işlem                          Varsayilan                Donmus
alana yazma                         geçti  !FrozenInstanceError
alan silme                          geçti  !FrozenInstanceError
yeni alan açma                      geçti  !FrozenInstanceError
yerine yeni nesne                   geçti                 geçti
kümeye koyma                   !TypeError                 geçti

Varsayilan: beş işlemden düşen 1, geçen 4
Donmus: beş işlemden düşen 3, geçen 2
ayrı iki nesne: a is b -> False, a == b -> True, kümede kaç öğe -> 1

Dondurma 3 işlemi kapatıyor ve 1 işlemi açıyor. Kapananların üçü de yazmadır: alana yazma, alan silme ve yeni bir alan açma. Üçüncüsü ayrıca dikkat çekicidir — üçüncü dersteki öznitelik kümesi bildirimiyle aynı kapıyı kapatır, ama farklı bir gerekçeyle. Açılan işlem kümeye koymadır: dondurulmuş nesne karma değerine sahip olduğu için bir kapta anahtar olabilir, sıradan veri sınıfı olamaz.

Dördüncü satır iki sınıfta da geçiyor ve dondurmanın nasıl kullanıldığını gösteriyor. Değişmez bir nesne değiştirilmez, yerine yenisi konur; standart kitaplığın verdiği işlev istenen alanları değiştirilmiş yeni bir nesne üretir. Yazma engellenmiştir, yeniden üretme engellenmemiştir.

Son satır dondurulmuş bir veri sınıfının ne olduğunu tanımlıyor: is False, == True, kümede 1 öğe. İki ayrı nesne, aynı değerleri taşıdıkları için birbirinin yerine geçer. Kimlikle değil değerle tanımlanmak, bu üç sonucun toplamıdır — ve bu dersteki tanımın ürettiği şey tam olarak budur.

Dondurmanın sınırını da yazmak gerekir, çünkü tablo onu göstermez. Kapatılan şey alana yazma yoludur; alanın gösterdiği nesnenin kendisi değil. Dondurulmuş bir nesnenin alanı bir liste tutuyorsa o listeye öğe eklemek hiçbir engelle karşılaşmaz — üçüncü dersteki ölçümün son satırı burada aynen geçerlidir. Değişmezlik bir katman derinliğindedir; nesnenin gerçekten değiştirilemez olması, alanlarının da değiştirilemez nesneler olmasına bağlıdır. Karma değeri de aynı sınırı taşır: dondurulmuş bir sınıf alanında bir liste taşıyorsa kümeye koyma satırı TypeError ile düşer, çünkü karma alanlardan üretilir.

Özet

  • Kısa tanım 4 kaynak satırında, elle yazılan 10 satırın yazdığı 4 özel yöntemin aynısını sınıfın sözlüğüne yazar; yazılan adlar birebir aynıdır.
  • Eşitliği yazan her sınıfta olduğu gibi kısa tanımda da __hash__ adı yazılır ve değeri boş bırakılır; karma yeteneği ancak dondurmayla geri gelir.
  • Seçenekler sözlüğe ad ekler: sıralama 4 karşılaştırma yöntemi ekleyip toplamı 8’e, dondurma 2 yöntem ekleyip 6’ya çıkarır, ikisi birlikte 10 yapar.
  • Değiştirilebilir varsayılan elle yazılan tanımda kurulur ve üç örnek tek kabı paylaşıp [3, 3, 3] görür; kısa tanımda tanım anında ValueError ile düşer ve üretici işlevle yazıldığında [1, 1, 1] görülür.
  • Dondurma beş işlemin 3’ünü kapatır ve 1’ini açar: üç yazma yolu düşer, kümeye koyma geçer, yerine yeni nesne koyma iki sınıfta da geçer.
  • Dondurulmuş iki ayrı nesne için is False, == True ve kümede 1 öğe kalır.

Sonraki Adım

Bu ders bir sınıfın ne yazdığını sözlüğüne bakarak saydı; önceki dersler de yanıtı hep sınıf ağacında aradı. İki yöntem de aynı varsayıma dayanıyor: bir nesnenin ne yapabildiğini bilmek için onun hangi sınıftan geldiğini bilmek gerekir. Oysa dördüncü ders bunun aksini göstermişti — katılım tipin adından değil, yazılan yöntemlerden geliyordu. Sarmal hiçbir şeyden türemez ama dört yeteneğin dördünü de karşılar. Sonraki ders bu ikiliği sınamaya çevirir: aynı nesne, soy bağı arayan bir sınamayla yöntem arayan bir sınamadan hangisini geçer, soyut bir tabanın bildirdiği sözleşme hangi anda uygulanır, ve yöntem arayan sınama gerçekten neye bakar?

İ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