İçeriğe geç
academia.sh

Ders 04 / 16

İşleçler

On bir işleç biçimi 10 ayrı özel yöntemi çağırır ve biri karşılıksız kalır: `n + m` `__add__`, `n += m` `__iadd__` çağırır; `x in n` `__contains__` bulamazsa yineleme protokolüne düşer ve tek çağrı yerine beş çağrı ödenir.

İçindekiler

Önceki ders x += ek yazımının tipe göre iki ayrı iş yaptığını ölçtü: üç tipte adı yeni bir nesneye bağladı, birinde nesneyi yerinde değiştirdi. Yaptığı işin adı konmadı. Bu ders o adı koyar.

Programlama Temelleri kursu işleçleri öncelik, birleşme yönü ve kısa devre değerlendirme başlıklarıyla kurmuş, aynı işlecin farklı tiplerde farklı anlam taşıyabildiğini de göstermişti. Bunlar tekrarlanmaz. Orada kurulan şey kavramdı — bir işlecin anlamı işlenenin tipine bağlıdır. Burada ölçülen şey o bağın düzeneğidir: hangi işleç hangi adı arıyor, aradığını bulamazsa nereye düşüyor ve hiçbir yerde bulamazsa ne oluyor.

İşleç Bir Yöntem Çağrısıdır

İlk dersin kurgusu bu ders için genişletilir. Izci var olan yöntemlerini korur; üzerine çıkarma, çarpma, sağdan çarpma, küçüklük, işaret değiştirme ve kesişim eklenir. Her yöntem çağrıldığında yine adını kaydeder.

  • DT24 — Ölçülen on bir biçimin tamamı iki Izci nesnesi ya da bir Izci ile bir tam sayı üzerinde çalışır; ölçüm işlecin sonucuna değil, çağırdığı yöntem adına bakar.
  • DT25 — Son biçim (n / m) bilerek karşılıksız bırakılmıştır: Izci bölme için hiçbir yöntem tanımlamaz. O satırdaki ünlem önekli kayıt, bir çağrıyı değil bir istisnayı gösterir.
"""Isleclerin cagirdigi ozel yontemler; Izci her katilimini kaydeder."""

KAYIT = []


def kaydet(ad):
    KAYIT.append(ad)


class Izci:
    """Ortak kurgunun islec bolumu; bu derste alti yontem eklenmistir."""

    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 __contains__(self, x):
        kaydet("__contains__")
        return x in self.ogeler

    def __add__(self, o):
        kaydet("__add__")
        return Izci(self.ogeler + list(o.ogeler))

    def __iadd__(self, o):
        kaydet("__iadd__")
        self.ogeler.extend(o.ogeler)
        return self

    def __eq__(self, o):
        kaydet("__eq__")
        return isinstance(o, Izci) and self.ogeler == o.ogeler

    def __sub__(self, o):                      # eklendi
        kaydet("__sub__")
        return Izci([x for x in self.ogeler if x not in o.ogeler])

    def __mul__(self, k):                      # eklendi
        kaydet("__mul__")
        return Izci(self.ogeler * k)

    def __rmul__(self, k):                     # eklendi
        kaydet("__rmul__")
        return Izci(self.ogeler * k)

    def __lt__(self, o):                       # eklendi
        kaydet("__lt__")
        return len(self.ogeler) < len(o.ogeler)

    def __neg__(self):                         # eklendi
        kaydet("__neg__")
        return Izci([-x for x in self.ogeler])

    def __and__(self, o):                      # eklendi
        kaydet("__and__")
        return Izci([x for x in self.ogeler if x in o.ogeler])


def olc(islev):
    """Bir islec bicimini calistirir ve cagrilan protokolleri dondurur."""
    KAYIT.clear()
    try:
        islev()
    except Exception as e:
        KAYIT.append(f"!{type(e).__name__}")
    return list(KAYIT)


def yerinde_toplama():
    a = Izci()
    a += Izci()


BICIMLER = (
    ("n + m", lambda: Izci() + Izci()),
    ("n += m", yerinde_toplama),
    ("n - m", lambda: Izci() - Izci()),
    ("n * 2", lambda: Izci() * 2),
    ("2 * n", lambda: 2 * Izci()),
    ("-n", lambda: -Izci()),
    ("n < m", lambda: Izci() < Izci()),
    ("n == m", lambda: Izci() == Izci()),
    ("n & m", lambda: Izci() & Izci()),
    ("x in n", lambda: 9 in Izci()),
    ("n / m", lambda: Izci() / Izci()),
)

print(f"{'işleç biçimi':<14s} {'çağrı':>5s}  protokol")
for ad, islev in BICIMLER:
    c = olc(islev)
    print(f"{ad:<14s} {len(c):5d}  {' '.join(c)}")

kayitlar = [olc(f) for _, f in BICIMLER]
tekil = sorted({p for c in kayitlar for p in c if not p.startswith("!")})
print(f"\n{len(BICIMLER)} biçim, ayrı protokol {len(tekil)}, "
      f"toplam çağrı {sum(len(c) for c in kayitlar)}, "
      f"karşılıksız biçim {sum(1 for c in kayitlar if any(p.startswith('!') for p in c))}")
işleç biçimi   çağrı  protokol
n + m              1  __add__
n += m             1  __iadd__
n - m              1  __sub__
n * 2              1  __mul__
2 * n              1  __rmul__
-n                 1  __neg__
n < m              1  __lt__
n == m             1  __eq__
n & m              1  __and__
x in n             1  __contains__
n / m              1  !TypeError

11 biçim, ayrı protokol 10, toplam çağrı 11, karşılıksız biçim 1

On bir biçim 10 ayrı protokol çağırıyor ve 1 biçim karşılıksız kalıyor. Tablonun okunuşu tektir: solda yazılan simge, sağdaki adın kısaltmasıdır. - işleci __sub__, * işleci __mul__, < işleci __lt__ demektir. & satırı bu okumanın en açık örneğidir — işlecin ne yaptığı tipe kalmıştır: tam sayılarda bit düzeyinde birleştirme yapar, burada Izci onu kesişim olarak tanımlamıştır. İşleç bir anlam taşımaz, bir ad taşır.

Son satır bunun öteki yüzü. Izci bölme için hiçbir yöntem tanımlamadığından n / m biçimi TypeError ile düşüyor. Kaynak kusursuzdur ve derleme aşamasını geçmiştir; kusur, çağrılacak adın bulunamamasıdır. Bir işlecin “desteklenmemesi” diye ayrı bir durum yoktur — yalnız tanımlanmamış bir yöntem vardır. Bir tipe bölme kazandırmanın yolu da bu yüzden tektir: __truediv__ adında bir yöntem yazmak.

Toplama ile Yerinde Toplama

Tablonun ilk iki satırı kursun ikinci iddiasını ödüyor. n + m __add__, n += m __iadd__ çağırıyor. İkisi birbirinin kısaltması değildir; ayrı adlar, ayrı sözleşmelerdir.

__add__ işlenenlerine dokunmaz, yeni bir nesne üretip döndürür. __iadd__ var olan nesneyi yerinde değiştirip kendisini döndürür. Önceki dersin ölçtüğü ayrım budur: bir tip __iadd__ tanımlamışsa += nesneyi değiştirir ve ona bağlı ikinci ad da değişikliği görür; tanımlamamışsa += yazımı __add__’e düşer, yeni nesne üretilir ve yalnız soldaki ad ona bağlanır. Tam sayı, dizgi ve demetin geçen derste “adı yeniden bağladı” çıkmasının nedeni tam olarak budur — bu tipler değiştirilemez olduğu için __iadd__ tanımlayamazlar.

Buradan pratik bir sonuç çıkar: a += b yazımı, a başkalarıyla paylaşılan bir nesneye bağlıysa onların gördüğü değeri de değiştirir. a = a + b yazımı ise asla değiştirmez. İki satır aynı işi yapmaz ve aralarındaki seçim üslup değildir.

