İçeriğe geç
academia.sh

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ırn 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.
  • DT3Izci üç öğeyle kurulur; döngü ölçümünde okunacak çağrı sayısı bu üçe bağlıdır.
  • DT4__exit__ her koşumda False dö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. Izci her ö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 n tek başına 5 çağrı üretir.
  • Üç öğelik nesnede __next__ 4 kez çağrılır ve sonuncusu StopIteration fı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ı TypeError ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat