İçeriğe geç
academia.sh

Ders 05 / 11

Özellikler ve Betimleyiciler

Aynı yedi erişim doğrudan alanda 0 gövde çalıştırır ve geçersiz değeri geçirir, araya giren tanımda 7 gövde çalıştırır ve düşürür; betimleyici veriyi kendi üzerinde tutarsa üç örnek tek değeri paylaşır.

İçindekiler

Önceki ders sözdizime katılan tarafı yazdı: len(n) bir gövde çalıştırdı, n[0] bir gövde çalıştırdı. Öznitelik okumak ise hiçbir gövde çalıştırmadı — n.ogeler bir sözlük aramasıydı ve araya kimse girmedi. Üçüncü dersin bıraktığı açık tam buradaydı: değişmezi koruyan gövdeye uğramayan yazma denetimsiz kalıyordu ve adlandırma bunu engellemiyordu.

Bu ders erişimin kendisini yakalamayı ölçer. Ölçünün ekseni değişmiyor: sorulan şey yine çağrıyı kimin yanıtladığıdır, ama bu kez çağrı bir yöntem çağrısı gibi görünmüyor — bir alan okuması gibi görünüyor. Bir öznitelik okuması ve yazması da bir gövdeye uğratılabilir; uğratıldığında yazımı değişmez, bedeli değişir. Üç soru sorulur: aynı erişim dizisi iki tasarımda kaç gövde çalıştırır, araya giren kod verisini nerede tutar, ve erişimi yakalayan iki kancadan hangisi kaç kez çalışır?

Erişim Kaç Gövdeye Uğradı

Üç sınıf aynı tek alanı sunar ve dışarıdan üçü de n.deger yazımıyla kullanılır. Dogrudan alanı çıplak tutar. Ozellikli alanı bir özellik (property) olarak tanımlar: okuma ve yazma için birer gövde yazar. Betimleyicili aynı denetimi bir betimleyiciye (descriptor) devreder — erişim mantığını kendi sınıfında taşıyan, sınıf gövdesinde bir kez var edilen bir nesneye.

Üç sınıfa aynı iş verilir: değeri üç kez bir artırmak ve sonra geçersiz bir değer yazmayı denemek.

  • SN52 — Üç sınıfın alanı da deger adıyla sunulur; çağıran taraf üç sınıfta da aynı satırları yazar.
  • SN53 — Ölçülen iş yedi erişimdir: üç artırma her turda bir okuma ve bir yazma üretir, sonuncu satır bir yazma daha ekler. Kurulumda oluşan kayıtlar sayıma girmez.
  • SN54 — Geçersiz değer bilerek negatiftir; denetim yazan gövdenin içindedir ve ValueError fırlatır. Denetim yoksa değer olduğu gibi yazılır.
  • SN55 — “Son değer” sütunu ölçüm bittikten sonra alandan okunan değerdir; bu okuma sayıma dahil değildir.
"""Erisim kac kez araya giren koda ugradi: dogrudan alan, ozellik, betimleyici."""

KAYIT = []


class Dogrudan:
    """Alan ciplaktir; erisim hicbir govdeye ugramaz."""

    def __init__(self):
        self.deger = 0


class Ozellikli:
    """Erisim araya giren iki govdeye ugrar."""

    def __init__(self):
        self._deger = 0

    @property
    def deger(self):
        KAYIT.append("okuma")
        return self._deger

    @deger.setter
    def deger(self, yeni):
        KAYIT.append("yazma")
        if yeni < 0:
            raise ValueError("deger negatif olamaz")
        self._deger = yeni


class Alan:
    """Betimleyici: erisim mantigini kendi sinifinda tasir."""

    def __set_name__(self, sahip, ad):
        self.ad = "_" + ad

    def __get__(self, nesne, sahip=None):
        KAYIT.append("okuma")
        return getattr(nesne, self.ad)

    def __set__(self, nesne, yeni):
        KAYIT.append("yazma")
        if yeni < 0:
            raise ValueError("deger negatif olamaz")
        setattr(nesne, self.ad, yeni)


