İçeriğe geç
academia.sh

Ders 12 / 16

Dekoratörler

Sarmalama özgün işlevi silmez, üstüne bir katman koyar: tek dekoratör iki katman kurar, iki dekoratör üç, ve meta veri korunmadığında işlevin adı, belgesi ve imzası sarmalayıcınınkiyle değişir.

İçindekiler

Önceki derste işlevler hep argüman olarak geçti ve hiçbiri değiştirilmedi. Ama aynı ders bir işlevin döndürülebildiğini de ölçtü: carpan_ureten her çağrıda yeni bir işlev nesnesi üretiyordu. İki yönü birleştirmek mümkün — bir işlevi alan ve onun yerine geçecek başka bir işlev döndüren bir işlev.

Böyle bir işleve dekoratör (decorator) denir ve döndürdüğü nesneye sarmalayıcı (wrapper) denir. Bu dersin soruları şunlardır: sarmalamak kaç nesne var eder, özgün işleve ne olur, ve sarmalanmış işlevin __name__ özniteliği ne söyler? Önceki dersin önemsiz görünen tek sütunu — def ile lambda arasındaki ad farkı — burada bir sonuç üretecek.

Sarmalayıcı Kalıbı

Kalıp üç adımlıdır. Dekoratör bir işlev alır; gövdesinde yeni bir işlev tanımlar; o yeni işlevi döndürür. İçteki işlev çağrıldığında kendi işini yapar ve aldığı işlevi de çağırır — böylece özgün davranış korunur, çevresine bir şey eklenir.

Adın önüne @ konarak yazılan biçim bir kısaltmadır. @dekorator satırı bir işlev tanımının üstüne konduğunda, tanım bittikten sonra hesapla = dekorator(hesapla) yapılır. Yani ortada yeni bir düzenek yoktur; önceki dersin yüksek mertebeden fonksiyonu, sonucu aynı ada geri bağlanmıştır.

Buradan önemli bir sonuç çıkar: özgün işlev yok edilmez. Ad artık sarmalayıcıya bağlıdır, ama sarmalayıcının gövdesi özgün nesneye başvurur ve onu canlı tutar. Sarmalama bir silme değil, bir katman eklemedir — ve katmanlar sayılabilir.

İki Ayrı Zaman

Sarmalama iki ayrı anda iş yapar ve karıştırılmaları sık görülür.

Tanım anı: dekoratörün gövdesi çalışır ve sarmalayıcıyı üretir. Bu bir kez olur — işlevin tanımlandığı yerde, hiç çağrılmasa bile.

Çağrı anı: sarmalayıcının gövdesi çalışır. Bu her çağrıda olur.

Ayrımın ölçülebilir sonucu şudur: dekoratör gövdesindeki iş kaç işlev sarmalanmışsa o kadar kez, sarmalayıcı gövdesindeki iş kaç çağrı yapılmışsa o kadar kez yapılır.

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

  • YN45 — Ortak tanımın Oge sınıfı sayma düzeniyle aynen kullanılır; dekoratör gövdesi, sarmalayıcı ve özgün gövde ayrı sayaçlarda tutulur. Kâhin kurgunun kendisidir.
  • YN46 — Katman sayısı __wrapped__ zinciri izlenerek bulunur; bu zincir yalnız meta veri koruma kullanıldığında kurulur.
  • YN47 — Dört yazımın dördü de aynı özgün işlevi sarmalar ve dördü de bir kez çağrılır. Sayaçlar ölçümden önce sıfırlanır.
  • YN48 — Parametreli dekoratör özgün gövdeyi kasıtlı olarak üç kez çağırır; sayı keyfîdir ve sarmalayıcının davranış eklediğini görünür kılmak için seçilmiştir.
  • YN49 — Meta veri ölçümünde üç yazım karşılaştırılır: sarmasız, koruma kullanmayan ve kullanan. Kimlik is ile gösterilir, sayısı basılmaz.
  • YN50 — Belge sütunu belgenin varlığını sorar, içeriğini değil; imza sütunu standart kitaplığın inceleme aracından alınır.
  • YN51 — Not alma ölçümünde yirmi istek beş ayrı değere düşer; istek dizisi sabittir ve yinelenen isteklerin oranı ölçümün kendisidir.
  • YN52 — Sıra ölçümünde iki dekoratör aynı dekoratördür ve yalnız etiketleriyle ayrılır; iz kaydı giriş ve çıkış noktalarını ayrı yazar, böylece sıra okunabilir.
  • YN53 — Dar imzalı sarmalayıcı kasıtlı olarak yalnız tek konumsal argüman kabul eder; ölçülen şey çağrının başarısız olduğu ve hangi istisna sınıfının doğduğudur.

Ölçüm

"""Dekorator: sarmalama kac katman kurar ve meta veri korunmazsa ne kaybolur."""

import functools
import inspect

SAYAC = {"uretilen": 0, "dekorator": 0, "sarmalayici": 0, "govde": 0}


class Oge:
    """Her uretimi sayan oge."""

    def __init__(self, deger):
        SAYAC["uretilen"] += 1
        self.deger = deger

    def __repr__(self):
        return f"Oge({self.deger})"


def sifirla():
    for k in SAYAC:
        SAYAC[k] = 0


def kaba(islev):
    """Meta veriyi korumayan sarmalayici."""
    SAYAC["dekorator"] += 1

    def sarmalayici(*a, **k):
        SAYAC["sarmalayici"] += 1
        return islev(*a, **k)
    return sarmalayici


def korumali(islev):
    """functools.wraps ile meta veriyi tasiyan sarmalayici."""
    SAYAC["dekorator"] += 1

    @functools.wraps(islev)
    def sarmalayici(*a, **k):
        SAYAC["sarmalayici"] += 1
        return islev(*a, **k)
    return sarmalayici


def yineleyen(kere):
    """Parametreli dekorator: once fabrika cagrilir, sonra dekorator."""
    def dekorator(islev):
        SAYAC["dekorator"] += 1

        @functools.wraps(islev)
        def sarmalayici(*a, **k):
            SAYAC["sarmalayici"] += 1
            for _ in range(kere):
                sonuc = islev(*a, **k)
            return sonuc
        return sarmalayici
    return dekorator


def hesapla(n: int):
    """Verilen sayidan bir oge uretir."""
    SAYAC["govde"] += 1
    return Oge(n)


def katman(islev):
    sayi = 1
    while hasattr(islev, "__wrapped__"):
        islev, sayi = islev.__wrapped__, sayi + 1
    return sayi


def cekirdek(islev):
    while hasattr(islev, "__wrapped__"):
        islev = islev.__wrapped__
    return islev


sifirla()
sarmasiz = hesapla
tekli = korumali(hesapla)
ikili = korumali(korumali(hesapla))
parametreli = yineleyen(3)(hesapla)
tanimda = SAYAC["dekorator"]

print(f"{'yazım':<24s} {'katman':>7s} {'çekirdek özgün mü':>18s}"
      f" {'gövde çağrısı':>14s}")
for ad, f in (("sarmasız", sarmasiz), ("tek dekoratör", tekli),
              ("iki dekoratör", ikili), ("parametreli dekoratör", parametreli)):
    SAYAC["govde"] = 0
    f(7)
    print(f"  {ad:<22s} {katman(f):7d}"
          f" {str(cekirdek(f) is hesapla):>18s} {SAYAC['govde']:14d}")

print()
print(f"dört yazım kurulurken çalışan dekoratör gövdesi: {tanimda};"
      f" her biri bir kez çağrılınca çalışan sarmalayıcı:"
      f" {SAYAC['sarmalayici']}")
print(f"tekli is hesapla -> {tekli is hesapla},"
      f" tekli.__wrapped__ is hesapla -> {tekli.__wrapped__ is hesapla}")

print()
print(f"{'yazım':<16s} {'__name__':>12s} {'belge var mı':>13s} {'imza':>18s}")
for ad, f in (("sarmasız", hesapla), ("wraps yok", kaba(hesapla)),
              ("wraps var", korumali(hesapla))):
    print(f"  {ad:<14s} {f.__name__:>12s} {str(bool(f.__doc__)):>13s}"
          f" {str(inspect.signature(f)):>18s}")


@functools.cache
def pahali(n):
    SAYAC["govde"] += 1
    return Oge(n % 5)


sifirla()
istekler = [i % 5 for i in range(20)]
for i in istekler:
    pahali(i)
print()
print(f"not alma: {len(istekler)} istek, gövde çağrısı {SAYAC['govde']},"
      f" var edilen {SAYAC['uretilen']}, önbellekte tutulan"
      f" {pahali.cache_info().currsize}")

IZ = []


def izleyen(etiket):
    def dekorator(islev):
        @functools.wraps(islev)
        def sarmalayici(*a, **k):
            IZ.append(f"{etiket} giriş")
            sonuc = islev(*a, **k)
            IZ.append(f"{etiket} çıkış")
            return sonuc
        return sarmalayici
    return dekorator


@izleyen("dış")
@izleyen("iç")
def ustten(n):
    IZ.append("gövde")
    return Oge(n)


@izleyen("iç")
@izleyen("dış")
def alttan(n):
    IZ.append("gövde")
    return Oge(n)


print()
for ad, f in (("@dış @iç", ustten), ("@iç @dış", alttan)):
    IZ.clear()
    f(1)
    print(f"  {ad:<10s} {' -> '.join(IZ)}")


def dar(islev):
    @functools.wraps(islev)
    def sarmalayici(n):
        return islev(n)
    return sarmalayici


@dar
def iki_argumanli(n, k=2):
    return n * k


try:
    iki_argumanli(3, k=5)
except TypeError as e:
    print(f"dar imzalı sarmalayıcı anahtarlı argümanla: {type(e).__name__}")
print(f"geniş imzalı sarmalayıcı aynı çağrıda:"
      f" {izleyen('x')(lambda n, k=2: n * k)(3, k=5)}")
yazım                     katman  çekirdek özgün mü  gövde çağrısı
  sarmasız                     1               True              1
  tek dekoratör                2               True              1
  iki dekoratör                3               True              1
  parametreli dekoratör        2               True              3

dört yazım kurulurken çalışan dekoratör gövdesi: 4; her biri bir kez çağrılınca çalışan sarmalayıcı: 4
tekli is hesapla -> False, tekli.__wrapped__ is hesapla -> True

yazım                __name__  belge var mı               imza
  sarmasız            hesapla          True           (n: int)
  wraps yok       sarmalayici         False          (*a, **k)
  wraps var           hesapla          True           (n: int)

not alma: 20 istek, gövde çağrısı 5, var edilen 5, önbellekte tutulan 5

  @dış @iç   dış giriş -> iç giriş -> gövde -> iç çıkış -> dış çıkış
  @iç @dış   iç giriş -> dış giriş -> gövde -> dış çıkış -> iç çıkış
dar imzalı sarmalayıcı anahtarlı argümanla: TypeError
geniş imzalı sarmalayıcı aynı çağrıda: 15

Katmanların Sayısı

Üst tabloda katman sütunu doğrudan sayıyor: sarmasız işlev 1, tek dekoratör 2, iki dekoratör 3. Her sarmalama zincire bir halka ekliyor ve halka sayısı dekoratör sayısının bir fazlası.

İkinci sütun daha önemli olanı söylüyor: her satırda zincirin dibindeki nesne özgün işlevin kendisi. Sarmalama özgün nesneyi ne değiştiriyor ne yok ediyor; onu bir başvurunun ucunda canlı tutuyor. Alttaki iki satır bunu doğrudan gösteriyor: tekli is hesapla yanlış — ad artık başka bir nesneye bağlı — ama tekli.__wrapped__ is hesapla doğru. Yeni nesne var edildi, eski nesne paylaşıldı.

Kursun ekseninden bakınca sarmalama bir kopyalama değil, bir kap kurmadır: dış nesne yeni, içindeki eski. Birinci konunun dilimleme ölçümüyle aynı örüntü, bu kez işlevler üzerinde.

Dördüncü satır katmanın davranış eklediğini gösteriyor: parametreli dekoratörle sarmalanmış işlev bir kez çağrıldığında özgün gövde 3 kez çalışıyor. Katman sayısı yine 2 — parametre bir katman eklemiyor, yalnız katmanın davranışını belirliyor. Parametreli dekoratör üç aşamalıdır: önce fabrika çağrılır ve dekoratörü üretir, sonra dekoratör çağrılır ve sarmalayıcıyı üretir, sonra sarmalayıcı çağrılır.

Sayının maliyet tarafı da doğrudan okunur. Her sarmalama bir işlev nesnesi var eder ve özgün nesneyi canlı tutar; iki dekoratörle sarmalanmış bir işlev için bellekte 3 işlev nesnesi durur ve her çağrı üç çerçeve açar. Bir işlevi sarmalamak ucuzdur, ama ücretsiz değildir ve ücret katman başına ödenir.

Sonraki satır iki zamanı ayırıyor. Dört yazım kurulurken dekoratör gövdesi 4 kez çalıştı — sarmalanan her işlev için bir kez, hiçbiri henüz çağrılmadan. Sarmalayıcılar ise ancak çağrılar yapıldığında çalıştı. Tanım anındaki iş bir kez ödenir; çağrı anındaki iş her çağrıda.

Kaybolan Meta Veri

Orta tablo dersin ikinci sorusunu yanıtlıyor ve orta satır kaybı gösteriyor.

Meta veri koruma kullanılmadığında sarmalanmış işlevin __name__ özniteliği sarmalayıcı oluyor, belgesi kayboluyor ve imzası (*a, **k) görünüyor. Üç bilgi de özgün nesnede duruyor, ama artık kimse ona bakmıyor: ada bağlı olan nesne sarmalayıcıdır ve bu üç özniteliği kendisininkilerdir.

Kaybın nerede ödendiği önemlidir. Ad hata izlerinde ve günlük kayıtlarında görünür; on ayrı işlev aynı dekoratörle sarmalanmışsa onunun da adı aynı olur ve iz hangi işlevin çalıştığını söyleyemez. Belge çevrimiçi yardımda ve belge üretiminde okunur. İmza ise en sinsi olanıdır: (*a, **k) bir imza değil, imzanın yokluğudur — hangi argümanların beklendiğini inceleyen hiçbir araç doğru yanıtı alamaz.

Alt satır düzeltmeyi gösteriyor. Standart kitaplığın wraps aracı bir dekoratördür ve sarmalayıcıya uygulandığında özgün nesnenin ad, belge ve imza bilgisini ona kopyalar; ayrıca __wrapped__ bağını kurar. Üst tablodaki katman sayımı tam olarak bu bağ sayesinde mümkün oldu — koruma yoksa zincir izlenemez ve sarmalanmış bir işlevin altında ne olduğu dışarıdan görülemez.

Ayrım tek cümleyle şudur: sarmalayıcı özgün davranışı kendiliğinden korur, çünkü onu çağırır; özgün kimliğini korumaz, çünkü o ayrıca taşınmalıdır.

Tutmanın Kazandırdığı

Son satır dekoratörün en yaygın kullanımlarından birini ölçüyor ve kursun eksenini tersine çeviriyor. Yirmi istek yapılıyor, bunlar beş ayrı değere düşüyor; özgün gövde yalnız 5 kez çalışıyor, var edilen 5 ve önbellekte tutulan 5.

Sarmasız hâlde yirmi istek yirmi gövde çağrısı ve yirmi nesne demekti. Sarmalayıcı, gördüğü argümanlar için sonucu saklayarak üretimi dörtte bire indiriyor. Programlama Temelleri kursu not almayı bir teknik olarak kurmuştu; buradaki katkı onun sayısını vermektir — ve sayı iki sütunda birden okunmalıdır.

Üretim 20’den 5’e indi, ama tutulan 0’dan 5’e çıktı. Üçüncü ders tembelliğin tutmayı azalttığını ölçmüştü; bu satır tam tersini yapıyor: tutmayı artırarak üretimi azaltıyor. İki ölçüm birlikte kursun ekseninin son biçimini veriyor — var etmek ile tutmak arasında bir takas vardır ve hangi yönde durulacağı işin kendisine bağlıdır. Bir kez kullanılıp atılacak bin öğe için tutmamak doğrudur; yirmi kez sorulacak beş değer için tutmak doğrudur.

Önbelleğin sınırsız büyümesi bu takasın açık ucudur: tutulan sayı ayrı argüman sayısı kadar büyür ve kimse onu düşürmez. Standart kitaplık bu yüzden sınırlı boyutlu bir biçim de sunar; sınır konduğunda tutulan sabitlenir ve gövde çağrısı yeniden artar.

Sıra ve İmza

Son dört satır sarmalamanın iki pratik ayrıntısını ölçüyor.

