İçeriğe geç
academia.sh

Ders 04 / 11

Özel Yöntemler

Hiçbir özel yöntem yazmayan sınıf on bir sözdizim biçiminin 5'ine katılır ama hiçbirini kendisi yanıtlamaz; `__eq__` yazmak `__hash__` adını boşaltır ve tek yöntem iki sözdizimi birden kapatır.

İçindekiler

Önceki üç ders sınıfları kendi yazdığımız yöntemlerle çağırdı: ekle, bicimle, kaynak. Oysa Python Temelleri kursu sözdizimin bir kısaltma olduğunu ölçmüştü — n + m bir yöntem çağrısıdır, x in n bir yöntem çağrısıdır, for bir protokoldür. O listeler tekrarlanmaz. Orada ölçülen taraf çağıran taraftı ve karşılayan taraf hep hazırdı: liste, dizgi, sözlük, üreteç. Katılan nesneleri dil vermişti.

Bu ders karşılayan tarafı bizim yazdığımız durumu ölçer. İki soru vardır. Birincisi: hiçbir özel yöntem yazmayan bir sınıf bu sözdizimlerin kaçına katılabilir, ve katıldıklarında yanıtı kim veriyor? İkincisi: yazdığımız her yöntem bir sözdizim açar da, hiç kapatır mı?

Yazmayan Sınıf Ne Yapabiliyor

On bir sözdizim biçimi seçilir; her biri belli bir özel yöntem adını arar. İki sınıf aynı veriyi taşır. Ciplak hiçbir özel yöntem yazmaz. Yazili on bir yöntemin on birini de yazar. Ölçüm iki şeyi ayrı ayrı sorar: sözdizim çalıştı mı, ve o adı hangi sınıf sağlıyor. İki sorunun ayrı sorulması gerekir, çünkü yanıtları her satırda aynı çıkmaz — bir sözdizim çalışmadan da adı bulunmuş olabilir, bulunmadan da çalışmış olabilir.

  • SN42 — İki sınıf aynı üç öğeyi taşır ve __init__ gövdeleri birbirinin aynısıdır; aralarındaki tek fark yazılan özel yöntemlerdir. Yazili Ciplak’tan türemez.
  • SN43 — Her biçim iki taze örnek üzerinde çalıştırılır; ölçüm sonucun değerine değil, çağrının karşılanıp karşılanmadığına bakar. Karşılanmayan çağrı TypeError ile düşer.
  • SN44 — “Yanıtlayan” sütunu adı çözümleme sırasında ilk taşıyan sınıfı verir; hiçbir sınıf taşımıyorsa None yazılır.
  • SN45 — Hiçbir sözdizimin sonucu basılmaz. Ciplak bir temsil yöntemi yazmadığı için varsayılan temsili ortama bağlı veri taşır; ölçüm bu yüzden yalnız durum ve yanıtlayan sınıf basar.
"""Sozdizime katilan tarafi biz yaziyoruz: yazmayan ve yazan iki sinif."""


def saglayan(sinif, ad):
    for s in sinif.__mro__:
        if ad in s.__dict__:
            return s.__name__
    return None


class Ciplak:
    """Hicbir ozel yontem yazmaz; yalnizca ogelerini tasir."""

    def __init__(self, ogeler=(1, 2, 3)):
        self.ogeler = list(ogeler)


class Yazili:
    """Ayni veriyi tasir, katildigi her sozdizim icin bir yontem yazar."""

    def __init__(self, ogeler=(1, 2, 3)):
        self.ogeler = list(ogeler)

    def __repr__(self):
        return f"Yazili({self.ogeler})"

    def __str__(self):
        return "-".join(str(x) for x in self.ogeler)

    def __len__(self):
        return len(self.ogeler)

    def __eq__(self, o):
        return isinstance(o, Yazili) and self.ogeler == o.ogeler

    def __lt__(self, o):
        return len(self.ogeler) < len(o.ogeler)

    def __bool__(self):
        return bool(self.ogeler)

    def __contains__(self, x):
        return x in self.ogeler

    def __iter__(self):
        return iter(self.ogeler)

    def __enter__(self):
        return self

    def __exit__(self, *k):
        return False

    def __getitem__(self, i):
        return self.ogeler[i]

    def __hash__(self):
        return hash(tuple(self.ogeler))