class Betimleyicili:
    """Ayni denetimi bir betimleyiciye devreder."""

    deger = Alan()

    def __init__(self):
        self.deger = 0


def olc(sinif):
    KAYIT.clear()
    n = sinif()
    KAYIT.clear()
    for _ in range(3):
        n.deger = n.deger + 1
    try:
        n.deger = -1
    except ValueError as e:
        hata = type(e).__name__
    else:
        hata = "yok"
    return list(KAYIT), n.deger, hata


print(f"{'sınıf':<14s} {'okuma':>6s} {'yazma':>6s} {'toplam gövde':>13s} "
      f"{'son değer':>10s} {'negatif yazma':>14s}")
for sinif in (Dogrudan, Ozellikli, Betimleyicili):
    kayit, son, hata = olc(sinif)
    print(f"{sinif.__name__:<14s} {kayit.count('okuma'):>6d} "
          f"{kayit.count('yazma'):>6d} {len(kayit):>13d} {son:>10d} {hata:>14s}")
sınıf           okuma  yazma  toplam gövde  son değer  negatif yazma
Dogrudan            0      0             0         -1            yok
Ozellikli           3      4             7          3     ValueError
Betimleyicili       3      4             7          3     ValueError

Aynı satırlar, üç ayrı bedel. Dogrudan yedi erişimde 0 gövde çalıştırıyor; erişim doğrudan örneğin sözlüğüne gidiyor ve dönüyor. Ozellikli ve Betimleyicili aynı yedi erişimde 7 gövde çalıştırıyor — üç okuma, dört yazma. Sayı erişim sayısının aynısıdır, çünkü araya giren kod her erişimde çalışır, ilkinde ya da bazılarında değil.

Bedelin karşılığı son iki sütunda. Denetimsiz alanda negatif yazma geçti ve son değer -1 oldu; sınıf geçersiz bir durumda kaldı ve kimse bunu bilmiyor. Araya giren gövdenin olduğu iki sınıfta aynı yazma ValueError ile düştü ve son değer 3’te, yani son geçerli değerinde kaldı. Önceki dersin bıraktığı açık burada kapanıyor: değişmezi koruyan tek yer bir gövdedir, ve n.deger = -1 yazımını o gövdeye uğratmanın yolu alanı bir tanımın arkasına almaktır.

Bu tasarımın en önemli özelliği çağıran tarafta hiçbir şeyin değişmemesidir. Üç sınıf da n.deger yazımıyla kullanılıyor; alanın çıplak mı yoksa iki gövdeyle mi karşılandığı çağıran taraftan görünmüyor. Bir alanı sonradan denetim altına almak, o alanı kullanan hiçbir satırı değiştirmeden yapılabilir. Alanları önden getir–koy yöntemlerinin arkasına saklamanın gereksiz olmasının nedeni budur: çıplak alan, gerektiğinde arkasına gövde konabilecek bir alandır.

İki tanım aynı sonucu veriyorsa aralarındaki seçim nedir? Özellik gövdelerini sınıfın kendi içinde tutar; tek bir sınıfa özgü bir denetim için doğrudan yazımdır. Betimleyici mantığı ayrı bir sınıfa taşır ve o sınıf birçok yerde yeniden kullanılabilir; aynı denetim on alanda gerekiyorsa özellik on çift gövde ister, betimleyici on satır ister.

Aynı düzenin iki yaygın kullanımı daha vardır ve ikisi de ölçülen sayının aynısını öder. Birincisi salt okunur alandır: yalnız okuma gövdesi yazılır, yazma gövdesi hiç yazılmaz ve alana atama AttributeError ile düşer. Değeri kurulumda belirlenip sonra değişmemesi gereken alanlar böyle bildirilir. İkincisi hesaplanan alandır: okuma gövdesi saklanmış bir değeri döndürmek yerine onu her seferinde üretir. İkisi de dışarıdan bir alan gibi görünür, ve görünüşün yanıltıcı tarafı buradadır — n.deger yazımı bedava bir sözlük araması gibi okunur ama arkasında koşan bir gövde vardır. Bir döngünün içinde aynı alanı bin kez okumak, bin kez gövde çalıştırmak demektir. Bu yüzden araya giren gövdenin ucuz olması beklenir; pahalı bir iş bir alan gibi sunulmaz, açık bir yöntem çağrısı olarak sunulur.

