İçeriğe geç
academia.sh

Ders 02 / 16

Sözdizim ve Girinti

Girinti bir yazım tercihi değil dilbilgisinin parçasıdır: aynı 21 içerik birimi iki ayrı girintiyle iki ayrı ağaç ve iki ayrı sonuç üretir, dört ayrı genişlik tek bir ağaç verir ve on parçadan yalnız 5'i ifadedir.

İçindekiler

Önceki ders kaynağın önce bütün olarak derlendiğini gösterdi ve derlemenin ilk adımının metni sözcük birimlerine ayırmak olduğunu söyledi. O adımda bir soru açıkta kaldı: satır başındaki boşluğa ne oluyor?

Ayraçla blok kuran dillerin çoğunda hiçbir şey — boşluk atılır, gövdenin nerede başlayıp nerede bittiğini ayraçlar söyler ve girinti yalnızca okuyucu için konur. Python’da boşluk atılmaz. Girinti (indentation) birim dizisine kendi birimlerini ekler ve blok yapısını tam olarak o birimler kurar. Bunun gözlenebilir sonucu şudur: aynı sözcük birimi dizisi, yalnız girintisi değiştirilerek başka bir ağaca ve başka bir sonuca dönüşür.

Bloğu Ayraç Değil Boşluk Kurar

Birimlere ayırma adımı her satırın başındaki boşluğu ölçer ve bir yığıt üzerinde tutar. Bir satır bir öncekinden daha içerideyse akışa bir INDENT birimi konur; daha dışarıdaysa, kapanan her düzey için bir DEDENT birimi konur. Birden çok düzey aynı anda kapanıyorsa o sayıda DEDENT üretilir, dolayısıyla dizideki DEDENT sayısı kapanan blok sayısını verir. Bu birimlerin kaynak metinde karşılığı olan hiçbir karakter yoktur — boşluğun kendisinden üretilirler.

Aşağıdaki iki kaynak, birimlerin adına bakıldığında ayırt edilemez. Yalnız bir satırın girintisi farklıdır: print çağrısı birinde döngü gövdesinin dışında, ötekinde içindedir.

Ölçümün varsayımları:

  • DT8 — İki kaynak yalnız son satırın girintisiyle ayrılır; hiçbir karakteri eklenmemiş, çıkarılmamış ya da değiştirilmiştir.
  • DT9 — Karşılaştırma birimlerin adları üzerinden yapılır; INDENT, DEDENT, NEWLINE, NL ve dosya sonu birimi “düzen birimi” sayılıp ayrı tutulur, kalanlar “içerik birimi” sayılır.
  • DT10 — Ağaç, çözümlenen yapının en üst düzeyindeki deyimlerin sınıf adlarıyla yazılır; döngü deyiminin gövdesi köşeli ayraç içinde verilir. Ağacın tam dökümü değil, bu ölçüme yeten kabuğu okunur.
  • DT11 — Yürütme çıktısı ayrı bir tampona alınır ve basılan satırların listesi olarak yazılır; ortama bağlı hiçbir değer okunmaz.
"""Ayni sozcuk birimi dizisi, iki ayri girinti: uc gorunum."""
import ast
import contextlib
import io
import tokenize

DIS = "toplam = 0\nfor x in (1, 2, 3):\n    toplam += x\nprint(toplam)\n"
IC = "toplam = 0\nfor x in (1, 2, 3):\n    toplam += x\n    print(toplam)\n"
KAYNAK = {"gövde dışında": DIS, "gövde içinde": IC}
DUZEN = {"INDENT", "DEDENT", "NEWLINE", "NL", "ENDMARKER"}


def birimler(kaynak):
    ak = io.StringIO(kaynak).readline
    return [tokenize.tok_name[t.type] for t in tokenize.generate_tokens(ak)]


def agac(kaynak):
    parcalar = []
    for d in ast.parse(kaynak).body:
        ad = type(d).__name__
        if isinstance(d, ast.For):
            ad += "[" + ",".join(type(g).__name__ for g in d.body) + "]"
        parcalar.append(ad)
    return " ".join(parcalar)


