Ders 11 / 16
Yerleşik Fonksiyonlar
Yerleşikler iş yapan işlevler değil, protokol çağıran ince kabuklardır: sekiz yerleşik yirmi bir çağrı üretir, toplayanların hepsi aynı beşli diziyi kurar ve protokolü tanımlamayan bir nesnede üçü değer verirken beşi TypeError fırlatır.
İçindekiler
Önceki ders ad aramasının dört düzeyini ölçtü ve son basamaktaki adları yalnız gölgelenecek bir şey olarak gösterdi. O düzeyde duran adlar dilin en çok kullanılanlarıdır ve şimdiye kadar ölçülen her protokole doğrudan bağlanırlar.
Bu dersin sorusu, o adların ne yaptığı değil — kendilerinin bir iş yapıp yapmadığıdır.
len uzunluğu kendisi mi hesaplıyor, yoksa nesneye mi soruyor? sorted sıralamayı kendisi
mi biliyor, yoksa karşılaştırmayı nesneye mi bırakıyor? Kursun ölçü ekseni bu soruya bir
yanıt öneriyor: sözdizim bir kısaltmaysa, yerleşikler de birer kısaltma olabilir. Ölçüm
bunu sınar.
Yerleşik Bir Kabuktur
Buraya kadarki dört ders sözdizim biçimlerini ölçtü: if, for, while, çağrı yazımı.
Yerleşikler sözdizim değildir; sıradan işlevlerdir ve çıplak bir adla çağrılırlar. Ama
ölçü ekseni ikisi için de aynıdır — bir yerleşiğin sayısı da, hangi özel yöntemi kaç kez
çağırdığıdır.
Sözleşme şudur: yerleşik, işi kendisi yapmaz. Nesnede adı belli bir özel yöntemi arar,
bulursa çağırır ve döndürdüğünü verir. len uzunluğu saymaz, __len__’e sorar; str
metni kurmaz, __str__’e sorar; iter bir yineleyici üretmez, __iter__’den ister.
Yerleşiğin katkısı iş değil, tek bir çağrı adı: nesnenin tipi ne olursa olsun aynı ad
kullanılır.
Bunun sonucu, yerleşiklerin tipe göre değil protokole göre çalışmasıdır. Bir nesnenin
len ile ölçülüp ölçülemeyeceğini belirleyen şey sınıfının adı değil, __len__
tanımlayıp tanımlamadığıdır.
Bu tasarımın pratik karşılığı, yeni bir sınıfın yerleşiklerle çalışmak için hiçbir şey devralmak ya da hiçbir yere kaydolmak zorunda olmamasıdır. Sınıf yalnız beklenen adı taşıyan bir yöntem tanımlar; yerleşik onu bulur. Ortak tanımın dördüncü okuması burada ikinci kez ödenir — bir nesnenin ne yapabildiğini tipinin adı değil, tanımladığı özel yöntemler belirler. Yerleşiğin nesne hakkında bildiği tek şey, aradığı yöntemin orada olup olmadığıdır.
Toplayan Yerleşikler
İkinci bir öbek, tek bir yöntem çağırmakla yetinmez. list, sum ve sorted nesnenin
bütün öğelerini ister; bunun için yineleme protokolünü baştan sona yürütürler.
Beklenen dizi ikinci dersin ölçtüğü diziyle aynı olmalıdır: bir __iter__, ardından öğe
sayısından bir fazla __next__.
Bu öbeğin bir sonucu, öğeleri toplamanın her zaman öğe sayısı kadar iş olmasıdır:
yerleşik kaç öğe olduğunu baştan bilmez ve bilemez, çünkü yineleme protokolü bir sayı
bildirmez. Toplama ancak StopIteration geldiğinde biter — yani öğeler tükenene kadar
sürer.
sorted bu öbek içinde ayrı durur, çünkü topladıktan sonra bir de sıralaması gerekir.
Toplamak bir protokol, sıralamak başka bir protokoldür; ikincisi öğeler arasında
karşılaştırma yapar. Bir yerleşiğin tek bir özel yöntemle eşleştiği varsayımı burada
sınanır.
Protokol Yoksa
Üçüncü ölçü, yöntemin bulunamadığı durumdur. Ortak tanımın dördüncü okuması bunu söylüyordu: protokol yoksa sözdizim de yoktur. Yerleşikler için de geçerli mi?
Yanıtın tek olmadığı baştan bellidir, çünkü birinci derste ölçülen bir durum var: doğruluk sınaması, hiçbir yöntem bulamadığında hata vermiyor, nesneyi doğru sayıyordu. Demek ki en az bir yerleşiğin varsayılan davranışı var. Kaçının var, kaçının yok — ölçüm bunu ayırır.
Ölçümün varsayımları:
- PA31 — Kâhin kurgunun kendisidir: her özel yöntem çağrıldığında adını kaydeder ve yerleşiğin hangi yöntemi çağırdığı bu kayıttan okunur.
- PA32 — İki sınıf kullanılır.
Akisyalnız yineleme ve karşılaştırma yöntemlerini tanımlar;Izciona uzunluk ve metin yöntemlerini ekler. Toplayan yerleşiklerAkisüzerinde ölçülür — böylece ölçülen dizi yalnız yineleme protokolünden gelir. - PA33 —
Sessizhiçbir özel yöntem tanımlamaz ve “protokol yok” durumunun ölçüsüdür. - PA34 — Sonuçlar değer olarak basılmaz, yalnız “değer üretti” ya da istisnanın sınıfı yazılır. Yöntem tanımlamayan bir nesnenin metin karşılığı ortama bağlı bilgi taşır; ölçüme girmez.
- PA35 —
Izci.__repr__gövdesinde__str__’i çağırır; bu, ortak tanımdaki davranıştır ve iki çağrının nedeni budur. - PA36 — Sıralama ölçümünde çağrı sayısı değil, çağrılan protokollerin kümesi yazılır. Karşılaştırma sayısı sıralama düzeninin bir ayrıntısıdır ve ölçünün parçası değildir.
- PA37 — Toplam çağrı sayısı yalnız bu sekiz yerleşiğe ve üç öğelik nesneye özgüdür.
- PA38 — Anahtar işlevi ölçümünde
keyolaraklenyerleşiği verilir; böylece anahtarın çağrıldığı her yer kayda düşer ve öğe başına kaç kez çağrıldığı sayılabilir.
Ölçüm
"""Yerlesiklerin sozlesmesi: her biri hangi ozel yontemi cagirir.""" KAYIT = [] def kaydet(ad): KAYIT.append(ad) class Akis: """Yalniz yineleme ve siralama protokolu; __len__ tanimlamaz.""" def __init__(self, ogeler=(1, 2, 3)): self.ogeler = list(ogeler) def __iter__(self): kaydet("__iter__") self._i = 0 return self def __next__(self): kaydet("__next__") if self._i >= len(self.ogeler): raise StopIteration deger = self.ogeler[self._i] self._i += 1 return deger def __lt__(self, o): kaydet("__lt__") return len(self.ogeler) < len(o.ogeler) class Izci(Akis): """Uzunluk ve metin protokollerini de tanimlar.""" def __len__(self): kaydet("__len__") return len(self.ogeler) def __str__(self): kaydet("__str__") return f"Izci{tuple(self.ogeler)}" def __repr__(self): kaydet("__repr__") return self.__str__() class Bosluksuz(Izci): """__bool__ tanimli: dogruluk sinamasi __len__'e dusmez.""" def __bool__(self): kaydet("__bool__") return len(self.ogeler) > 0 class Sessiz: """Hicbir ozel yontem tanimlamaz.""" def olc(islev): KAYIT.clear() try: islev() durum = "değer üretti" except Exception as e: durum = f"!{type(e).__name__}" return list(KAYIT), durum TEKIL = (("len(n)", len, Izci), ("str(n)", str, Izci), ("repr(n)", repr, Izci), ("bool(n)", bool, Izci), ("iter(n)", iter, Izci)) TOPLAYAN = (("list(n)", list, Akis), ("sum(n)", sum, Akis), ("sorted(n)", sorted, Akis)) print(f"{'yerleşik':<12s} {'çağrı':>6s} çağrılan protokol dizisi") toplam = 0 for ad, f, sinif in TEKIL + TOPLAYAN: c, _ = olc(lambda f=f, s=sinif: f(s())) toplam += len(c) print(f" {ad:<10s} {len(c):6d} {' '.join(c)}") print(f"\nsekiz yerleşik, toplam {toplam} çağrı") print() print(f"{'yerleşik':<12s} {'protokolü tanımlayan nesne':<27s} tanımlamayan nesne") for ad, f, sinif in TEKIL + TOPLAYAN: _, d1 = olc(lambda f=f, s=sinif: f(s())) _, d2 = olc(lambda f=f: f(Sessiz())) print(f" {ad:<10s} {d1:<27s} {d2}") print() c, _ = olc(lambda: bool(Izci())) print(f"bool(__bool__ tanımsız) -> {' '.join(c)}") c, _ = olc(lambda: bool(Bosluksuz())) print(f"bool(__bool__ tanımlı) -> {' '.join(c)}") print() c, _ = olc(lambda: sorted(Akis())) print(f"sorted(tek nesne, öğeleri sayı) -> {' '.join(sorted(set(c)))}") UCLU = [Akis((1, 2, 3)), Akis((1,)), Akis((1, 2))] c, _ = olc(lambda: sorted(UCLU)) print(f"sorted(üç nesnelik liste) -> {' '.join(sorted(set(c)))}") print() UCLU_IZCI = [Izci((1, 2, 3)), Izci((1,)), Izci((1, 2))] print(f"{'üç nesnelik liste':<28s} {'çağrılan protokol':<20s} karşılaştırma nesneye sorulur mu") for ad, f in (("sorted(liste)", lambda: sorted(UCLU_IZCI)), ("sorted(liste, key=len)", lambda: sorted(UCLU_IZCI, key=len))): c, _ = olc(f) print(f" {ad:<26s} {' '.join(sorted(set(c))):<20s}" f" {'evet' if '__lt__' in c else 'hayır'}") c, _ = olc(lambda: sorted(UCLU_IZCI, key=len)) print(f" key=len çağrısı öğe başına: {c.count('__len__')} / {len(UCLU_IZCI)} öğe")
yerleşik çağrı çağrılan protokol dizisi len(n) 1 __len__ str(n) 1 __str__ repr(n) 2 __repr__ __str__ bool(n) 1 __len__ iter(n) 1 __iter__ list(n) 5 __iter__ __next__ __next__ __next__ __next__ sum(n) 5 __iter__ __next__ __next__ __next__ __next__ sorted(n) 5 __iter__ __next__ __next__ __next__ __next__ sekiz yerleşik, toplam 21 çağrı yerleşik protokolü tanımlayan nesne tanımlamayan nesne len(n) değer üretti !TypeError str(n) değer üretti değer üretti repr(n) değer üretti değer üretti bool(n) değer üretti değer üretti iter(n) değer üretti !TypeError list(n) değer üretti !TypeError sum(n) değer üretti !TypeError sorted(n) değer üretti !TypeError bool(__bool__ tanımsız) -> __len__ bool(__bool__ tanımlı) -> __bool__ sorted(tek nesne, öğeleri sayı) -> __iter__ __next__ sorted(üç nesnelik liste) -> __lt__ üç nesnelik liste çağrılan protokol karşılaştırma nesneye sorulur mu sorted(liste) __lt__ evet sorted(liste, key=len) __len__ hayır key=len çağrısı öğe başına: 3 / 3 öğe
Bire Bir Eşleşme
Üst tablonun ilk beş satırı sözleşmeyi doğruluyor. len bir çağrı yapıyor ve çağırdığı
şey __len__; str bir çağrı, __str__; iter bir çağrı, __iter__. Yerleşiğin
gövdesinde uzunluk sayan, metin kuran ya da yineleyici üreten bir kod yok — olsaydı çağrı
sayısı sıfır olurdu, çünkü nesneye hiç sormaya gerek kalmazdı.
repr satırındaki 2 bir istisna gibi görünüyor ama değil. repr yine tek bir yöntem
çağırıyor: __repr__. İkinci çağrı yerleşikten gelmiyor — __repr__ gövdesinin kendisi
__str__’i çağırıyor. Yerleşiğin sözleşmesi bozulmuş değil; ölçülen dizi, çağrılan
yöntemin kendi yaptığı işi de gösteriyor.
bool satırı birinci dersin sonucunu tekrar üretiyor: nesnede __bool__ tanımlı olmadığı
için sınama __len__’e düşüyor. Alt bölümdeki iki satır ikisini yan yana koyuyor —
__bool__ tanımlıyken yalnız o çağrılıyor, __len__ sınamaya hiç girmiyor. Bir
yerleşiğin tek bir yöntemle eşleştiği kuralının tek gerçek istisnası budur: bool bir
değil, sıralı iki yöntem arar.
Toplayanların Ortak Dizisi
Son üç satır aynı diziyi veriyor: bir __iter__, dört __next__, toplam 5. Üç ayrı
yerleşik — biri liste kuruyor, biri toplam alıyor, biri sıralıyor — ve üçü de öğeleri tam
olarak aynı biçimde topluyor.
Dizi ikinci dersin ölçtüğü diziyle birebir aynıdır ve aynı nedenle bir fazladır: üç
öğe için dört __next__, sonuncusu StopIteration fırlatarak toplamanın bittiğini
bildiriyor. for döngüsü, list, sum ve sorted — dördü de aynı protokolü aynı sayıda
çağırıyor. Aralarındaki fark topladıkları öğelerle ne yaptıklarıdır, nasıl
topladıkları değil.
Bunun pratik sonucu, yineleme protokolünü tanımlayan bir nesnenin bu yerleşiklerin
tamamıyla kendiliğinden çalışmasıdır. Sınıfa list desteği, sum desteği, sorted
desteği ayrı ayrı eklenmez; __iter__ ile __next__ yazıldığı anda üçü de gelir.
Sıralamanın İki Protokolü
Alt iki satır sorted yerleşiğini ayırıyor ve tek yöntemle eşleşmediğini gösteriyor.
Birinci satırda sıralanan nesnenin öğeleri sayı. Çağrılan protokoller __iter__ ve
__next__ — yani yalnız toplama. Karşılaştırma da yapılıyor elbette, ama sayılar arasında;
kurgunun nesnesi o karşılaştırmaya girmiyor ve kayda hiçbir şey düşmüyor.
İkinci satırda sıralanan şey üç nesnelik bir liste. Bu kez kayıtta yalnız __lt__ var.
Liste zaten bir liste olduğu için toplama aşamasında kurgunun protokolü çağrılmıyor;
sıralama aşamasında ise her karşılaştırma nesneye soruluyor.
İki satır birlikte okunduğunda sorted iki ayrı protokol çağırıyor: öğeleri toplamak
için yineleme protokolü, sıralamak için karşılaştırma protokolü. Hangisinin kayda
düştüğü, kurgunun nesnesinin hangi rolde bulunduğuna bağlı — dışta kapsayıcı olarak mı,
içte öğe olarak mı.
Buradan çıkan okuma şudur: sorted sıralamayı bilmez, karşılaştırmayı nesneye sorar.
Sıralama düzenini belirleyen şey yerleşiğin gövdesi değil, öğelerin __lt__ gövdesidir.
Yerleşik yalnız hangi öğenin hangisinden önce geleceğini sorar ve gelen yanıta uyar.
Aynı liste, öğelerin __lt__ tanımı değiştirilerek başka bir sırayla dizilir ve sorted
çağrısında tek bir karakter değişmez.
Anahtar Protokolü Değiştirir
Son tablo aynı çağrının bir anahtarlı argümanla nasıl başka bir protokole geçtiğini
ölçüyor. İki satırın sıraladığı liste aynı, sıralanan nesneler aynı; değişen tek şey
key argümanının verilip verilmediği.
Anahtarsız çağrıda kayda düşen protokol __lt__ — karşılaştırma nesnelere soruluyor.
Anahtarlı çağrıda kayda düşen protokol __len__ ve __lt__ hiç çağrılmıyor. Sağdaki
sütun bunu doğrudan söylüyor: karşılaştırma artık nesneye sorulmuyor.
Olan şey şudur: key verildiğinde sorted önce her öğeye anahtar işlevini uygular, sonra
sıralamayı dönen anahtar değerleri üzerinde yapar. Nesneler kendi aralarında hiç
karşılaştırılmaz; karşılaştırılan şey onların anahtarlarıdır. Bu yüzden __lt__
tanımlamayan nesneler bile uygun bir anahtarla sıralanabilir — sıralanabilirlik nesnenin
değil, anahtarın özelliği hâline gelir.
Son satır anahtarın kaç kez çağrıldığını veriyor: üç öğe için 3 çağrı, yani öğe başına tam bir kez. Anahtar her karşılaştırmada yeniden hesaplanmıyor; bir kez hesaplanıp saklanıyor. Bunun sonucu, pahalı bir anahtar işlevinin sıralama boyunca bir kez ödenmesidir — karşılaştırma sayısı ne olursa olsun.
Varsayılanı Olan ve Olmayan
Orta tablo protokolün bulunamadığı durumu ölçüyor ve sekiz yerleşik ikiye ayrılıyor.
Beşi hiçbir varsayılan tanımıyor: len, iter, list, sum ve sorted, yöntemi
bulamadığında bir değer değil TypeError üretiyor. Bu yerleşikler için ortak tanımın
dördüncü okuması harfiyen geçerli — protokol yoksa yerleşik de yoktur.
Üçü değer üretiyor. str ile repr, yöntem tanımlanmamış bir nesne için de bir metin
veriyor; dil her nesne için varsayılan bir metin karşılığı tanımlar. bool ise birinci
dersin üçüncü basamağını uyguluyor ve nesneyi doğru sayıyor.
Ayrımın gerekçesi sorunun yanıtlanabilir olup olmamasıdır. “Bu nesnenin uzunluğu kaçtır?” sorusunun nesne söylemezse makul bir yanıtı yoktur; uydurulan her sayı yanlış olurdu. “Bu nesnenin metin karşılığı nedir?” sorusunun ise her nesne için üretilebilecek bir yanıtı vardır. Yerleşiğin varsayılanı olması, sorunun nesneden bağımsız bir yanıtının bulunmasına bağlıdır.
Bu ayrımın bir bedeli var ve tabloda görünmüyor: varsayılanı olan yerleşikler sessizce
çalışır. __len__ yazmayı unutan bir sınıf len çağrısında hemen belli olur; __bool__
yazmayı unutan sınıf ise hiçbir belirti vermeden her if sınamasından doğru döner.
Hatanın erken çıkması, TypeError üreten yerleşiklerin sağladığı bir güvencedir.
Özet
- Yerleşikler iş yapan işlevler değil, nesnedeki özel yöntemi çağıran ince kabuklardır;
lenuzunluğu saymaz,strmetni kurmaz,iteryineleyici üretmez — üçü de sorar. - Sekiz yerleşik üç öğelik nesnede toplam 21 çağrı üretir; ilk beşi birer çağrıyla
tek bir yöntemle eşleşir,
boolise sıralı iki yöntem arar ve__bool__yoksa__len__’e düşer. list,sumvesortedaynı beşli diziyi kurar — bir__iter__, dört__next__— ve bu,fordöngüsünün dizisiyle birebir aynıdır; yineleme protokolünü yazan bir sınıf üçünü birden kazanır.sortedtek değil iki protokol çağırır: toplamak için yineleme, sıralamak için karşılaştırma. Sıralama düzenini yerleşiğin gövdesi değil, öğelerin__lt__gövdesi belirler.- Anahtarlı argüman verildiğinde
sortedkarşılaştırmayı nesneye hiç sormaz:__lt__yerine anahtar işlevi çağrılır — öğe başına tam bir kez — ve sıralanabilirlik nesnenin değil anahtarın özelliği olur. - Protokolü tanımlamayan bir nesnede sekiz yerleşiğin beşi
TypeErrorverir, üçü değer üretir; varsayılanı olanlar nesneden bağımsız bir yanıtı bulunanlardır ve eksikliği sessizce geçiştirme bedelini taşırlar.
Sonraki Adım
Buraya kadar ölçülen her protokol beklenen yolu izledi. Sözdizim bir yöntem aradı,
buldu ve çağırdı; bulamadığında ikinci bir yönteme düştü; o da yoksa ya bir varsayılan
uyguladı ya da düzenli bir biçimde TypeError fırlattı. Döngüyü bitiren StopIteration
bile bir kaza değil, protokolün planlanmış son adımıydı.
Peki bir protokol başarısız olursa? Çağrılan yöntem bulunuyor, çalışıyor, ama işini
tamamlayamıyor — dosya yok, sayı çevrilemiyor, dizin sınırın dışında. Bu derste
TypeError bir kez göründü, önceki derslerde NameError, UnboundLocalError, IndexError
ve StopIteration göründü. Beşi de birer sınıftı ve hepsi aynı yazımla yakalanabilir.
Sonraki ders bu sınıfların birbiriyle ilişkisini kurar: hangisini yakalamak hangilerini de
yakalar, ve geniş bir yakalama neyi yutar.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.