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,NLve 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) veINDENTsayı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
INDENTveDEDENTbirimlerine ç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
DEDENTbiriminin 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;
TabErrorbirIndentationError, o da birSyntaxError’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 veINDENTsayı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.