def calistir(kaynak):
    tampon = io.StringIO()
    with contextlib.redirect_stdout(tampon):
        exec(compile(kaynak, "<ders>", "exec"), {})
    return tampon.getvalue().split()


for ad, kaynak in KAYNAK.items():
    tumu = birimler(kaynak)
    icerik = [b for b in tumu if b not in DUZEN]
    print(f"{ad:14s} birim {len(tumu)}  düzen dışı {len(icerik)}  "
          f"INDENT/DEDENT {[i for i, b in enumerate(tumu) if b in ('INDENT', 'DEDENT')]}")

a, b = (birimler(k) for k in KAYNAK.values())
print(f"düzen birimleri atılınca iki dizi eşit mi: "
      f"{[x for x in a if x not in DUZEN] == [x for x in b if x not in DUZEN]}")
print()
for ad, kaynak in KAYNAK.items():
    print(f"{ad:14s} ağaç: {agac(kaynak)}")
    print(f"{'':14s} basılan: {calistir(kaynak)}")
gövde dışında  birim 28  düzen dışı 21  INDENT/DEDENT [16, 21]
gövde içinde   birim 28  düzen dışı 21  INDENT/DEDENT [16, 26]
düzen birimleri atılınca iki dizi eşit mi: True

gövde dışında  ağaç: Assign For[AugAssign] Expr
               basılan: ['6']
gövde içinde   ağaç: Assign For[AugAssign,Expr]
               basılan: ['1', '3', '6']

İki kaynak da 28 birim üretiyor ve bunların 21’i içerik birimi. Düzen birimleri atıldığında kalan iki dizi birebir eşit — adları da sıraları da aynı. Ayrım tek bir birimin yerindedir: DEDENT birinde 21., ötekinde 26. sırada duruyor. Kaynakta değişen tek şey dört boşluktur ve o dört boşluk bir birimi beş sıra öteye taşımıştır.

Bu tek fark ağacı değiştiriyor. Birinci kaynakta çağrı, döngünün kardeşi: ağaçta For[AugAssign] bittikten sonra ayrı bir Expr geliyor. İkincisinde çağrı döngünün çocuğu: For[AugAssign,Expr]. Sonuç da buna göre ayrışıyor — biri tek satır, öteki üç satır basıyor.

Buradan çıkan kural, dile giriş için en pahalı yanlış anlamalardan birini kapatır: girintiyi bozmak bir üslup kusuru değil, programı değiştirmektir. Ayraçlı bir dilde aynı iki kaynak birbirinin biçimlendirilmiş hâli olurdu ve ikisi de aynı şeyi yapardı. Burada girinti tek belirleyicidir; başka hiçbir işaret gövdenin sınırını söylemez.

Genişlik Serbest, Tutarlılık Zorunlu

Girintinin dilbilgisel olması, belli bir genişliğin dilbilgisel olduğu anlamına gelmez. Dil bir sayı dayatmaz; dayattığı şey bir bloğun kendi içinde tutarlı olmasıdır. Sınır gözlenebilir: aynı yapı dört ayrı genişlikle yazılıp ağaçları karşılaştırılabilir, sonra tutarsız dört kaynak derlenmeye çalışılabilir.

  • DT12 — Doğru kaynaklarda genişlik değişir ama blok içinde tutarlı kalır; bozuk kaynaklarda tutarlılık dört ayrı biçimde kırılır. Kaynaklar derlenir, yürütülmez — ölçülen şey derleme aşamasının kabul edip etmediğidir.
"""Girintinin kurali: genislik serbest, tutarlilik zorunlu."""
import ast

GENISLIK = {"iki boşluk": "if 1:\n  a = 1\n  b = 2\n",
            "dört boşluk": "if 1:\n    a = 1\n    b = 2\n",
            "sekiz boşluk": "if 1:\n        a = 1\n        b = 2\n",
            "sekme": "if 1:\n\ta = 1\n\tb = 2\n"}

BOZUK = {"girinti hiç yok": "for x in (1, 2):\nb = 1\n",
         "beklenmeyen girinti": "a = 0\n    b = 1\n",
         "hizalanmayan çıkış": "if 1:\n        a = 1\n    b = 2\n",
         "sekme ile boşluk karışık": "if 1:\n\ta = 1\n        b = 2\n"}


def agac(kaynak):
    d = ast.parse(kaynak).body[0]
    return f"{type(d).__name__}[{','.join(type(g).__name__ for g in d.body)}]"


print("aynı yapı, ayrı genişlik:")
for ad, kaynak in GENISLIK.items():
    print(f"  {ad:12s} ağaç: {agac(kaynak)}")
print(f"  dört ağacın hepsi aynı mı: "
      f"{len({agac(k) for k in GENISLIK.values()}) == 1}")

print("\ntutarsız girinti:")
for ad, kaynak in BOZUK.items():
    try:
        compile(kaynak, "<ders>", "exec")
        print(f"  {ad:24s} derlendi")
    except SyntaxError as e:
        print(f"  {ad:24s} {type(e).__name__}")
print(f"  IndentationError bir SyntaxError mı: "
      f"{issubclass(IndentationError, SyntaxError)}")
print(f"  TabError bir IndentationError mı: "
      f"{issubclass(TabError, IndentationError)}")
aynı yapı, ayrı genişlik:
  iki boşluk   ağaç: If[Assign,Assign]
  dört boşluk  ağaç: If[Assign,Assign]
  sekiz boşluk ağaç: If[Assign,Assign]
  sekme        ağaç: If[Assign,Assign]
  dört ağacın hepsi aynı mı: True

tutarsız girinti:
  girinti hiç yok          IndentationError
  beklenmeyen girinti      IndentationError
  hizalanmayan çıkış       IndentationError
  sekme ile boşluk karışık TabError
  IndentationError bir SyntaxError mı: True
  TabError bir IndentationError mı: True

Dört genişlik tek bir ağaç veriyor. Dilbilgisel olan şey girintinin miktarı değil, yapısı: bir düzeyin açılıp aynı hizada sürmesi ve kapanırken daha önce açılmış bir hizaya oturması. İki boşlukla yazılmış program ile sekiz boşlukla yazılmış program aynı programdır; aralarındaki seçim ekip sözleşmesine kalır, dile değil.

Tutarlılık kırıldığında dördünde de derleme durur. Üçü IndentationError verir: gövdesi hiç açılmayan bir blok, hiçbir şeyi açmadan içeri kayan bir satır ve daha önce açılmış hiçbir hizaya oturmayan bir çıkış. Dördüncüsü ayrılır — sekme ile boşluğun karıştığı kaynak TabError verir, çünkü bu iki karakterin genişliği kaynağa bakarak çözülemez. Çıktının son iki satırı üçünün de tek bir soydan geldiğini gösteriyor: TabError bir IndentationError, o da bir SyntaxError.

Bu soy zinciri önceki dersin sınırını da yerine oturtuyor. Girinti kusurları derleme aşamasında bulunur; hiçbiri yürütmeye kalmaz. Dosyanın son satırındaki bozuk bir girinti, ilk satırdaki atamanın da yapılmamasına yol açar.

Mantıksal Satır ile Fiziksel Satır

Girintinin dilbilgisel olması bir yerde biter ve sınırı bilmek gerekir. Blok yapısını kuran şey mantıksal satırın başındaki boşluktur; ekrandaki her satır başı bir mantıksal satır başlatmaz. Bir ifade açılmış bir ayracın içindeyken satır sonu deyimi bitirmez, akışa NEWLINE yerine NL konur ve o satırların girintisi hiçbir birim üretmez. Aynı süreklilik ters bölü ile de kurulabilir.

  • DT13 — Dört biçim aynı toplamı yazar ve yalnız satırlara bölünüşleriyle ayrılır; ölçülen şey fiziksel satır sayısı, mantıksal satır sonu (NEWLINE), süreklilik satır sonu (NL) ve INDENT sayısıdır. Değer sütunu kaynağın kendi ürettiği toplamdır.
"""Mantiksal satir ile fiziksel satir: ayrac icinde girinti dilbilgisel degildir."""
import ast
import io
import tokenize