def girdi(n):
    with n:
        return True


BICIMLER = (
    ("repr(n)", "__repr__", lambda n, m: repr(n)),
    ("str(n)", "__str__", lambda n, m: str(n)),
    ("len(n)", "__len__", lambda n, m: len(n)),
    ("n == m", "__eq__", lambda n, m: n == m),
    ("n < m", "__lt__", lambda n, m: n < m),
    ("bool(n)", "__bool__", lambda n, m: bool(n)),
    ("2 in n", "__contains__", lambda n, m: 2 in n),
    ("for x in n", "__iter__", lambda n, m: [x for x in n]),
    ("with n", "__enter__", lambda n, m: girdi(n)),
    ("n[0]", "__getitem__", lambda n, m: n[0]),
    ("hash(n)", "__hash__", lambda n, m: hash(n)),
)


def olc(sinif, islev):
    try:
        islev(sinif(), sinif())
    except TypeError:
        return "düştü"
    return "çalıştı"


print(f"{'sözdizim':<12s} {'özel yöntem':<14s} {'Ciplak':>8s} "
      f"{'yanıtlayan':>11s} {'Yazili':>8s} {'yanıtlayan':>11s}")
for ad, yontem, islev in BICIMLER:
    print(f"{ad:<12s} {yontem:<14s} {olc(Ciplak, islev):>8s} "
          f"{str(saglayan(Ciplak, yontem)):>11s} {olc(Yazili, islev):>8s} "
          f"{str(saglayan(Yazili, yontem)):>11s}")

for sinif in (Ciplak, Yazili):
    calisan = sum(1 for _, _, f in BICIMLER if olc(sinif, f) == "çalıştı")
    kendi = sum(1 for _, y, _ in BICIMLER if saglayan(sinif, y) == sinif.__name__)
    print(f"\n{sinif.__name__}: çalışan {calisan}/{len(BICIMLER)}, "
          f"yanıtı kendi veren {kendi}/{len(BICIMLER)}")
sözdizim     özel yöntem      Ciplak  yanıtlayan   Yazili  yanıtlayan
repr(n)      __repr__        çalıştı      object  çalıştı      Yazili
str(n)       __str__         çalıştı      object  çalıştı      Yazili
len(n)       __len__           düştü        None  çalıştı      Yazili
n == m       __eq__          çalıştı      object  çalıştı      Yazili
n < m        __lt__            düştü      object  çalıştı      Yazili
bool(n)      __bool__        çalıştı        None  çalıştı      Yazili
2 in n       __contains__      düştü        None  çalıştı      Yazili
for x in n   __iter__          düştü        None  çalıştı      Yazili
with n       __enter__         düştü        None  çalıştı      Yazili
n[0]         __getitem__       düştü        None  çalıştı      Yazili
hash(n)      __hash__        çalıştı      object  çalıştı      Yazili

Ciplak: çalışan 5/11, yanıtı kendi veren 0/11

Yazili: çalışan 11/11, yanıtı kendi veren 11/11

Ciplak on bir biçimin 5’ine katılıyor ve yanıtı kendi veren biçim sayısı 0. Yazmamak sözdizimi kapatmaz; sessiz bir varsayılana bırakır. Çalışan beş satırın dördünde yanıtı bütün sınıfların ortak tabanı veriyor, bool(n) satırında ise hiçbir sınıf vermiyor — orada sözdizim bir yöntem bulamadığında düşmek yerine “her nesne doğrudur” kuralına geçiyor.

Varsayılanların ne söylediği önemlidir, çünkü hepsi kimliğe dayanır. Ortak tabanın verdiği eşitlik iki ayrı nesneyi eşit saymaz; verdiği karma değeri de kimlikten üretilir. Ciplak öğeleriyle karşılaştırılmak ya da öğeleriyle anahtar olmak isterse bu varsayılanlar yanlış yanıt verir — hata vermezler, yanlış yanıt verirler. Bir hatanın en pahalı biçimi budur: aynı içeriği taşıyan iki Ciplak nesnesi bir kümeye konduğunda iki ayrı öğe sayılır ve program hiçbir uyarı vermeden çalışmaya devam eder.

