Ders 15 / 16
Bağlam Yöneticileri
with bloğu girerken __enter__, çıkarken __exit__ çağırır; istisnasız blokta 2 çağrı olur, istisnalı blokta 3 ve __exit__ istisnayı görür — dönüş değeri True olduğunda çağrı yine 2'ye iner çünkü istisna dışarı çıkmaz.
İçindekiler
Önceki dersin bütün dosya örnekleri with bloğu içinde açıldı ve blok bitince dosya kapandı.
Ölçüm sonunda geçici dizin bile silinebildi, çünkü açık kalan bir tutamak kalmamıştı. with
şimdiye kadar bir alışkanlık olarak kullanıldı.
Oysa with de bir sözdizimdir ve bu kursun kuralı şudur: her sözdizim bir kısaltmadır ve
adı belli bir özel yöntemi çağırır. Bu ders o protokolü ölçer. Kaynak yaşam süresi ve
kaynağın serbest bırakılması İşletim Sistemi Kavramları kursunda kuruldu; orada kavram
kuruldu, burada Python’un onu hangi protokolle gerçekleştirdiği ölçülüyor: with girerken
__enter__, çıkarken __exit__ çağırır — ve blok içinde bir istisna oluştuğunda
__exit__ yine çağrılır.
Protokol İki Yöntemden İbarettir
Bir nesnenin with deyiminde kullanılabilmesi için tek koşul vardır: __enter__ ve
__exit__ tanımlamak. Tipinin adı, hangi sınıftan indiği, ne iş yaptığı önemsizdir. Protokol
yoksa sözdizim de yoktur; protokol varsa nesne with içine girer.
__enter__ argümansızdır ve döndürdüğü değer as ile bağlanan addır. Kaynağın kendisini
döndürmek zorunda değildir; başka bir nesne de döndürebilir.
__exit__ üç argüman alır: oluşan istisnanın türü, nesnesi ve izi. Blok
istisnasız bittiyse üçü de boştur. Ve __exit__ bir değer döndürür; bu dönüş değeri
istisnanın yutulup yutulmayacağına karar verir. Doğru sayılan bir dönüş istisnayı yutar,
yanlış sayılan bir dönüş onu dışarı bırakır.
Ortak kurgunun Izci nesnesi bu iki yöntemi tanımlar ve __exit__ yöntemi False döndürür —
yani yutmaz. Ölçüm buna bir kardeş ekler: Yutan, yalnız dönüş değerini değiştirir.
Ölçümün Kurgusu
Üç sözdizim biçimi çalıştırılır: istisnasız with, istisnalı with, ve istisnalı with ama
yutan bir nesneyle. Her biçimde hangi özel yöntemin kaç kez çağrıldığı sayılır. Ölçüm kaydı
istisnayı da bir olay olarak yazar: bir biçim istisnayla bittiyse kayda ! önekiyle sınıf
adı düşer.
- HD31 —
Izci’nin__enter__ve__exit__yöntemleri ortak kurgudan değiştirilmeden alınır;__exit__yineFalsedöndürür. - HD32 —
Yutanyalnız__exit__dönüşünü değiştirir ve üst sınıfın kaydını bozmadan çağırır; kayıt düzeni ortak kurgunun düzenidir. - HD33 —
Tanikde üst sınıfın kaydını bozmaz; ek olarak__exit__’in gördüğü üçlüyü ayrı bir listeye yazar.Izci’nin davranışına dokunulmaz. - HD34 — Çağrı sayısı kayıttaki öğe sayısıdır;
!ValueErrorda bir öğedir, çünkü ölçülen şey biçimin ürettiği olay dizisidir, yalnız yöntem çağrıları değil. - HD35 — Dönüş değeri sınaması
__exit__doğrudan çağrılarak okunur; yutup yutmadığı ise gerçek birwithbloğuyla ayrıca sınanır. İki yol birbirini doğrular.
"""Baglam yoneticisi protokolu: with girerken ve cikarken hangi yontem cagrilir.""" KAYIT = [] GORULEN = [] def kaydet(ad): KAYIT.append(ad) class Izci: """Ortak tanimin cekirdegi; burada baglam parcasi kullaniliyor.""" def __init__(self, ogeler=(1, 2, 3)): self.ogeler = list(ogeler) def __enter__(self): kaydet("__enter__") return self def __exit__(self, tur, deger, iz): kaydet("__exit__") return False # istisnayi yutmaz class Yutan(Izci): """__exit__ True dondurur: istisna disari cikmaz.""" def __exit__(self, tur, deger, iz): super().__exit__(tur, deger, iz) return True class Tanik(Izci): """__exit__'in gordugu ucluyu ayrica kaydeder; Izci'nin kaydini bozmaz.""" def __init__(self, ad, yutar=False, ogeler=(1, 2, 3)): super().__init__(ogeler) self.ad = ad self.yutar = yutar def __exit__(self, tur, deger, iz): GORULEN.append((self.ad, tur.__name__ if tur else "-", str(deger) if deger else "-", iz is not None)) super().__exit__(tur, deger, iz) return self.yutar 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 baglam(): with Izci(): pass def baglam_istisnali(): with Izci(): raise ValueError("ornek") def baglam_yutan(): with Yutan(): raise ValueError("ornek") BICIMLER = ( ("with n", baglam), ("with n + istisna", baglam_istisnali), ("with yutan + istisna", baglam_yutan), ) 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() print(f"{'sınıf':<10s}{'__exit__ dönüşü':<18s}istisna dışarı çıktı mı") for ad, sinif in (("Izci", Izci), ("Yutan", Yutan)): n = sinif() donus = n.__exit__(ValueError, ValueError("ornek"), None) KAYIT.clear() try: with sinif(): raise ValueError("ornek") except ValueError: cikti = "evet" else: cikti = "hayır" print(f" {ad:<8s}{str(donus):<18s}{cikti}") print() GORULEN.clear() with Tanik("istisnasız"): pass try: with Tanik("istisnalı"): raise ValueError("ölçüm kapsam dışı") except ValueError: pass print(f"{'tanık':<14s}{'gördüğü tür':<14s}{'gördüğü değer':<22s}iz nesnesi var mı") for ad, tur, deger, iz in GORULEN: print(f" {ad:<12s}{tur:<14s}{deger:<22s}{'evet' if iz else 'hayır'}")
sözdizim çağrı protokol dizisi with n 2 __enter__ __exit__ with n + istisna 3 __enter__ __exit__ !ValueError with yutan + istisna 2 __enter__ __exit__ sınıf __exit__ dönüşü istisna dışarı çıktı mı Izci False evet Yutan True hayır tanık gördüğü tür gördüğü değer iz nesnesi var mı istisnasız - - hayır istisnalı ValueError ölçüm kapsam dışı evet
Üç Satırın Söylediği
İlk iki satır ortak kurgunun with satırlarıdır. with n iki çağrı üretir: __enter__
ve __exit__. with n + istisna üç olay üretir: __enter__, __exit__ ve
!ValueError. Aradaki farkı doğru okumak gerekir — fazladan olan bir yöntem çağrısı
değildir. Her iki biçimde de __enter__ bir kez, __exit__ bir kez çağrıldı. Fazladan olan
şey, biçimin istisnayla bitmesidir.
Bu, dersin merkez cümlesini veriyor: istisna oluştuğunda __exit__ yine çağrılır. Blok
yarıda kesildi, kalan satırlar çalışmadı, denetim with deyiminden dışarı fırladı — ve
__exit__ yine de çalıştı. Kaynağın kapanmasını sağlayan şey budur ve with yazmanın
gerekçesi de budur.
Üçüncü satır aynı istisnayı Yutan ile deniyor ve çağrı sayısı 2’ye iniyor. Dizi yine
__enter__ __exit__ — yöntem çağrıları değişmedi. Değişen tek şey, kayda !ValueError
düşmemesi: istisna dışarı çıkmadı. Orta tablo bunu doğruluyor. Izci’nin __exit__
dönüşü False ve istisna dışarı çıktı; Yutan’ın dönüşü True ve çıkmadı. Tek
satırlık bir dönüş değeri, üç olaylık bir biçimi iki olaya indirdi.
Son tablo __exit__’in ne gördüğünü veriyor. İstisnasız blokta üç argümanın da boş
olduğu görülüyor: tür -, değer -, iz hayır. İstisnalı blokta tür ValueError, değer
istisnanın iletisi ve iz nesnesi var. __exit__ istisnayı yalnız yutup yutmamakla
kalmaz; hangi istisna olduğunu bilir ve kararını ona bakarak verebilir. Belli bir sınıfı
yutup ötekileri geçiren bir bağlam yöneticisi bu üçlüyle yazılır.
İç İçe Bloklar ve try/finally
İkinci ölçüm iki soruyu birden yanıtlar. Birincisi: iki bağlam yöneticisi iç içe olduğunda
istisnayı hangisi görür ve içteki yutarsa dıştaki ne görür? İkincisi: with ile try/finally
arasındaki fark ölçülebilir mi?
- HD36 — İç içe ölçümde dört birleşim denenir: dıştaki ve içteki yönetici ayrı ayrı yutan ya da yutmayan olur. Başka hiçbir şey değişmez.
- HD37 — “Gördü” sütunu
__exit__’e geçen tür argümanından okunur; boş geçildiyse-yazılır. - HD38 — Çıkış biçimi tablosunda dört yol denenir: normal bitiş,
return,breakve istisna.returnvebreakiçin blok bir döngü ve bir işlev içine konur, çünkü ikisi de ancak orada anlamlıdır. - HD39 —
withkolu iletry/finallykolu aynı dört yolu dener; aralarındaki tek fark, birinde bağlam yöneticisi, ötekinde birfinallybloğu olmasıdır.
"""Ic ice with, cikis bicimleri ve try/finally ile karsilastirma.""" KAYIT = [] GORULEN = [] def kaydet(ad): KAYIT.append(ad) class Izci: def __init__(self, ogeler=(1, 2, 3)): self.ogeler = list(ogeler) def __enter__(self): kaydet("__enter__") return self def __exit__(self, tur, deger, iz): kaydet("__exit__") return False class Tanik(Izci): """Adiyla kaydeder ve yutup yutmayacagini kurulusta alir.""" def __init__(self, ad, yutar=False, ogeler=(1, 2, 3)): super().__init__(ogeler) self.ad = ad self.yutar = yutar def __exit__(self, tur, deger, iz): GORULEN.append((self.ad, tur.__name__ if tur else "-")) super().__exit__(tur, deger, iz) return self.yutar print(f"{'dış':<10s}{'iç':<9s}{'çağrı':>6s} {'dizi':<40s}" f"{'dış gördü':<12s}{'iç gördü':<12s}istisna") for dis_yutar in (False, True): for ic_yutar in (False, True): KAYIT.clear() GORULEN.clear() try: with Tanik("dış", dis_yutar), Tanik("iç", ic_yutar): raise ValueError("ölçüm kapsam dışı") except ValueError: sonuc = "çıktı" else: sonuc = "yutuldu" gorulen = dict(GORULEN) print(f" {('yutar' if dis_yutar else 'yutmaz'):<8s}" f"{('yutar' if ic_yutar else 'yutmaz'):<9s}{len(KAYIT):6d} " f"{' '.join(KAYIT):<40s}{gorulen['dış']:<12s}" f"{gorulen['iç']:<12s}{sonuc}") print() FINALLY = [] def with_ile(bicim): KAYIT.clear() GORULEN.clear() for _ in range(1): with Tanik("tek"): if bicim == "return": return if bicim == "break": break if bicim == "istisna": raise ValueError("ölçüm kapsam dışı") def finally_ile(bicim): FINALLY.clear() for _ in range(1): try: if bicim == "return": return if bicim == "break": break if bicim == "istisna": raise ValueError("ölçüm kapsam dışı") finally: FINALLY.append("finally") print(f"{'çıkış biçimi':<16s}{'__exit__ çağrıldı':<19s}{'finally çalıştı':<17s}" f"__exit__ istisnayı gördü") for bicim in ("normal", "return", "break", "istisna"): try: with_ile(bicim) except ValueError: pass cagrildi = "evet" if "__exit__" in KAYIT else "hayır" gordu = "evet" if GORULEN and GORULEN[0][1] != "-" else "hayır" try: finally_ile(bicim) except ValueError: pass calisti = "evet" if FINALLY else "hayır" print(f" {bicim:<14s}{cagrildi:<19s}{calisti:<17s}{gordu}")
dış iç çağrı dizi dış gördü iç gördü istisna yutmaz yutmaz 4 __enter__ __enter__ __exit__ __exit__ ValueError ValueError çıktı yutmaz yutar 4 __enter__ __enter__ __exit__ __exit__ - ValueError yutuldu yutar yutmaz 4 __enter__ __enter__ __exit__ __exit__ ValueError ValueError yutuldu yutar yutar 4 __enter__ __enter__ __exit__ __exit__ - ValueError yutuldu çıkış biçimi __exit__ çağrıldı finally çalıştı __exit__ istisnayı gördü normal evet evet hayır return evet evet hayır break evet evet hayır istisna evet evet evet
Sayıların Okunması
İç içe tablonun dört satırında da çağrı sayısı 4 ve dizi aynı:
__enter__ __enter__ __exit__ __exit__. Giriş dıştan içe, çıkış içten dışa. Yutma kararı
hiçbir çağrıyı iptal etmiyor — dört yöntem dört birleşimde de çalışıyor.
Değişen şey gördü sütunlarıdır. İçteki yönetici dört satırda da ValueError görüyor;
istisna ona en yakın yerde oluşuyor ve önce ona geliyor. Dıştaki ise içteki yutmadığında
görüyor, yuttuğunda - alıyor. İkinci ve dördüncü satırlar bunu gösteriyor: içteki yuttuysa
dıştaki için ortada bir istisna kalmamıştır ve __exit__ boş bir üçlüyle çağrılır.
Üçüncü satır tersini gösteriyor: içteki yutmadı, dıştaki gördü ve yuttu. İstisna dışarı çıkmadı. Yutma yetkisi yukarı doğru sırayla dolaşır ve ilk yutan zinciri keser.
İkinci tablo with ile try/finally arasındaki ortak yanı ve farkı ayırıyor. Ortak yan
şudur: dört çıkış biçiminin hepsinde hem __exit__ çağrılıyor hem finally çalışıyor —
blok hangi yolla biterse bitsin temizlik kodu çalışır. Bu yüzden with bir “hata yakalama”
aracı değildir; çıkış aracıdır
ve dört yolun dördünde de devrededir.
Fark son sütundadır. __exit__ yalnız istisna biçiminde istisnayı görüyor; öbür üç
biçimde boş üçlüyle çağrılıyor. finally bloğu ise hiçbir biçimde bir üçlü almaz — çalıştığı
sırada hangi istisnanın uçtuğunu bilmez ve ona bakarak karar veremez. finally içinde
istisnayı yutmanın tek yolu oradan yeni bir deyimle çıkmaktır ve bu, blokta ne olduğuna
bakmadan her istisnayı yutar. __exit__ ise türe bakıp seçebilir.
Seçim buradan çıkar: temizlik tek bir yerde bir kez gerekiyorsa finally yeter. Aynı temizlik
birden çok yerde tekrarlanıyorsa ya da karar istisnanın türüne bağlıysa protokol yazılır.
Bağlanan Nesne ve Temizliğin Kendi Hatası
İki ayrıntı kaldı ve ikisi de protokolün kenarında duruyor. Birincisi as ile bağlanan
addır: bu ad yöneticinin kendisi değil, __enter__’ın döndürdüğü şeydir. İkincisi
__exit__’in kendi kırılmasıdır — temizlik de başarısız olabilir.
- HD40 —
Kapakyalnız__enter__’ın dönüşünü değiştirir; kaydı üst sınıftan alır.Kirilanyalnız temizlik sırasında bir istisna ekler. - HD41 — Bağlanan nesnenin kimliği
isile sınanır; kimlik sayısı basılmaz. - HD42 — Zincir uzunluğu birinci dersteki ölçümle aynı yöntemle sayılır: bastırılmamış bağlam ve neden bağları izlenir.
"""__enter__'in donusu ve __exit__ icinde olusan istisna.""" KAYIT = [] class Izci: def __init__(self, ogeler=(1, 2, 3)): self.ogeler = list(ogeler) def __enter__(self): KAYIT.append("__enter__") return self def __exit__(self, tur, deger, iz): KAYIT.append("__exit__") return False class Kapak(Izci): """__enter__ kendini degil, blogun kullanacagi baska bir nesne dondurur.""" def __enter__(self): super().__enter__() return self.ogeler class Kirilan(Izci): """Temizlik sirasinda kendisi de bir istisna uretir.""" def __exit__(self, tur, deger, iz): super().__exit__(tur, deger, iz) raise RuntimeError("kaynak kapatılamadı") n = Izci() with n as bagli: pass print("Izci: as ile bağlanan nesne yöneticinin kendisi mi:", bagli is n) k = Kapak() with k as bagli: pass print("Kapak: as ile bağlanan nesne yöneticinin kendisi mi:", bagli is k) print("Kapak: as ile bağlanan nesne __enter__'in döndürdüğü mü:", bagli is k.ogeler) print() print(f"{'durum':<36s}{'dışarı çıkan':<15s}{'bağlam':<14s}zincir") for ad, sinif, firlat in (("blok temiz, __exit__ temiz", Izci, False), ("blok istisnalı, __exit__ temiz", Izci, True), ("blok temiz, __exit__ kırılan", Kirilan, False), ("blok istisnalı, __exit__ kırılan", Kirilan, True)): try: with sinif(): if firlat: raise ValueError("ölçüm kapsam dışı") except BaseException as d: cikan = type(d).__name__ baglam = type(d.__context__).__name__ if d.__context__ else "-" uzunluk, halka = 0, d while halka is not None: uzunluk += 1 halka = halka.__cause__ or (None if halka.__suppress_context__ else halka.__context__) else: cikan = baglam = "-" uzunluk = 0 print(f" {ad:<34s}{cikan:<15s}{baglam:<14s}{uzunluk}")
Izci: as ile bağlanan nesne yöneticinin kendisi mi: True Kapak: as ile bağlanan nesne yöneticinin kendisi mi: False Kapak: as ile bağlanan nesne __enter__'in döndürdüğü mü: True durum dışarı çıkan bağlam zincir blok temiz, __exit__ temiz - - 0 blok istisnalı, __exit__ temiz ValueError - 1 blok temiz, __exit__ kırılan RuntimeError - 1 blok istisnalı, __exit__ kırılan RuntimeError ValueError 2
İlk üç satır as bağlamasını çözüyor. Izci kendini döndürdüğü için bağlanan ad
yöneticinin kendisi. Kapak başka bir nesne döndürüyor ve bağlanan ad artık yönetici
değil. Bir dosyayı with ile açtığınızda as ile bağlanan şeyin neden dosya nesnesi
olduğunu bu açıklar: __enter__ onu döndürür.
Alt tablo temizliğin kendi hatasını sayıyor ve birinci dersin zincir ölçümüyle birleşiyor.
Blok da __exit__ de temizse dışarı hiçbir şey çıkmıyor, zincir 0. Yalnız blok
kırıldığında ValueError çıkıyor, zincir 1. Yalnız __exit__ kırıldığında RuntimeError
çıkıyor, zincir yine 1.
Son satır tehlikeli olanıdır. Blok ValueError fırlatmış, __exit__ çağrılmış ve o da
kırılmış. Dışarı çıkan RuntimeError — yani temizliğin hatası, alanın hatasının
yerine geçiyor. Asıl hata kaybolmuyor: __context__ alanında duruyor ve zincir 2. Ama
dışarıdaki except ValueError klozu artık tutmaz, çünkü uçan sınıf değişti. Bu yüzden
__exit__ içindeki iş kırılmamalıdır; kırılabilecekse kendi içinde ele alınmalıdır.
Özet
withbir kısaltmadır: girerken__enter__, çıkarken__exit__çağırır ve protokolü tanımlamayan nesnewithdeyimine giremez.- İstisnasız blok 2 olay üretir, istisnalı blok 3; fazladan olan bir yöntem çağrısı
değil, biçimin istisnayla bitmesidir —
__exit__her ikisinde de bir kez çağrılır. __exit__üç argüman alır ve istisnalı blokta türü, nesneyi ve izi görür; dönüş değeri doğru sayıldığında istisna yutulur ve olay sayısı 3’ten 2’ye iner.- İç içe iki yöneticide çağrı sayısı dört birleşimde de 4’tür; içteki istisnayı hep görür, dıştaki ancak içteki yutmadığında görür.
__exit__vefinallydört çıkış biçiminin hepsinde çalışır; ayrıldıkları yer,__exit__istisnanın türünü görüp seçebilirkenfinally’nin görememesidir.asile bağlanan ad yöneticinin kendisi değil__enter__’ın dönüşüdür;__exit__kendi kırılırsa dışarı temizliğin hatası çıkar, asıl hata bağlamda kalır ve zincir 2 olur.
Sonraki Adım
Bu kursun ölçtüğü son sözdizim biçimi geride kaldı. Şimdiye kadar her ölçüm, dilin kendi
protokollerini çağıran bir deyim ya da ifadeydi: for, if, +, with. Sonraki dersin
konusu ise dilin sözdizimi değil, standart kitaplığın sunduğu ayrı bir küçük dildir:
düzenli ifadeler. Orada örüntü bir dizgide yazılır, ayrı bir kural kümesine göre yorumlanır ve
eşleşme bir nesne olarak geri döner. Ölçülecek şey o nesnenin ne taşıdığı ve örüntüdeki
parantezlerin hangi sırayla numaralandığıdır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.