BICIM = {
    "tek satır": "toplam = 1 + 2 + 3\n",
    "ayraç içinde hizalı": "toplam = (1 +\n          2 +\n          3)\n",
    "ayraç içinde savruk": "toplam = (1 +\n2 +\n              3)\n",
    "ters bölü ile": "toplam = 1 + \\\n    2 + \\\n    3\n",
}


def say(kaynak):
    ak = io.StringIO(kaynak).readline
    adlar = [tokenize.tok_name[t.type] for t in tokenize.generate_tokens(ak)]
    return adlar.count("NEWLINE"), adlar.count("NL"), adlar.count("INDENT")


print(f"{'biçim':<21s} {'fiziksel':>8s} {'NEWLINE':>7s} {'NL':>3s} {'INDENT':>6s}"
      f"  ağaç     değer")
for ad, kaynak in BICIM.items():
    mantiksal, nl, ind = say(kaynak)
    ortam = {}
    exec(compile(kaynak, "<ders>", "exec"), ortam)
    agac = " ".join(type(d).__name__ for d in ast.parse(kaynak).body)
    print(f"{ad:<21s} {len(kaynak.splitlines()):8d} {mantiksal:7d} {nl:3d} {ind:6d}"
          f"  {agac:8s} {ortam['toplam']}")
biçim                 fiziksel NEWLINE  NL INDENT  ağaç     değer
tek satır                    1       1   0      0  Assign   6
ayraç içinde hizalı          3       1   2      0  Assign   6
ayraç içinde savruk          3       1   2      0  Assign   6
ters bölü ile                3       1   0      0  Assign   6

Dört biçim de tek bir Assign düğümü ve tek bir değer veriyor. Üçü 3 fiziksel satıra yayıldığı hâlde hepsinde NEWLINE sayısı 1: mantıksal satır bir tanedir. Ayraçlı iki biçimde aradaki satır sonları NL olarak, yani “burada bir deyim bitmedi” işaretiyle geçiyor. Ters bölülü biçimde satır sonu birime hiç dönüşmüyor; süreklilik daha metin birimlere ayrılmadan çözülüyor.

En çok söyleyen sütun INDENT: dördünde de 0. “Savruk” biçimin ikinci satırı sütun başından, üçüncüsü çok içeriden başlıyor ve hiçbiri bir şey değiştirmiyor. Demek ki girinti dilbilgisel olmayı ayracın içinde bırakır; orada yapılan hizalama tümüyle okuyucu içindir ve tutarsızlığı bir kusur üretmez. Kural şöyle daraltılabilir: girinti yalnız bir mantıksal satırın başında anlam taşır.

Deyim ve İfade

Girinti bir blok kurar; blok ise deyimlerden oluşur. Bu, dilin ikinci temel ayrımını gerektirir. İfade (expression) bir değer üretir: hesaplanır ve geriye bir nesne bırakır. Deyim (statement) bir iş yapar: bir ad bağlar, bir bloğa girer, akışı yönlendirir; geriye bir değer bırakmaz.

Ayrım sezgiye bırakılmak zorunda değildir, çünkü derleme kipi onu doğrudan söyler. eval kipi yalnız bir ifade kabul eder; exec kipi bir deyim dizisi kabul eder.

  • DT14 — Her parça iki kipte de derlenmeye çalışılır; ölçülen şey kabul edilip edilmediğidir. Değeri yazılan parçalar tek bir adın bağlı olduğu bir ortamda değerlendirilir, dolayısıyla değer sütunu ortama değil o bağlamaya bağlıdır.
"""Deyim mi ifade mi: 'eval' kipi yalniz ifadeyi derler, 'exec' kipi ikisini de."""

PARCALAR = (
    "2 + 3",
    "toplam",
    "len('olcum')",
    "[k for k in (1, 2, 3)]",
    "toplam if toplam else 0",
    "toplam = 5",
    "toplam += 1",
    "if toplam: pass",
    "for k in (1, 2): pass",
    "pass",
)

print(f"{'parça':<26s} {'ifade':>5s} {'deyim':>5s}  değeri")
ifade_sayisi = deyim_sayisi = 0
for p in PARCALAR:
    try:
        kod = compile(p, "<ders>", "eval")
        ifade = True
    except SyntaxError:
        ifade = False
    try:
        compile(p, "<ders>", "exec")
        deyim = True
    except SyntaxError:
        deyim = False
    ifade_sayisi += ifade
    deyim_sayisi += deyim
    deger = repr(eval(kod, {"toplam": 3})) if ifade else "—"
    print(f"{p:<26s} {str(ifade):>5s} {str(deyim):>5s}  {deger}")