n < m satırı ayrı bir okumadır. Yanıtlayan sütunu boş değil, ortak tabanı gösteriyor; ad bulundu. Buna rağmen sözdizim düştü. Ortak taban bu adı tanımlar ama gövdesi “bu işi bilmiyorum” der ve karşılaştırma karşılıksız kalır. Adın bulunması yanıtın verildiği anlamına gelmez. Bir yeteneği aramanın doğru yolu adın varlığına bakmak değil, çağrının karşılanıp karşılanmadığına bakmaktır.

Yazili on bir biçimin on birine katılıyor ve on birinin de yanıtını kendisi veriyor. Bir sınıfın “dilin nesnelerine benzemesi” ayrı bir yetenek değildir; yazılan yöntemlerdir. Katılım tipin adından ya da tabanından değil, sınıfın sözlüğüne konan adlardan gelir.

Sayının bir ayrıntısı tabloda görünmüyor. Yazili on bir sözdizim için on iki gövde yazar: bağlam sözdizimi tek satırdır ama girişi ve çıkışı ayrı iki yöntemle karşılanır, ve tablo yalnız girişi listeler. Sözdizim ile yöntem arasındaki eşleme birebir değildir; bir sözdizim birden çok gövde isteyebilir, bool(n) satırında görüldüğü gibi hiç gövde istemeyebilir de.

Yazılan yöntemlerin bazıları kendilerine bitişik sözdizimleri de açar. Eşitliği yazan sınıf eşitsizliği ayrıca yazmaz; küçüklüğü yazan sınıf, aynı tipten iki nesne arasında büyüklük karşılaştırmasını da karşılar, çünkü dil işlemi ters çevirip öteki işlenene sorar. Bunlar Python Temelleri kursunda çağıran taraftan ölçülmüştü; burada aynı düşüşlerin yazan taraftaki karşılığı görülüyor — bir gövde yazmak, o gövdeye düşen bütün yazımları birden karşılamaktır.

Bir Yöntem Yazmak Bir Sözdizimi Kapatabilir

Yazılan yöntemler yalnız kapı açmaz. Eşitlikle karma değeri arasında dilin sürdürdüğü bir sözleşme vardır: eşit sayılan iki nesnenin karma değeri de eşit olmalıdır. Bir sınıf eşitliği kendisi tanımladığında bu sözleşme kendiliğinden sağlanamaz hâle gelir ve dil kararı sınıfa bırakmaz — karma yeteneğini kaldırır.

  • SN46 — Üç sınıf aynı tek alanı taşır; farkları yalnız hangi yöntemi yazdıklarıdır. Üçüncü sınıf ikincisinden türer ve yalnız karma yöntemini ekler.
  • SN47 — Karma sınaması değerin kendisini basmaz, yalnız çağrının karşılanıp karşılanmadığını bildirir; karma değeri ortama bağlı olabilir.
  • SN48 — Son iki sütun __hash__ adının sınıfın kendi sözlüğünde bulunup bulunmadığını ve değerinin boş olup olmadığını ayrı ayrı gösterir.
class Yalin:
    """Ne esitlik ne karma yazar."""

    def __init__(self, deger=1):
        self.deger = deger


class Esit:
    """Yalniz esitligi yazar."""

    def __init__(self, deger=1):
        self.deger = deger

    def __eq__(self, o):
        return isinstance(o, Esit) and self.deger == o.deger


class EsitVeKarma(Esit):
    """Esitligi devralir, karmayi kendisi yazar."""

    def __hash__(self):
        return hash(self.deger)


def dene(islev):
    try:
        return str(islev())
    except TypeError as e:
        return f"!{type(e).__name__}"


print(f"{'sınıf':<13s} {'a == b':>8s} {'hash(a)':>16s} {'anahtar olma':>16s} "
      f"{'__hash__ yazılı':>16s} {'değeri boş':>11s}")
for sinif in (Yalin, Esit, EsitVeKarma):
    a, b = sinif(), sinif()
    print(f"{sinif.__name__:<13s} {dene(lambda: a == b):>8s} "
          f"{dene(lambda: hash(a) is not None):>16s} "
          f"{dene(lambda: len({a: 1})):>16s} "
          f"{str('__hash__' in sinif.__dict__):>16s} "
          f"{str(sinif.__hash__ is None):>11s}")
sınıf           a == b          hash(a)     anahtar olma  __hash__ yazılı  değeri boş
Yalin            False             True                1            False       False
Esit              True       !TypeError       !TypeError             True        True
EsitVeKarma       True             True                1             True       False