Betimleyici Veriyi Nerede Tutar

Betimleyicinin bir tuzağı vardır ve ilk ölçümdeki tanımın neden örneğin sözlüğüne yazdığını açıklar. Betimleyici nesnesi sınıf gövdesinde var edilir; yani sınıf başına bir tanedir ve bütün örnekler onu paylaşır. Veriyi kendi üzerinde tutan bir betimleyici, veriyi de paylaştırmış olur.

  • SN56 — İki betimleyici aynı işi yapar; tek farkları veriyi nereye yazdıklarıdır.
  • SN57 — Üç örnek var edilir ve her biri kendi sırasını yazar; sonra üçü de okunur.
  • SN58 — Son sütun üç örneğin kendi sözlüklerindeki toplam giriş sayısıdır; içerikleri basılmaz.
class KendindeTutan:
    """Veriyi betimleyici nesnesinin uzerinde tutar."""

    def __get__(self, nesne, sahip=None):
        return self.deger

    def __set__(self, nesne, yeni):
        self.deger = yeni


class OrnekteTutan:
    """Veriyi erisilen ornegin sozlugunde tutar."""

    def __set_name__(self, sahip, ad):
        self.ad = "_" + ad

    def __get__(self, nesne, sahip=None):
        return getattr(nesne, self.ad)

    def __set__(self, nesne, yeni):
        setattr(nesne, self.ad, yeni)


class KotuKutu:
    deger = KendindeTutan()


class IyiKutu:
    deger = OrnekteTutan()


print(f"{'sınıf':<10s} {'üç örneğin gördüğü':>20s} {'tek betimleyici':>16s} "
      f"{'örnek sözlüğünde':>17s}")
for sinif in (KotuKutu, IyiKutu):
    ucu = [sinif() for _ in range(3)]
    for i, n in enumerate(ucu):
        n.deger = i
    tanim = sinif.__dict__["deger"]
    print(f"{sinif.__name__:<10s} {str([n.deger for n in ucu]):>20s} "
          f"{str(all(type(n).__dict__['deger'] is tanim for n in ucu)):>16s} "
          f"{sum(len(n.__dict__) for n in ucu):>17d}")
sınıf        üç örneğin gördüğü  tek betimleyici  örnek sözlüğünde
KotuKutu              [2, 2, 2]             True                 0
IyiKutu               [0, 1, 2]             True                 3

Üç örnek üç ayrı değer yazdı. KotuKutu’da üçü de son yazılanı okuyor; ilk iki yazma kayboldu ve örnek sözlüklerinde 0 giriş var. IyiKutu’da her örnek kendi yazdığını okuyor ve 3 giriş açıldı. Orta sütun nedeni veriyor: iki sınıfta da üç örnek tek betimleyici nesnesini paylaşıyor. Betimleyici veriyi kendi üzerine yazdığında, üç örneğin yazması aynı tek nesnenin aynı alanına gidiyor.

Bu, ilk dersin sınıf gövdesindeki kap ölçümüyle aynı düzenektir ve başka bir kılıkta karşımıza çıkıyor: sınıf gövdesinde var edilen her şey örnekler arasında paylaşılır. Betimleyici de sınıf gövdesinde var edilir. Bu yüzden erişilen örneği ilk argüman olarak alır; verisini saklayacağı yer o örnektir, kendisi değil.

Örneğe yazarken kullanılacak adın nereden geldiği de ayrı bir sorudur. Betimleyici sınıf gövdesinde deger = ... biçiminde bağlanır ama nesnenin kendisi bu adı bilmez; kendisine verilen adı öğrenmesi için sınıf kurulurken çağrılan bir kanca vardır. İlk ölçümdeki tanım da, buradaki OrnekteTutan da o kancayı yazar ve örnekte kullanacağı adı ondan üretir. Kanca yazılmazsa ad elle verilmek zorunda kalır ve aynı betimleyici iki alanda kullanıldığında iki alan aynı yere yazar — KotuKutu’nun sonucunun bir başka biçimi.

“Kim” sorusunun bu ölçümdeki yanıtı iki katmanlıdır. Çağrıyı yanıtlayan betimleyicinin sınıfıdırKotuKutu’nun kendi gövdesinde deger için hiçbir gövde yoktur. Ama yanıtın hangi veriye bakacağını yanıtlayan taraf değil, verinin yazıldığı yer belirler. İkisi ayrıştığında ölçüm de ayrışır.

İki Kanca, İki Sıklık

Erişimi yakalamanın son biçimi tek tek alanlar için değil, bütün öznitelik erişimi içindir. İki kanca vardır ve karıştırılmaları kolaydır: biri her erişimde çalışır, öteki yalnız normal arama başarısız olduğunda çalışır.

  • SN59 — Tek sınıf iki kancayı birden yazar; her ikisi de yalnız kayıt tutar ve normal aramayı bozmaz.
  • SN60 — Dört ad okunur: ikisi örnek sözlüğünde vardır, ikisi hiçbir yerde yoktur. Kurulumda oluşan kayıtlar temizlenir.
  • SN61 — Ölçülen şey her kancanın kaç kez çalıştığıdır; dönen değerler basılmaz.
IZ = []


class Kancali:
    """Iki kanca: biri her erisimde, oteki yalniz arama basarisiz olunca."""

    def __init__(self):
        self.birinci = 1
        self.ikinci = 2

    def __getattribute__(self, ad):
        IZ.append(("her erişim", ad))
        return object.__getattribute__(self, ad)

    def __getattr__(self, ad):
        IZ.append(("bulunamadı", ad))
        return None


n = Kancali()
IZ.clear()
OKUMALAR = ("birinci", "ikinci", "ucuncu", "dorduncu")
for ad in OKUMALAR:
    getattr(n, ad)

print(f"{'okunan ad':<12s} {'örnekte var':>12s} {'her erişim kancası':>19s} "
      f"{'bulunamadı kancası':>19s}")
for ad in OKUMALAR:
    print(f"{ad:<12s} {str(ad in ('birinci', 'ikinci')):>12s} "
          f"{sum(1 for k, a in IZ if k == 'her erişim' and a == ad):>19d} "
          f"{sum(1 for k, a in IZ if k == 'bulunamadı' and a == ad):>19d}")

print()
print(f"okuma {len(OKUMALAR)}, her erişim kancası "
      f"{sum(1 for k, _ in IZ if k == 'her erişim')} kez, bulunamadı kancası "
      f"{sum(1 for k, _ in IZ if k == 'bulunamadı')} kez çalıştı")
okunan ad     örnekte var  her erişim kancası  bulunamadı kancası
birinci              True                   1                   0
ikinci               True                   1                   0
ucuncu              False                   1                   1
dorduncu            False                   1                   1

okuma 4, her erişim kancası 4 kez, bulunamadı kancası 2 kez çalıştı

Dört okumada birinci kanca 4, ikincisi 2 kez çalıştı. Sayılar iki kancanın yerini söylüyor: birincisi aramanın önündedir ve okunan ad ne olursa olsun çalışır; ikincisi aramanın arkasındadır ve yalnız arama boş döndüğünde devreye girer.

Ayrımın pratik sonucu maliyet ve risktir. Her erişimde çalışan kanca yazıldığında sınıfın bütün öznitelik okumaları — yöntem erişimleri dahil — o gövdeden geçer; ölçümdeki dört okuma dörde çıkmıştır, ama gerçek bir sınıfta bu sayı sınıfın yaptığı her iş kadardır. Üstelik gövdenin içinde self. yazımı kullanmak kancayı yeniden tetikler ve sonsuz bir zincir kurar; ölçümdeki gövdenin ortak tabanın aramasını doğrudan çağırmasının nedeni budur.