Sağdaki İşlenene Düşme

n * 2 ile 2 * n satırları ayrı yöntemler çağırıyor: __mul__ ve __rmul__. Nedeni şudur: 2 * n yazıldığında önce soldaki işlenenin tipine sorulur. Tam sayı, bir Izci ile nasıl çarpılacağını bilmez ve “bilmiyorum” yanıtı verir. Bunun üzerine sağdaki işlenene, işlemin ters yönü sorulur — aranan ad __mul__ değil, başında r olan __rmul__’dır.

Bu düşüş, dilin genişletilebilirliğinin temelidir. Yerleşik bir tipe yeni bir davranış eklenemez; ama yeni bir tip, yerleşik tiplerin solda durduğu ifadelere __radd__, __rmul__ gibi yöntemlerle katılabilir. Aranan yöntem iki işlenende de yoksa n / m satırındaki sonuç alınır.

Önceliğin Belirlediği Çağrı Sırası

Bir ifadede birden çok işleç varsa hangisinin önce çağrıldığını öncelik belirler. Programlama Temelleri kursu öncelik ve birleşme yönü tablosunu kurmuştu; burada ölçülen şey o tablonun kod düzeyindeki karşılığıdır. Öncelik bir okuma kolaylığı değildir: ikinci dersin gösterdiği ağacı biçimlendirir ve ağaç da yöntemlerin çağrılma sırasını verir.

  • DT26 — Üç ifade aynı üç nesne üzerinde kurulur ve yalnız işleçleri ile parantezleriyle ayrılır; ölçülen şey kayıt dizisinin sırasıdır, uzunluğu değil.
def carpim_once():
    Izci((1,)) + Izci((2,)) * 2


def toplama_once():
    (Izci((1,)) + Izci((2,))) * 2


def karsilastirma_sonra():
    Izci((1,)) + Izci((2,)) == Izci((1, 2))


print(f"{'ifade':<16s} çağrı sırası")
for ad, islev in (("n + m * 2", carpim_once),
                  ("(n + m) * 2", toplama_once),
                  ("n + m == k", karsilastirma_sonra)):
    print(f"{ad:<16s} {' '.join(olc(islev))}")
ifade            çağrı sırası
n + m * 2        __mul__ __add__
(n + m) * 2      __add__ __mul__
n + m == k       __add__ __eq__

İlk iki satırda aynı iki yöntem çağrılıyor, sıraları ters. Çarpma toplamadan önce geldiği için __mul__ önce çalışıyor ve ürettiği nesne __add__’e işlenen olarak giriyor; parantez konduğunda sıra tersine dönüyor. Üçüncü satır aritmetiğin karşılaştırmadan önce geldiğini gösteriyor: toplama tamamlanmadan eşitlik sorulamaz, çünkü karşılaştırılacak nesne henüz yoktur. Öncelik kuralları, hangi yöntemin hangi nesneyi işlenen olarak göreceğini belirler.

Üyelik Sınaması Yöntemini Bulamazsa

İkinci iddianın en görünür örneği üyelik sınamasıdır. x in n biçimi önce __contains__ arar; bulamazsa yineleme protokolüne düşer ve nesneyi baştan sona tarayarak öğeleri tek tek karşılaştırır. Sözdizim aynıdır, bedel değildir.

  • DT27 — Karşılaştırılan iki nesne aynı üç öğeyi taşır; tek fark Izci’nin __contains__ tanımlaması, Yinelenir’in tanımlamamasıdır.
  • DT28 — Her sınama bir kez bulunan ve bir kez bulunmayan bir değerle çalıştırılır; ölçülen şey sonuç ile çağrı sayısıdır.
class Yinelenir:
    """__contains__ tanimlamaz; yalniz yineleme protokolunu tasir."""

    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