print(f"\n{len(PARCALAR)} parçadan ifade {ifade_sayisi}, deyim {deyim_sayisi}; "
      f"yalnız deyim olan {deyim_sayisi - ifade_sayisi}")
parça                      ifade deyim  değeri
2 + 3                       True  True  5
toplam                      True  True  3
len('olcum')                True  True  5
[k for k in (1, 2, 3)]      True  True  [1, 2, 3]
toplam if toplam else 0     True  True  3
toplam = 5                 False  True  —
toplam += 1                False  True  —
if toplam: pass            False  True  —
for k in (1, 2): pass      False  True  —
pass                       False  True  —

10 parçadan ifade 5, deyim 10; yalnız deyim olan 5

On parçanın 5’i ifade, 10’u deyim. Deyim sütununun tamamen dolu olması ayrımın yönünü verir: her ifade aynı zamanda bir deyimdir — tek başına bir satıra konduğunda değeri hesaplanır ve atılır. Tersi geçerli değildir; yalnız deyim olan 5 parça bir ifadenin durabileceği hiçbir yere konamaz.

Ayrımın kurs için doğrudan bir sonucu var. Önceki dersin ölçtüğü on bir biçimin hepsi bir nesneyi tüketiyordu ve tükettikleri şey bir ifadenin değeriydi; özel yöntemler ifadeler üzerinde çalışır. Tabloda toplam += 1 satırının ifade çıkmaması da bu yüzden önemlidir: Programlama Temelleri kursu bileşik atamanın bir deyim olduğunu söylemişti, burada ölçülen şey o deyimin hangi yöntemi çağırdığı ve dönen değeri ne yaptığıdır. __iadd__ çağrısının sonucu ortada bir değer olarak durmaz, doğrudan ada geri bağlanır — ayrıntısı işleçler dersinde ödenecek.

Son bir gözlem tabloda saklıdır: if toplam: pass tek satırda yazıldığı hâlde geçerli bir deyimdir ve hiç girinti kullanmaz. Girinti bir bloğu yazmanın tek yolu değildir; bloğu zorunlu kılan şey deyimin kendisidir, girinti ise birden çok deyimli bir gövdeyi yazmanın yoludur.

Özet

  • Girinti atılmaz; satır başındaki boşluk INDENT ve DEDENT birimlerine çevrilir ve blok yapısını bu birimler kurar.
  • Yalnız girintisiyle ayrılan iki kaynak aynı 21 içerik birimini aynı sırada taşır; tek bir DEDENT biriminin yeri ağacı ve sonucu değiştirir.
  • Girintinin genişliği serbesttir — dört ayrı genişlik tek bir ağaç verir — ama bir blok içinde tutarlı olmak zorundadır.
  • Tutarsız girinti derleme aşamasında durdurur; TabError bir IndentationError, o da bir SyntaxError’dır.
  • Girinti yalnız mantıksal satır başında dilbilgiseldir: açık ayracın içindeki 3 fiziksel satır tek bir NEWLINE üretir ve INDENT sayısı 0 kalır.
  • On parçadan 5’i ifade, 10’u deyimdir: her ifade bir deyim olarak yazılabilir, hiçbir deyim ifadenin yerine konamaz.

Sonraki Adım

Bu derste en sık yazılan deyim olan atama, tabloda yalnızca “ifade değil” olduğu için göründü; ne yaptığına bakılmadı. Programlama Temelleri kursu atamanın bir bağlama olduğunu kurmuştu — ad bir kutu değil, bir nesneye takılan etikettir. Sonraki ders bu bağı nesne tarafından ölçer: iki ad aynı nesneye bağlı olduğunda bunu ne söyler, eşit iki değerin aynı nesne çıkması hangi koşulda gerçekleşir ve bir tipin “değiştirilemez” olması bağlamayı nasıl etkiler?

İ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