Yalnız bulunamayan adlarda çalışan kanca ise ölçüde ucuzdur: var olan adların yolunda hiç görünmez. Var olmayan adları karşılamak, bir nesneye devredilmiş erişim sunmak ya da eksik bir alanı hesaplayarak üretmek için doğru olan da budur. Kural şudur: var olanı denetlemek için alan başına tanım, var olmayanı karşılamak için bulunamadı kancası.

Üç ölçüm birlikte okunduğunda öznitelik erişiminin tek bir sözlük araması olmadığı görülüyor. Bir ad okunduğunda önce sınıfın çözümleme sırasına bakılır; orada yazma gövdesi de olan bir betimleyici varsa onun okuma gövdesi çalışır ve örneğin sözlüğü hiç sorulmaz. Böyle bir tanım yoksa örneğin sözlüğüne bakılır. Orada da yoksa sınıflardaki sıradan değerlere ve yalnız okuma gövdesi olan tanımlara düşülür, ve en sonda bulunamadı kancası devreye girer. Sıranın bu düzeni ilk ölçümdeki denetimin neden atlanamadığını da açıklar: örnekte aynı adla bir giriş açılsa bile, yazma gövdesi olan tanım okumada öne geçer. İlk dersin ölçtüğü “önce örnek, sonra sınıf” kuralı bu zincirin ortasıdır; bu dersin eklediği şey zincirin başına ve sonuna yazılabilen gövdelerdir. Erişimi denetlemek, bu zincire bir halka koymaktır — ve her halka her erişimde ödenir.

Zincirin bir asimetrisini de kaydetmek gerekir: yazma tarafı okuma tarafı kadar zengin değildir. Bir betimleyici yazmayı da yakalayabilir, ama örneğin sözlüğüne doğrudan yazan bir kod sınıfın hiçbir gövdesine uğramaz. Üçüncü dersin ölçtüğü 0 engel bu dersten sonra da geçerlidir; değişen tek şey, alışılmış yazımın artık bir gövdeye uğramasıdır.

Özet

  • Araya giren tanım her erişimde çalışır: yedi erişim doğrudan alanda 0, özellikte ve betimleyicide 7 gövde çalıştırır — üç okuma, dört yazma.
  • Bedelin karşılığı denetimdir: geçersiz yazma çıplak alanda geçer ve son değer -1 olur, iki tanımlı sınıfta ValueError ile düşer ve son değer 3’te kalır.
  • Çağıran taraf üç sınıfta da aynı satırları yazar; bu yüzden çıplak bir alan, kullanıcılarını hiç değiştirmeden sonradan bir gövdenin arkasına alınabilir.
  • Betimleyici sınıf gövdesinde bir kez var edilir ve bütün örneklerce paylaşılır: veriyi kendi üzerinde tutan tanımda üç örnek [2, 2, 2] okur ve örnek sözlüklerinde 0 giriş açılır, örnekte tutan tanımda [0, 1, 2] okunur ve 3 giriş açılır.
  • İki kanca iki ayrı sıklıkta çalışır: dört okumada her erişim kancası 4, yalnız bulunamayan adlarda çalışan kanca 2 kez devreye girer.

Sonraki Adım

Buraya kadar yazılan her gövde elle yazıldı. Önceki ders on bir sözdizim için on iki gövde istedi; bu ders tek bir alanın denetimi için iki gövde daha ekledi. Oysa yazdığımız sınıfların çoğu aynı kalıbı tekrarlar: birkaç alan taşırlar, alanlarıyla karşılaştırılırlar ve alanlarıyla basılırlar. Sonraki ders bu tekrarı kısa bir tanımla değiştirmenin ne ürettiğini ölçer: kısa tanım hangi özel yöntemleri sınıfın sözlüğüne gerçekten yazar, elle yazılmış karşılığıyla yan yana konduğunda hangileri eksik kalır, ve tanıma eklenen her seçenek sözlüğe kaç yeni ad koyar?

İ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