UYELIK = (
    ("2 in Izci()      ", lambda: 2 in Izci()),
    ("9 in Izci()      ", lambda: 9 in Izci()),
    ("2 in Yinelenir() ", lambda: 2 in Yinelenir()),
    ("9 in Yinelenir() ", lambda: 9 in Yinelenir()),
)

print(f"{'üyelik sınaması':<18s} {'sonuç':>5s} {'çağrı':>5s}  protokol")
for ad, islev in UYELIK:
    KAYIT.clear()
    sonuc = islev()
    c = list(KAYIT)
    print(f"{ad:<18s} {str(sonuc):>5s} {len(c):5d}  {' '.join(c)}")
üyelik sınaması    sonuç çağrı  protokol
2 in Izci()         True     1  __contains__
9 in Izci()        False     1  __contains__
2 in Yinelenir()    True     3  __iter__ __next__ __next__
9 in Yinelenir()   False     5  __iter__ __next__ __next__ __next__ __next__

Izci iki durumda da 1 çağrı ödüyor: sorulan soru doğrudan nesneye gidiyor ve nesne yanıtı kendi biliyor. Yinelenir aynı sonuçları veriyor ama bedeli ayrı — bulunan değerde 3, bulunmayan değerde 5 çağrı.

Sayıların dağılımı düşüşün nasıl işlediğini de gösteriyor. Bulunan değerde tarama ikinci öğede duruyor: bir __iter__ ve iki __next__. Bulunmayan değerde ise üç öğenin üçü de okunuyor ve dördüncü __next__ StopIteration fırlatarak taramayı bitiriyor — ilk dersteki “öğe sayısından bir fazla” örüntüsünün aynısı. Bulunmayan bir değer her zaman tam tarama ödettirir.

Izci’nin __iter__ de tanımlaması bir noktayı daha kapatıyor: iki yöntem de tanımlıysa in biçimi __contains__’i seçer, çünkü düşüş ancak arananın yokluğunda olur. Sonuç ikisinde de aynı çıkabilir; ödenen çağrı sayısı çıkmaz.

Zincirlenen Karşılaştırma ve Mantıksal İşleçler

İki yazım kaldı ve ikisi de tabloya sığmıyor, çünkü çağırdıkları yöntem sayısı sabit değil.

  • DT29 — Ortadaki işlenen bir işlev çağrısıyla üretilir ve bu işlev de kayda katılır; böylece işlenenin kaç kez üretildiği sayılabilir.
  • DT30Izci doğruluk sınaması için hiçbir yöntem tanımlamaz; mantıksal satırlarda kaydın boş çıkması bu yüzden anlamlıdır. Sınamanın hangi yöntemi çağırdığı akış konusunun ilk dersinde ölçülür.
def orta():
    kaydet("orta()")                 # ortadaki islenen kac kez uretildi?
    return Izci((1, 2))


def zincir_iki_dogru():
    Izci((1,)) < orta() < Izci((1, 2, 3))


def acik_yazim():
    Izci((1,)) < orta() and orta() < Izci((1, 2, 3))


def zincir_ilk_yanlis():
    Izci((1, 2, 3)) < orta() < Izci((1, 2, 3, 4))


print(f"{'yazım':<32s} {'çağrı':>5s}  protokol")
for ad, islev in (("a < orta() < c  (ikisi de doğru)", zincir_iki_dogru),
                  ("a < orta() and orta() < c", acik_yazim),
                  ("a < orta() < c  (ilki yanlış)", zincir_ilk_yanlis)):
    c = olc(islev)
    print(f"{ad:<32s} {len(c):5d}  {' '.join(c)}")

print()
print(f"{'mantıksal yazım':<12s} {'çağrı':>5s}  protokol")
for ad, islev in (("n and m", lambda: Izci() and Izci()),
                  ("n or m", lambda: Izci() or Izci()),
                  ("not n", lambda: not Izci()),
                  ("n & m", lambda: Izci() & Izci())):
    c = olc(islev)
    print(f"{ad:<12s} {len(c):5d}  {' '.join(c) if c else '(hiçbiri)'}")