İlk satır varsayılan durumu gösteriyor: iki ayrı nesne eşit değil, ama karma değeri var ve nesne bir kapta anahtar olabiliyor. İkinci satırda tek bir yöntem yazıldı ve iki sözdizim birden kapandı — hem hash(a) hem anahtar olma düştü. Yazılan yöntem __eq__’dı; kapanan sözdizimlerin ikisi de __hash__’e bağlıydı.

Son iki sütun bunun nasıl olduğunu söylüyor. Esit sınıfının kendi sözlüğüne __hash__ adı yazılmıştır ve değeri boştur. Dil, eşitliği yazan sınıfa sessizce bir giriş ekler ve o girişi boş bırakır; arama artık ortak tabana ulaşamaz, ilk adımda boş bir değere çarpar. Bu bir yan etki değil, bilinçli bir karardır: eşitliğini değiştirmiş bir nesnenin eski karma değerini kullanması, onu bir kapta kaybolmuş bir anahtar hâline getirirdi.

Üçüncü satır çözümü gösteriyor. EsitVeKarma eşitliği devralır ve karmayı kendisi yazar; giriş artık boş değildir ve iki sözdizim de geri açılır. Kural şudur: eşitliği yazan sınıf karmayı da yazar — ya da yazmayarak nesnenin anahtar olamayacağını kabul eder. Karma yazılırken eşitlikte kullanılan aynı alanların kullanılması gerekir, yoksa sözleşme yine bozulur.

Sözleşmenin ikinci yarısı da yazan tarafın sorumluluğundadır: karma değeri nesnenin ömrü boyunca değişmemelidir. Eşitliği değiştirilebilir bir alandan üreten bir sınıf, o alanı bir kapta anahtarken değiştirdiğinde nesneyi kendi kabında bulunamaz hâle getirir. Bu yüzden karmalanabilir olması istenen sınıfların eşitliği değiştirilmeyen alanlardan üretmesi beklenir. Yazili’nin ilk ölçümdeki karma gövdesinin öğeleri bir demete çevirmesi de bu nedenledir: değiştirilebilir bir kabın kendisi karmalanamaz.

İki Temsil

Kalan ayrım iki temsil yöntemi arasındadır. İkisi de nesneden bir dizgi üretir ama hedefleri ayrıdır: biri çözümleyici için nesneyi tanıtır, öteki okuyucu için nesneyi sunar. Hangisinin çağrıldığını kullanım yeri belirler.

  • SN49 — İki sınıf aynı iki dizgiyi döndürür; farkları yalnız birinin metin yöntemini yazmamasıdır.
  • SN50 — Altı kullanım biçimi denenir ve her biri taze bir örnek üzerinde çalışır; ölçüm dönen dizginin hangisi olduğuna bakar.
  • SN51 — Son satırlar altı biçimden kaçının metin yöntemine gittiğini sayar; kap içindeki basım da bu sayıya dahildir.
class YalnizTemsil:
    """Yalniz __repr__ yazar."""

    def __repr__(self):
        return "temsil"


class IkiTemsil:
    """Ikisini de yazar."""

    def __repr__(self):
        return "temsil"

    def __str__(self):
        return "metin"


KULLANIM = (
    ("str(n)", lambda n: str(n)),
    ("repr(n)", lambda n: repr(n)),
    ("f-dizgide n", lambda n: f"{n}"),
    ("f-dizgide n!r", lambda n: f"{n!r}"),
    ("[n] basımı", lambda n: str([n])),
    ("biçimlendirme", lambda n: "{}".format(n)),
)

print(f"{'yazım':<15s} {'yalnız __repr__ yazan':>22s} {'ikisini de yazan':>18s}")
for ad, islev in KULLANIM:
    print(f"{ad:<15s} {islev(YalnizTemsil()):>22s} {islev(IkiTemsil()):>18s}")

for sinif in (YalnizTemsil, IkiTemsil):
    metin = sum(1 for _, f in KULLANIM if "metin" in f(sinif()))
    print(f"\n{sinif.__name__}: __str__ sonucunu veren yazım {metin}/{len(KULLANIM)}")
yazım            yalnız __repr__ yazan   ikisini de yazan
str(n)                          temsil              metin
repr(n)                         temsil             temsil
f-dizgide n                     temsil              metin
f-dizgide n!r                   temsil             temsil
[n] basımı                    [temsil]           [temsil]
biçimlendirme                   temsil              metin

YalnizTemsil: __str__ sonucunu veren yazım 0/6

IkiTemsil: __str__ sonucunu veren yazım 3/6

Sol sütunda altı yazımın altısı da aynı sonucu veriyor ve metin yöntemine giden yazım 0. Metin yöntemi yazılmadığında istek karşılıksız kalmaz, temsil yöntemine düşer. Bu düşüşün yönü tek taraflıdır: temsil yöntemi yazılmadığında metin yöntemine düşüş yoktur. İki yöntemden yalnız biri yazılacaksa yazılacak olan temsildir.

Sağ sütunda altı yazımın 3’ü metin yöntemine gidiyor. Ayrım açık: dizgiye çevirme ve biçimlendirme okuyucuya sunar, açık istek ve dönüştürme işareti çözümleyiciye tanıtır. Beşinci satır en çok şaşırtan yerdir — nesne bir kabın içindeyken her zaman temsil yöntemiyle basılır, kap dışarıdan metne çevrilmiş olsa bile. Kap kendi metnini üretirken içindeki her öğeden temsil ister, çünkü çözümleyicinin göreceği şey öğenin sunumu değil, kimliğidir.

Buradan yazım kuralı çıkar: temsil yöntemi nesneyi yeniden kurabilecek kadar bilgi taşımalıdır; metin yöntemi ise okunabilir olmalıdır. Yazili sınıfının ilk ölçümdeki iki gövdesi tam olarak bu ayrımı izler: temsil gövdesi sınıfın adını ve öğelerini birlikte verir, metin gövdesi yalnız öğeleri okunabilir biçimde dizer.

Bu ayrımın hata ayıklamadaki karşılığı doğrudan ölçülebilir bir kolaylıktır. Bir kapta duran yüz nesnenin dökümü alındığında görülecek olan şey, o sınıfın temsil gövdesidir; temsil yazılmamışsa döküm yüz satır boyunca hiçbir şey söylemez. Yazılan tek bir gövde, nesnenin göründüğü bütün kaplarda okunabilirlik kazandırır.

Özet

  • Hiçbir özel yöntem yazmayan sınıf on bir sözdizimin 5’ine katılır ve 0’ının yanıtını kendisi verir; yazmamak sözdizimi kapatmaz, sessiz bir varsayılana bırakır ve varsayılanların hepsi kimliğe dayanır.
  • Ad bulunmuş olması yanıtın verildiği anlamına gelmez: n < m için ortak tabanda bir ad vardır ama sözdizim yine de düşer.
  • On bir yöntemi yazan sınıf on bir sözdizimin tamamına katılır ve tamamının yanıtını kendisi verir; katılım tipin adından değil, yazılan yöntemlerden gelir.
  • Bir yöntem yazmak bir sözdizimi kapatabilir: __eq__ yazan sınıfın sözlüğüne __hash__ adı boş olarak eklenir ve karma ile anahtar olma birlikte düşer. Eşitliği yazan sınıf karmayı da yazar.
  • Metin yöntemi yazılmadığında istek temsil yöntemine düşer, tersi olmaz; ikisini de yazan sınıfta altı yazımın 3’ü metne gider ve kap içindeki basım her zaman temsili kullanır.

Sonraki Adım

Bu derste yazılan her yöntem bir çağrıyla tetiklendi: len(n) yazıldığında bir gövde koştu, n[0] yazıldığında bir gövde koştu. Öznitelik okumak ise hiçbir gövdeyi çalıştırmıyordu — n.ogeler bir sözlük aramasıydı ve araya kimse girmiyordu. Üçüncü dersin bıraktığı açık tam buradaydı: değişmezi koruyan gövdeye uğramayan yazma denetimsiz kalıyordu. Sonraki ders erişimin kendisini araya bir gövde koyarak yakalamayı ölçer: aynı sayıda okuma ve yazma, doğrudan alanla kaç gövde çalıştırır, araya giren bir tanımla kaç gövde çalıştırır, ve araya giren kod verisini nerede tutarsa üç örnek tek değeri paylaşmaya başlar?

İ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