Ders 01 / 16
Python'un Yürütme Modeli
Kaynak önce bütün olarak derlenir sonra yürütülür; sözdizim ise bir kısaltmadır — on bir biçim 12 ayrı özel yöntemi toplam 18 kez çağırır ve üç öğelik nesnede döngü öğe sayısından bir fazla, 4 kez `__next__` çağırır.
İçindekiler
Yazılım Geliştirme Pratiği müfredatı beş kursla bir ekip disiplini kurdu ve kapanışında ölçtüğü her şeyin dilden bağımsız olduğunu söyledi: sayılan şey hiçbir zaman yazılan kodun kendisi değil, onun çevresindeki işti. Bu müfredat çevreden merkeze döner. Sıra dilin kendisinde.
Programlama Temelleri kursu örneklerini Python’la yazmıştı; atamanın bir bağlama olduğu,
is ile == ayrımı, temel veri tipleri, işleçler ve tip dönüşümü orada kuruldu. Ama bir
dili örnek olarak kullanmak ile dilin kendisini öğrenmek ayrı şeylerdir: orada
Python bir anlatım aracıydı, burada ölçülen nesnedir. Bu kurs sözdizimin altına bakar ve
tek bir soruyu on altı derste taşır — yazılan biçim, çalışırken hangi düzeneği çağırıyor ve
kaç kez?
İki Aşama: Derleme ve Yürütme
Python bir yorumlayıcı (interpreter) ile çalışır, ama kaynağı satır satır okuyup hemen çalıştırmaz. Bir dosya çalıştırıldığında metin önce sözcük birimlerine (token) ayrılır, sonra bir soyut sözdizim ağacına dönüştürülür, sonra bayt koduna (bytecode) çevrilir. Yorumlayıcının yürüttüğü şey bayt koddur, kaynak metin değil.
İki aşama arasındaki sınır gözlenebilir. Derleme, dosyanın tamamı üzerinde yapılır:
kaynağın son satırındaki bir sözdizim kusuru, ilk satırdaki deyimin yürütülmesini de
engeller. Yürütme aşamasında doğan bir hata ise ancak kendi sırası geldiğinde ortaya çıkar
ve ondan öncekiler çalışmış olur. Aradaki fark iki yerleşik işlevle ölçülebilir: compile
kaynağı derler ve bir kod nesnesi döndürür, exec o nesneyi yürütür.
"""Iki asama: kaynak once bir butun olarak derlenir, sonra yurutulur.""" KAYNAK = { "eksik parantez": 'yaz = print\nyaz("bir")\nyaz(\n', "tanimsiz ad": 'yaz = print\nyaz("bir")\nyazz("iki")\n', } for ad, kaynak in KAYNAK.items(): print(f"--- {ad}") try: kod = compile(kaynak, "<ders>", "exec") except SyntaxError as e: print(f"derleme: {type(e).__name__} — hiçbir deyim yürütülmedi") continue print(f"derleme: sorunsuz, elde edilen {type(kod).__name__} nesnesi") try: exec(kod, {}) except Exception as e: print(f"yürütme: {type(e).__name__} — kendinden önceki deyimler yürütüldü")
--- eksik parantez derleme: SyntaxError — hiçbir deyim yürütülmedi --- tanimsiz ad derleme: sorunsuz, elde edilen code nesnesi bir yürütme: NameError — kendinden önceki deyimler yürütüldü
İkinci kaynakta bir satırı basılmış, ondan sonraki çağrı NameError ile düşmüştür.
Birinci kaynakta hiçbir şey basılmamıştır — oysa oradaki ilk iki satır kusursuzdur. Ayrım
şudur: sözdizim kusuru derleme aşamasında, ad kusuru yürütme aşamasında bulunur. Bir
adın var olup olmadığı derlenirken bilinmez, çünkü ad ancak yürütme sırasında bir nesneye
bağlanır.
Aynı iki adım etkileşimli kabukta (interactive shell) da işler; orada derlenen birim dosyanın tamamı değil, girilen bloktur. Bloğun sözdizimi bozuksa blok hiç yürütülmez.
Derlemenin ürünü olan bayt kodun düzeni dilin tanımının parçası değildir; yorumlayıcının iç işidir ve okunması bu kursun konusu değil. Okunacak olan şey bayt kodun çağırdığı yöntemlerdir.
Sözdizim Bir Kısaltmadır
Kursun ölçü ekseni tek bir gözlemden çıkar: Python’da sözdizimin çoğu biçimi bir
kısaltmadır. n + m yazıldığında yürütülen şey bir toplama değil, bir çağrıdır —
n nesnesinin __add__ yöntemi çağrılır ve dönen değer ifadenin değeridir. for x in n
bir döngü sözcüğü değil, iki yöntemin ardışık çağrısıdır: önce __iter__, sonra tükenene
kadar __next__.
Bu yöntemlerin adları dilin tanımında yazılıdır; her birine özel yöntem (special method), bir biçimin gerektirdiği yöntem kümesine de protokol denir. Adları iki alt çizgi arasında yazılır ve bu, onları sıradan yöntemlerden ayırmak içindir: özel yöntemler doğrudan çağrılmak için değil, sözdizim tarafından çağrılmak için vardır.
Her sözdizim biçimi bir kısaltma değildir ve bu ayrım baştan konmalıdır. Bir ada değer bağlamak, bir işlev tanımlamak ya da bir değer döndürmek nesneye hiçbir soru sormaz; bu biçimler adlarla ve akışla ilgilenir, nesnenin içine bakmaz. Kısaltma olanlar, bir nesneyi tüketen biçimlerdir: onu toplayan, içinde dolaşan, bir koşulda sınayan, dilimleyen, metne çeviren. Ölçülen on bir biçimin tamamı bu ikinci kümedendir.
Bunun doğrudan bir sonucu var: sözdizim tipin adına bakmaz, yöntemin varlığına bakar.
+ işlecinin sayılarda toplama, dizgilerde birleştirme yapması bir ayrıcalık değil, iki
ayrı tipin aynı adlı yöntemi ayrı biçimde tanımlamasıdır. Programlama Temelleri kursunda bu
gözlem işleç aşırı yükleme başlığıyla bir kavram olarak kuruldu; burada ölçülen şey
kavram değil, onu gerçekleştiren addır — hangi biçim hangi yöntemi kaç kez çağırıyor.
Ortak Kurgu: Katılımını Kaydeden Nesne
Ölçüm için tek bir sınıf yeter. Izci, çok sayıda özel yöntemi tanımlar ve her yöntem
çağrıldığında adını ortak bir listeye yazar. Üzerinde sıradan sözdizim çalıştırılır — for,
in, if, +, +=, dilim, str(), with, == — ve her biçimin hangi yöntemi kaç kez
çağırdığı listeden okunur.
İkinci bir sınıf, Bosluksuz, Izci’den türer ve yalnız tek bir şey ekler: __bool__.
Aynı if ifadesi bu nesnede artık başka bir yöntemi çağıracaktır. Kâhin kurgunun
kendisidir: hangi yöntemin çağrıldığını nesne kendi kaydettiği için biliriz, dolayısıyla
sayı bir çıkarım değil bir dökümdür.
"""Sozdizimin cagirdigi protokol: her ozel yontem cagrildiginda kaydedilir.""" KAYIT = [] def kaydet(ad): KAYIT.append(ad) class Izci: """Cok sayida protokole katilan bir nesne; her katilimi kaydeder.""" 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 __len__(self): kaydet("__len__") return len(self.ogeler) 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 __getitem__(self, k): kaydet("__getitem__") return self.ogeler[k] def __eq__(self, o): kaydet("__eq__") return isinstance(o, Izci) and self.ogeler == o.ogeler def __str__(self): kaydet("__str__") return f"Izci{tuple(self.ogeler)}" def __repr__(self): kaydet("__repr__") return self.__str__() def __enter__(self): kaydet("__enter__") return self def __exit__(self, tur, deger, iz): kaydet("__exit__") return False # istisnayi yutmaz class Bosluksuz(Izci): """__bool__ tanimli: dogruluk sinamasi artik __len__'e dusmez.""" def __bool__(self): kaydet("__bool__") return len(self.ogeler) > 0 def olc(islev): """Bir sozdizim bicimini calistirir ve cagrilan protokolleri dondurur.""" KAYIT.clear() try: islev() except Exception as e: KAYIT.append(f"!{type(e).__name__}") return list(KAYIT) def dongu(): for _ in Izci(): pass def uyelik(): 9 in Izci() def dogruluk_len(): if Izci(): pass def dogruluk_bool(): if Bosluksuz(): pass def toplama(): Izci() + Izci() def yerinde_toplama(): a = Izci() a += Izci() def dilim(): Izci()[0:2] def metne_cevirme(): str(Izci()) def baglam(): with Izci(): pass def baglam_istisnali(): with Izci(): raise ValueError("ornek") def esitlik(): Izci() == Izci() BICIMLER = ( ("for x in n", dongu), ("x in n", uyelik), ("if n (__len__)", dogruluk_len), ("if n (__bool__)", dogruluk_bool), ("n + m", toplama), ("n += m", yerinde_toplama), ("n[0:2]", dilim), ("str(n)", metne_cevirme), ("with n", baglam), ("with n + istisna", baglam_istisnali), ("n == m", esitlik), )
Ölçümün varsayımları:
- DT1 — Ölçülen biçimler on bir tanedir; dilin bütün sözdizimi değil, bu kursun ders ders ödeyeceği çekirdektir. Kalan biçimler kendi derslerinde ölçülür.
- DT2 — Kâhin nesnenin kendi kaydıdır: bir yöntem çağrıldığında adını listeye yazar, dolayısıyla ölçüm bir çıkarım değil bir sayımdır.
- DT3 —
Izciüç öğeyle kurulur; döngü ölçümünde okunacak çağrı sayısı bu üçe bağlıdır. - DT4 —
__exit__her koşumdaFalsedöndürür; bağlam bloğunda doğan istisna yutulmaz ve ölçüme, önüne bir ünlem işareti konmuş bir istisna adı olarak girer. - DT5 — Ölçüm hiçbir ortam verisi okumaz: bellek adresi, nesne kimliği sayısı ve süre kullanılmaz; sayılan tek şey çağrılan yöntem adlarıdır.
- DT6 — Her biçim iki kez ölçülür — tablo için bir, çift karşılaştırması için bir.
Izciher ölçümde yeniden kurulduğundan iki koşum aynı diziyi verir.
Ölçüm
print(f"{'sözdizim':<20s} {'çağrı':>6s} protokol dizisi") for ad, islev in BICIMLER: c = olc(islev) print(f"{ad:<20s} {len(c):6d} {' '.join(c)}") print() ogeli = {ad: olc(f) for ad, f in BICIMLER} tekil = sorted({p for c in ogeli.values() for p in c if not p.startswith("!")}) print(f"ayrı protokol {len(tekil)}: {', '.join(tekil)}") print(f"toplam çağrı {sum(len(c) for c in ogeli.values())}, " f"istisnayla biten biçim " f"{sum(1 for c in ogeli.values() if any(p.startswith('!') for p in c))}") print() print("aynı görünen, ayrı protokol çağıran çiftler:") for a, b in (("n + m", "n += m"), ("if n (__len__)", "if n (__bool__)")): print(f" {a:<18s} -> {' '.join(ogeli[a])}") print(f" {b:<18s} -> {' '.join(ogeli[b])}") print() dongu_kaydi = ogeli["for x in n"] print(f"üç öğelik nesnede döngü: {dongu_kaydi.count('__next__')} kez __next__ " f"çağrıldı — öğe sayısından bir fazla; sonuncusu StopIteration ile bitti")
sözdizim çağrı protokol dizisi for x in n 5 __iter__ __next__ __next__ __next__ __next__ x in n 1 __contains__ if n (__len__) 1 __len__ if n (__bool__) 1 __bool__ n + m 1 __add__ n += m 1 __iadd__ n[0:2] 1 __getitem__ str(n) 1 __str__ with n 2 __enter__ __exit__ with n + istisna 3 __enter__ __exit__ !ValueError n == m 1 __eq__ ayrı protokol 12: __add__, __bool__, __contains__, __enter__, __eq__, __exit__, __getitem__, __iadd__, __iter__, __len__, __next__, __str__ toplam çağrı 18, istisnayla biten biçim 1 aynı görünen, ayrı protokol çağıran çiftler: n + m -> __add__ n += m -> __iadd__ if n (__len__) -> __len__ if n (__bool__) -> __bool__ üç öğelik nesnede döngü: 4 kez __next__ çağrıldı — öğe sayısından bir fazla; sonuncusu StopIteration ile bitti
Sayıların Okunması
On bir biçim 12 ayrı protokolü ve toplam 18 çağrıyı üretiyor. Bu, kursun taşıyacağı ölçüdür: bir dersin sayısı, anlattığı sözdizimin hangi protokolü kaç kez çağırdığıdır. Tablonun her satırı bir sözdizim biçimini, karşısındaki dizi de o biçimin altında gerçekten çalışan çağrıları gösteriyor. Hiçbiri tahmin değil; hepsi nesnenin kendi kaydından okundu.
Aynı görünen iki ifade ayrı protokol çağırır. n + m ile n += m yazımca birbirinin
kısaltması gibi durur, ama biri __add__, öteki __iadd__ çağırıyor. Fark yalnız ad farkı
değildir: __add__ yeni bir nesne üretip döndürür, __iadd__ var olan nesneyi
değiştirip kendisini döndürür. Aynı biçimde if n, Izci üzerinde __len__, Bosluksuz
üzerinde __bool__ çağırıyor — tek bir sözdizim, iki ayrı yöntem. Ayrımı yaratan şey
nesnenin hangi yöntemi tanımladığıdır. Sözdizimin görünüşü çağrılan protokolü söylemez.
Bu iddianın ilk yarısı bu konunun işleçler dersinde, ikinci yarısı akış konusunun koşullar
dersinde ödenecek.
İstisna bir hata değil, protokolün parçasıdır. Üç öğelik nesnede döngü 4 kez
__next__ çağırdı — öğe sayısından bir fazla. Dördüncü çağrı bir değer döndürmez,
StopIteration fırlatır; döngüyü bitiren şey tam olarak budur. for sözcüğü öğelerin
bittiğini saymayla değil, bir istisnayı yakalamayla anlar. with n + istisna satırı aynı
sınıftandır: blok içinde bir istisna doğduğu hâlde __exit__ yine çağrılmıştır ve
kayıtta üçüncü öğe olarak istisnanın adı durur. __exit__ False döndürdüğü için istisna
dışarı çıktı — dönseydi True, çıkmayacaktı.
Protokol Yoksa Sözdizim de Yoktur
Yukarıdaki tablo Izci’nin yöntemleri olduğu için doludur. Aynı biçimler hiçbir özel
yöntem tanımlamayan bir nesne üzerinde denenirse ne olur? Ölçümün varsayımı:
- DT7 — Karşılaştırılan nesne hiçbir özel yöntem tanımlamaz; taşıdığı tek şey her nesnenin tabanından gelen tanımlardır. Düşen biçimler istisna türüyle, düşmeyenler “çalıştı” ile yazılır; değerleri basılmaz, çünkü taban tanımın ürettiği metin ortama bağlı bir kimlik taşır.
class Ciplak: """Hicbir ozel yontem tanimlamaz; nesnenin tabanindan geleni tasir.""" def ciplak_dongu(): for _ in Ciplak(): pass def ciplak_baglam(): with Ciplak(): pass CIPLAK_BICIMLER = ( ("for x in n", ciplak_dongu), ("x in n", lambda: 9 in Ciplak()), ("n + m", lambda: Ciplak() + Ciplak()), ("n[0:2]", lambda: Ciplak()[0:2]), ("len(n)", lambda: len(Ciplak())), ("with n", ciplak_baglam), ("str(n)", lambda: str(Ciplak())), ("n == m", lambda: Ciplak() == Ciplak()), ) print("özel yöntemi olmayan nesnede aynı biçimler:") dusen = 0 for ad, islev in CIPLAK_BICIMLER: try: islev() except Exception as e: dusen += 1 print(f" {ad:12s} {type(e).__name__}") else: print(f" {ad:12s} çalıştı — taban nesneden gelen tanım kullanıldı") print(f"düşen biçim {dusen} / {len(CIPLAK_BICIMLER)}")
özel yöntemi olmayan nesnede aynı biçimler: for x in n TypeError x in n TypeError n + m TypeError n[0:2] TypeError len(n) TypeError with n TypeError str(n) çalıştı — taban nesneden gelen tanım kullanıldı n == m çalıştı — taban nesneden gelen tanım kullanıldı düşen biçim 6 / 8
Sekiz biçimin 6’sı düşüyor ve hepsi aynı istisnayla: TypeError. Adı yerindedir —
kusur, kullanılan değerde değil, nesnenin o sözdizimi karşılayacak yöntemi
taşımamasındadır. Kaynak metin kusursuzdur ve derleme aşamasını sorunsuz geçmiştir; iş
yürütmeye geldiğinde çağrılacak yöntem bulunamaz.
Düşmeyen 2 biçim de aynı kuralın öbür yüzüdür. str(n) ile n == m çalışıyor, çünkü
her nesne bu iki yöntemi tabanından devralır: birincisi nesneyi tanıtan bir metin
üretir, ikincisi eşitliği kimlik üzerinden yanıtlar. Yani protokol yine vardır; yalnız
tanımı sınıfın kendisinden değil, tabanından gelir. Bir sınıf __str__ yazdığında sıfırdan
bir yetenek eklemez, devraldığı tanımın yerine geçer.
Sonuç, kursun taşıyacağı dördüncü okumadır: Python’da bir nesnenin “ne yapabildiği” tipinin
adıyla değil, erişebildiği özel yöntemlerle belirlenir. __iter__ erişilebilirse
üzerinde döngü kurulur, __len__ erişilebilirse uzunluğu sorulur; ikisi arasında bir
zorunluluk yoktur. Dil, bir biçimi çalıştırmadan önce tipin adına bakmaz — yöntemi arar,
bulamazsa TypeError fırlatır.
Özet
- Kaynak metin önce sözcük birimlerine ve soyut sözdizim ağacına, sonra bayt koda çevrilir; yorumlayıcının yürüttüğü şey bayt koddur.
- Derleme dosyanın tamamı üzerinde yapılır: sözdizim kusuru hiçbir deyimi çalıştırmaz, ad kusuru ise ancak kendi sırası gelince ortaya çıkar ve öncekiler çalışmış olur.
- Sözdizim bir kısaltmadır; her biçim adı belli bir özel yöntemi çağırır ve yöntem kümesine protokol denir.
- On bir biçim 12 ayrı protokolü toplam 18 çağrıyla kullanır;
for x in ntek başına 5 çağrı üretir. - Üç öğelik nesnede
__next__4 kez çağrılır ve sonuncusuStopIterationfırlatır; istisna burada bir kusur değil, döngüyü bitiren düzenektir. - Hiçbir özel yöntem tanımlamayan bir nesnede aynı sekiz biçimin 6’sı
TypeErrorile düşer; kalan 2’si taban nesneden devralınan tanımla çalışır.
Sonraki Adım
Bu derste sözdizimin altındaki çağrılar sayıldı, ama sözdizimin kendisi hiç sorgulanmadı:
for satırının altındaki gövdenin nereden başlayıp nerede bittiğini ne belirliyor? Python
bunu ayraçla değil boşlukla yapar ve boşluk burada bir yazım tercihi değil, dilbilgisinin
parçasıdır. Sonraki ders bunu ölçer: aynı sözcük birimi dizisi, yalnızca girintisi
değiştirilerek farklı bir ağaca ve farklı bir sonuca nasıl dönüşüyor — ve bir deyim ile bir
ifade arasındaki sınır tam olarak nerede?
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.