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
Izcinesnesi ya da birIzciile 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:Izcibö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.
- DT30 —
Izcidoğ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
TypeErrorile 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,orvenotö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.