print()
print("and/or ne döndürür:", repr("ol" and "cum"), repr("" or "cum"),
      repr(0 or []), "| not ne döndürür:", repr(not "ol"))
yazım                            çağrı  protokol
a < orta() < c  (ikisi de doğru)     3  orta() __lt__ __lt__
a < orta() and orta() < c            4  orta() __lt__ orta() __lt__
a < orta() < c  (ilki yanlış)        2  orta() __lt__

mantıksal yazım çağrı  protokol
n and m          0  (hiçbiri)
n or m           0  (hiçbiri)
not n            0  (hiçbiri)
n & m            1  __and__

and/or ne döndürür: 'cum' 'cum' [] | not ne döndürür: False

Zincirlenen karşılaştırma iki __lt__ çağırıyor ama ortadaki işleneni bir kez üretiyor. Elle açılmış yazım aynı iki karşılaştırmayı yapıyor ve ortayı iki kez üretiyor: dört çağrıya karşı üç. Fark yalnız sayıda değil, doğrulukta da olabilir — ortadaki ifade bir yan etki taşıyorsa iki yazım aynı programı vermez. Üçüncü satır zincirin ikinci yarısını da gösteriyor: ilk karşılaştırma yanlış çıktığında ikincisi hiç yapılmıyor, çağrı 2’de kalıyor.

Alt tablo bir sınır çiziyor. and, or ve not hiçbir özel yöntem çağırmıyor; kayıtları boş. Bunlar aşırı yüklenemez, çünkü yaptıkları iş bir işlem değil, bir seçimdir: işleneni bir doğruluk sınamasından geçirip işlenenlerden birini olduğu gibi döndürürler. Son satır bunu doğruluyor — "ol" and "cum" bir doğruluk değeri değil, 'cum' dizgisini veriyor; 0 or [] boş listeyi veriyor. Yalnız not bir doğruluk değeri üretir. Karıştırılmaya en elverişli çift and ile &’dir: ilki bir seçim, ikincisi __and__ çağıran bir işleçtir.

Özet

  • Her işleç adı belli bir özel yöntemin kısaltmasıdır; on bir biçim 10 ayrı protokol çağırır ve karşılığı olmayan biçim TypeError ile düşer. Öncelik, bu yöntemlerin hangi sırayla çağrılacağını belirler.
  • n + m __add__, n += m __iadd__ çağırır; ilki yeni nesne üretir, ikincisi var olanı değiştirir ve paylaşılan bir nesnede bu fark gözlenir.
  • Soldaki işlenen işlemi karşılayamazsa sağdakinin ters yöndeki yöntemi (__rmul__) aranır; ikisi de yoksa işleç karşılıksız kalır.
  • x in n önce __contains__ arar (1 çağrı); bulamazsa yineleme protokolüne düşer ve bulunmayan bir değer için 5 çağrı öder.
  • Zincirlenen karşılaştırma ortadaki işleneni bir kez üretir ve ilk karşılaştırma yanlışsa ikincisini hiç yapmaz.
  • and, or ve not özel yöntem çağırmaz; işlenenlerden birini olduğu gibi döndürürler ve bit düzeyi karşılıkları olan & ile |’dan ayrıdırlar.

Sonraki Adım

Bu derste + işlecinin Izci üzerinde yeni bir nesne ürettiği görüldü. Aynı işleç dizgiler üzerinde de çalışır ve orada seçenek bile yoktur: dizgi değiştirilemez bir tiptir, dolayısıyla __iadd__ tanımlayamaz. Sonraki ders bunun bedelini sayar — bir dizginin küçük harfe çevrilmesi, boşluklarının atılması ya da bir parçasının değiştirilmesi neden her seferinde yeni bir nesne üretmek zorundadır, biçimlendirme yazımları bu üretimi nasıl tek adıma indirir ve ard arda yapılan değişikliklerde kaç nesne ortaya çıkar?

İ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