İki dekoratör üst üste yazıldığında sıra sonucu değiştirir. @dış üstte olduğunda iz şöyle ilerliyor: dış giriş, iç giriş, gövde, iç çıkış, dış çıkış. Sıra ters çevrildiğinde iz de tersine dönüyor. Kural şudur: en alttaki dekoratör önce uygulanır ve zincirin en içine düşer; en üstteki en son uygulanır ve en dışta durur. Çağrı sırası bunun tersidir — en dış katman ilk çalışır, gövdeye en son ulaşılır ve dönüşte katmanlar ters sırayla kapanır.

Ayrım önemsiz değildir. Bir katman erişim denetimi yapıyor, bir başkası günlük tutuyorsa, hangisinin dışta olduğu reddedilen bir çağrının kaydedilip kaydedilmeyeceğini belirler. İki yazım aynı görünür; iz farklıdır.

Alttaki iki satır sarmalayıcının imzasının neden geniş yazıldığını gösteriyor. Yalnız tek konumsal argüman kabul eden bir sarmalayıcı, özgün işlev anahtarlı argüman alsa bile o çağrıyı geçiremiyor ve TypeError doğuyor. Aynı çağrı, argümanları olduğu gibi aktaran geniş imzalı sarmalayıcıda çalışıyor ve 15 veriyor.

Bunun nedeni sarmalayıcının artık çağrı yüzeyi olmasıdır: dışarıdan gelen her argüman biçimi önce ona uğrar. Özgün işlevin kabul ettiği her biçimi kabul etmiyorsa, kabul etmediklerini geçiremez. Değişken sayıda argüman toplama biçimi bu yüzden bir üslup tercihi değil, sarmalamanın gereğidir — ve önceki tablodaki (*a, **k) imzası tam da bunun izidir. Koruma kullanılmadığında dışarıya görünen imza bu geniş biçimdir; koruma kullanıldığında görünen imza özgün işlevinkidir, ama çağrıyı gerçekten karşılayan yine geniş biçimdir.

Özet

  • Dekoratör bir işlev alıp yerine geçecek başka bir işlev döndürür; @ yazımı hesapla = dekorator(hesapla) bağlamasının kısaltmasıdır.
  • Sarmalama özgün nesneyi yok etmez, katman ekler: sarmasız 1, tek dekoratör 2, iki dekoratör 3 katman. Ada bağlı nesne değişir (tekli is hesapla yanlış) ama özgün nesne paylaşılır (tekli.__wrapped__ is hesapla doğru).
  • Dekoratör gövdesi tanım anında ve sarmalanan işlev başına bir kez çalışır; sarmalayıcı çağrı anında ve her çağrıda çalışır. Parametreli dekoratör üç aşamalıdır ve katman sayısını artırmaz.
  • Meta veri korunmadığında ad sarmalayici, belge boş ve imza (*a, **k) olur; koruma kullanıldığında üçü de özgün değerini alır ve __wrapped__ bağı kurulur. Sarmalayıcı davranışı kendiliğinden korur, kimliği ayrıca taşınmalıdır.
  • Not alan sarmalayıcı yirmi isteği 5 gövde çağrısına indirir ve karşılığında 5 öğe tutar: üretim düşer, tutma yükselir. Takasın yönü işin kendisine bağlıdır.
  • En alttaki dekoratör önce uygulanır ve en içte durur; çağrı sırası tersinedir. Sarmalayıcı bir çağrı yüzeyi olduğu için argümanları olduğu gibi aktarmalıdır — dar imzalı bir sarmalayıcı, özgün işlevin kabul ettiği bir çağrıda TypeError doğurur.

Sonraki Adım

Buraya kadar her şey tek bir dosyanın içindeydi. Ölçülen her sınıf, her üreteç, her dekoratör aynı gövdede tanımlandı ve aynı gövdede kullanıldı; bir adın nereye yazıldığı sorusu hiç sorulmadı, çünkü yazılacak tek bir yer vardı. Oysa bu dersin sarmalayıcısı başka bir dosyada tanımlanmış bir işlevi de sarmalayabilirdi. Kod birden çok dosyaya yayıldığında adlar nereye yazılır, bir dosya bir başkasının adlarını nasıl görür, ve aynı dosya iki kez istendiğinde iki kez mi okunur? Sonraki konu bunun düzeneğini kurar.

İ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