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.YaziliCiplak’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ı
TypeErrorile 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
Noneyazılır. - SN45 — Hiçbir sözdizimin sonucu basılmaz.
Ciplakbir 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 < miç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.