İçeriğe geç
academia.sh

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.

  • HD31Izci’nin __enter__ ve __exit__ yöntemleri ortak kurgudan değiştirilmeden alınır; __exit__ yine False döndürür.
  • HD32Yutan yalnı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.
  • HD33Tanik de ü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; !ValueError da 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 bir with bloğ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, break ve istisna. return ve break için blok bir döngü ve bir işlev içine konur, çünkü ikisi de ancak orada anlamlıdır.
  • HD39with kolu ile try/finally kolu aynı dört yolu dener; aralarındaki tek fark, birinde bağlam yöneticisi, ötekinde bir finally bloğ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.

  • HD40Kapak yalnız __enter__’ın dönüşünü değiştirir; kaydı üst sınıftan alır. Kirilan yalnız temizlik sırasında bir istisna ekler.
  • HD41 — Bağlanan nesnenin kimliği is ile 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

  • with bir kısaltmadır: girerken __enter__, çıkarken __exit__ çağırır ve protokolü tanımlamayan nesne with deyimine 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__ ve finally dört çıkış biçiminin hepsinde çalışır; ayrıldıkları yer, __exit__ istisnanın türünü görüp seçebilirken finally’nin görememesidir.
  • as ile 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat