İçeriğe geç
academia.sh

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. Akis yalnız yineleme ve karşılaştırma yöntemlerini tanımlar; Izci ona uzunluk ve metin yöntemlerini ekler. Toplayan yerleşikler Akis üzerinde ölçülür — böylece ölçülen dizi yalnız yineleme protokolünden gelir.
  • PA33Sessiz hiç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.
  • PA35Izci.__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 key olarak len yerleş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; len uzunluğu saymaz, str metni kurmaz, iter yineleyici ü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, bool ise sıralı iki yöntem arar ve __bool__ yoksa __len__’e düşer.
  • list, sum ve sorted aynı beşli diziyi kurar — bir __iter__, dört __next__ — ve bu, for döngüsünün dizisiyle birebir aynıdır; yineleme protokolünü yazan bir sınıf üçünü birden kazanır.
  • sorted tek 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 sorted karşı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 TypeError verir, üçü 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat