İçeriğe geç
academia.sh

Ders 03 / 11

Kapsülleme Sözleşmeleri

Dört erişim biçiminin 3'ü değeri verir ve adın engellediği erişim 0'dır; çift alt çizgi bir kilit değil bir ad değişimidir ve iki sınıfın aynı adı için 1 yerine 2 giriş açar.

İçindekiler

Önceki ders iki tasarımı yanıtlayan sınıf üzerinden karşılaştırdı ve Sarmal’ın içeride iki nesne tuttuğunu gösterdi. O iki nesne Sarmal’ın iç düzenidir: yarın biri kaldırılabilir, adı değişebilir, ikisi tek nesnede birleşebilir. Ama ölçümde hiçbir şey onlara dışarıdan dokunulmasını engellemedi — nesne.imzali yazımı nesne.kaynak() kadar geçerlidir.

Bu ders o sınırın nasıl bildirildiğini ölçer. Bazı diller görünürlüğü dilin kendisiyle kapatır; Python kapatmaz. Sorulan şey şudur: adın başına konan alt çizgi erişimi gerçekten engelliyor mu, iki alt çizgi ne değiştiriyor, ve engellemiyorsa bu bildirimin ölçülebilir bir işi var mı?

Üç Adlandırma, Dört Erişim

Bir sınıf üç ayrı adlandırma biçimi kullanabilir. Alt çizgisiz ad dışarıya açıktır: bu adın adı, tipi ve anlamı sınıfın verdiği sözün parçasıdır. Tek alt çizgiyle başlayan ad içeridir: sınıf onu kullanır, dışarıdan okuyan kendi sorumluluğuna okur, ve sınıf onu haber vermeden değiştirebilir. İki alt çizgiyle başlayan ad ise dilin ad değişimi (name mangling) kuralını tetikler; adın önüne sınıfın adı eklenir.

Üçü de aynı biçimde atanır ve üçü de aynı sözlüğe yazılır. Ayrıldıkları yer, dışarıdan hangi yazımla okunabildikleridir.

  • SN27 — Üç ad tek bir __init__ gövdesinde, birbirinin aynısı olan üç atamayla kurulur; değerleri de adlarını yansıtır.
  • SN28 — Dört erişim biçimi denenir: üç ad doğrudan, çift alt çizgili ad ise bir de değişmiş yazımıyla. Erişemeyen biçim ünlem önekiyle istisnanın adını kaydeder.
  • SN29 — Sınıfın kendi gövdesinden yapılan okuma ayrıca basılır; ölçüm dışarıdan ve içeriden erişimi karşılaştırır.
  • SN30 — Örnek sözlüğündeki adlar sıralanmış olarak basılır; hiçbir kimlik sayısı ya da ortama bağlı veri basılmaz.
"""Ad neyi engelliyor: uc gorunurluk bildirimi ve dort erisim bicimi."""


class Depo:
    """Uc ayri adlandirma bicimi; ucu de ayni sekilde atanir."""

    def __init__(self):
        self.acik = "acik"
        self._ic = "ic"
        self.__kapali = "kapali"

    def kendi_okur(self):
        return (self.acik, self._ic, self.__kapali)


d = Depo()

BICIMLER = (
    ("d.acik", lambda: d.acik),
    ("d._ic", lambda: d._ic),
    ("d.__kapali", lambda: getattr(d, "__kapali")),
    ("d._Depo__kapali", lambda: d._Depo__kapali),
)


def dene(cagri):
    try:
        return repr(cagri())
    except AttributeError as e:
        return f"!{type(e).__name__}"


print(f"{'yazım':<22s} {'dışarıdan erişim':>17s}")
for ad, cagri in BICIMLER:
    print(f"{ad:<22s} {dene(cagri):>17s}")

veren = sum(1 for _, c in BICIMLER if not dene(c).startswith("!"))
print()
print(f"sınıfın kendisi üçünü de okur -> {d.kendi_okur()}")
print(f"örnek sözlüğündeki adlar -> {sorted(d.__dict__)}")
print(f"dört erişim biçiminin {veren}'ü değeri verdi; "
      f"adın engellediği erişim {len(BICIMLER) - veren - 1}")
yazım                   dışarıdan erişim
d.acik                            'acik'
d._ic                               'ic'
d.__kapali               !AttributeError
d._Depo__kapali                 'kapali'

sınıfın kendisi üçünü de okur -> ('acik', 'ic', 'kapali')
örnek sözlüğündeki adlar -> ['_Depo__kapali', '_ic', 'acik']
dört erişim biçiminin 3'ü değeri verdi; adın engellediği erişim 0

Dört biçimin üçü değeri veriyor. İlk iki satır beklenen sonucu üretiyor: açık ad da, tek alt çizgili ad da dışarıdan okunuyor. Tek alt çizginin erişime hiçbir etkisi yoktur; dilin gözünde _ic ile acik arasında hiçbir fark yoktur. Aradaki tek fark, insanların ve araçların okuduğu bir bildirimdir.

Üçüncü satır bir engel gibi görünüyor ama değildir; dördüncü satır bunu gösteriyor. Aynı değer d._Depo__kapali yazımıyla sorunsuz okunuyor. Örnek sözlüğüne bakıldığında neden görünür: o adın sözlükteki karşılığı zaten _Depo__kapali’dir. Çift alt çizgi bir kilit değil, derleme sırasında yapılan bir ad değişimidir — sınıf gövdesindeki self.__kapali yazımı, adın önüne sınıfın adı eklenmiş hâline çevrilir. d.__kapali yazımının başarısız olmasının nedeni erişimin kapatılması değil, öyle bir adın hiç var olmamasıdır.

Bu yüzden son satırdaki sayı 0‘dır: dört biçimin hiçbirinde erişim engellenmedi, yalnız bir biçimde aranan ad yanlıştı. Python’da görünürlük dilin uyguladığı bir kural değil, sınıfın verdiği bir bildirimdir; uyup uymamak çağıranın kararıdır ve uymamanın bedeli çalışma zamanında değil, sınıf değiştiğinde ödenir.

Bildirimin bir alıcısı olduğu için de boş bir jest değildir. Alt çizgili adlar sınıfın açık yüzeyinin dışında sayılır: bir sınıfın hangi adlarını kullanmanın güvenli olduğunu soran bir okuyucu, yardım metnini üreten bir araç ve from ... import * yazımının hangi adları getireceğine karar veren kural aynı işareti okur. Bildirimin gücü dilin zorlamasından değil, herkesin aynı biçimde okumasından gelir. Önceki dersin Sarmal sınıfı buna örnektir: içeride tuttuğu iki nesnenin adı _ ile yazılsaydı, dışarıdan onlara dokunan kodun kırılma sorumluluğu açıkça devredilmiş olurdu.

Ad Değişimi Neyi Çözer

Çift alt çizgi erişim engellemiyorsa ne işe yarar? Ölçülebilir bir işi vardır ve tek başına bir sınıfta görünmez: iki sınıf aynı adı kullandığında ortaya çıkar. Kalıtımda taban ve türev tek bir örnek sözlüğünü paylaşır; ikisi de self.durum yazarsa ikinci yazma birincisini siler. Ad değişimi her sınıfın adını öneke koyduğu için bu çakışmayı ortadan kaldırır.

  • SN31 — Taban ve türev aynı iki adı yazar; türevin kurulum gövdesi önce tabanınkini çağırır, sonra kendi değerlerini atar.
  • SN32 — Her sınıfın kendi gövdesinden yapılan okuma ayrı ayrı basılır; ölçülen şey aynı yazımın iki gövdede hangi değeri gördüğüdür.
  • SN33 — Son satır adların örnek sözlüğünde kaç giriş açtığını sayar.
class Taban:
    def __init__(self):
        self._durum = "Taban"
        self.__durum = "Taban"

    def taban_okur(self):
        return {"tek": self._durum, "cift": self.__durum}


class Turev(Taban):
    def __init__(self):
        super().__init__()
        self._durum = "Turev"
        self.__durum = "Turev"

    def turev_okur(self):
        return {"tek": self._durum, "cift": self.__durum}


t = Turev()
print(f"{'yazım':<14s} {'Taban gövdesi okur':>19s} {'Turev gövdesi okur':>19s}")
for anahtar, ad in (("tek", "_durum"), ("cift", "__durum")):
    print(f"{ad:<14s} {t.taban_okur()[anahtar]:>19s} {t.turev_okur()[anahtar]:>19s}")

print()
print(f"örnek sözlüğündeki adlar -> {sorted(t.__dict__)}")
print(f"iki sınıfın tek alt çizgili adı için giriş "
      f"{sum(1 for a in t.__dict__ if a == '_durum')}, "
      f"çift alt çizgili adı için {sum(1 for a in t.__dict__ if a.endswith('__durum'))}")
yazım           Taban gövdesi okur  Turev gövdesi okur
_durum                       Turev               Turev
__durum                      Taban               Turev

örnek sözlüğündeki adlar -> ['_Taban__durum', '_Turev__durum', '_durum']
iki sınıfın tek alt çizgili adı için giriş 1, çift alt çizgili adı için 2

İlk satır çakışmanın kendisidir. Taban’ın gövdesi self._durum okuduğunda Turev değerini görüyor — kendi yazdığı değeri değil. Türevin kurulumu tabanınkinden sonra koştuğu için aynı sözlük girişinin üzerine yazdı ve tabanın değeri kayboldu. Taban sınıfı bu adı yazan tek taraf olduğunu sanıyordu; bir alt sınıf farkında olmadan onu bozdu.

İkinci satırda çakışma yok. Her gövde kendi yazdığı değeri okuyor, çünkü iki yazım iki ayrı ada çevrildi. Alt satırdaki sayılar bunu doğruluyor: tek alt çizgili ad iki sınıf için 1 giriş açıyor, çift alt çizgili ad 2 giriş açıyor. Sözlükteki adlar da bunu gösteriyor.

Ad değişiminin çözdüğü sorun budur ve tek budur: bir sınıfın iç adının, kim türetirse türetsin yalnız kendisine ait kalması. Bir sınıf başkalarının türetmesi beklenen bir tabansa ve iç durumunun alt sınıflarca ezilmemesi gerekiyorsa çift alt çizgi ölçülebilir bir güvence verir. Sıradan bir iç ad için ise tek alt çizgi yeterlidir — çift alt çizgi orada yalnız yazımı uzatır ve alt sınıfın erişimini gereksizce zorlaştırır. Ad değişiminin sınıf adına bağlı olduğunu da eklemek gerekir: iki ayrı yerde aynı ada sahip iki sınıf varsa önek de aynı çıkar ve güvence düşer. Verilen söz “kimse erişemez” değil, “aynı adı taşımayan sınıflar çakışmaz”dır.

Bildirim Değişmezi Korumaz

Kapsüllemenin amacı adı gizlemek değil, bir değişmezi ayakta tutmaktır. Nesneye dayalı programlamada durumun korunması ve değişmezlerin sürdürülmesi Programlama Paradigmaları kursunda kurulmuştu; bu tartışma tekrarlanmaz. Burada ölçülen şey bir soru: adlandırma bildirimi değişmezi korumaya yeter mi?

  • SN34 — Ölçülen sınıfın tek değişmezi vardır: tutulan toplam, öğelerin toplamına eşit olmalıdır. Tutarlılık her yazmadan sonra sınanır.
  • SN35 — Beş yazma denenir: üçü sınıfın yöntemi üzerinden, ikisi doğrudan iç adlara. İkisi de tek alt çizgili adları kullanır ve hiçbiri engellenmez.
  • SN36 — “Gövdeden geçti” sütunu yazmanın sınıfın yöntemine uğrayıp uğramadığını, öteki sütun o yazmadan sonra değişmezin ayakta olup olmadığını gösterir.
  • SN37 — Yazmalar sırayla ve aynı nesne üzerinde yapılır; bozulan bir değişmez sonraki satırlarda da bozuk kalır.
class Sayac:
    """Degismez: _toplam her zaman _ogeler toplamina esittir."""

    def __init__(self):
        self._ogeler = []
        self._toplam = 0

    def ekle(self, n):
        self._ogeler.append(n)
        self._toplam += n

    def tutarli(self):
        return self._toplam == sum(self._ogeler)


s = Sayac()
YAZMALAR = (
    ("s.ekle(2)", lambda: s.ekle(2)),
    ("s.ekle(3)", lambda: s.ekle(3)),
    ("s.ekle(5)", lambda: s.ekle(5)),
    ("s._toplam = 99", lambda: setattr(s, "_toplam", 99)),
    ("s._ogeler.append(7)", lambda: s._ogeler.append(7)),
)

print(f"{'yazma':<21s} {'gövdeden geçti':>15s} {'değişmez korundu':>17s}")
gecen = korunan = 0
for ad, islev in YAZMALAR:
    govdeden = ad.startswith("s.ekle")
    islev()
    korundu = s.tutarli()
    gecen += govdeden
    korunan += korundu
    print(f"{ad:<21s} {str(govdeden):>15s} {str(korundu):>17s}")

print()
print(f"yazma {len(YAZMALAR)}, gövdeden geçen {gecen}, "
      f"adın engellediği 0, değişmezin korunduğu {korunan}")
yazma                  gövdeden geçti  değişmez korundu
s.ekle(2)                        True              True
s.ekle(3)                        True              True
s.ekle(5)                        True              True
s._toplam = 99                  False             False
s._ogeler.append(7)             False             False

yazma 5, gövdeden geçen 3, adın engellediği 0, değişmezin korunduğu 3

Beş yazmanın üçü sınıfın gövdesinden geçti ve üçünde de değişmez ayakta kaldı. İkisi geçmedi ve ikisi de değişmezi bozdu. Adın engellediği yazma sayısı yine 0’dır: iki ihlal de tek alt çizgili adlara yapıldı ve hiçbiri istisna üretmedi.

Son satır bir sınırı bozmayı da gösteriyor. s._ogeler.append(7) bir atama bile değil; okunan bir nesne üzerinde yapılan bir değişikliktir. Bir adı okutmak, o adın gösterdiği değiştirilebilir nesneyi de teslim etmektir. Kapsüllemenin bu tarafı adlandırmayla hiç çözülemez; ancak dışarıya kopya vermekle ya da değiştirilemez bir kap vermekle çözülür, ve ikisi de gövdede alınmış bir karardır.

Ölçümün sıralı yapılması bir noktayı daha gösteriyor. Dördüncü satırda bozulan değişmez beşinci satırda da bozuk; nesne kendini onarmıyor ve kimse durumu sorgulamıyor. Bozulmanın ortaya çıkacağı yer, ihlalin yapıldığı satır değil, toplamın okunduğu herhangi bir satır olacaktır. İhlal ile belirtinin arasının açılması, denetimsiz yazmanın asıl bedelidir.

“Kim” sorusunun bu dersteki yanıtı tektir: değişmezi koruyan tek yer ekle gövdesidir. Ad ne yapıyorsa yapsın, o gövdeye uğramayan her yazma denetimsizdir. Adlandırma bildirimi sözleşmenin hangi yazımların desteklendiğini söyler; sözleşmeyi uygulayan taraf değildir. Uygulayan tarafı yazmak ayrı bir iştir ve erişimin kendisini araya bir gövde koyarak yakalamayı gerektirir.

Gerçekten Engelleyen Tek Bildirim

Adlandırma hiçbir şeyi engellemiyorsa, engelleyen bir bildirim var mıdır? Bir tane vardır ve görünürlüğü değil, öznitelik kümesini bildirir. Bir sınıf gövdesinde __slots__ adıyla yazılan bir dizi, o sınıfın örneklerinin hangi adları taşıyabileceğini sabitler; bildirilmemiş bir ada yazma çalışma zamanında düşer ve örnekler ayrı bir sözlük taşımaz.

  • SN38 — İki sınıf aynı kurulum gövdesini paylaşır; tek fark birinin öznitelik kümesini bildirmesi, ötekinin bildirmemesidir.
  • SN39 — İki yazma denenir: bildirilmiş bir ada ve bir harf farkla yanlış yazılmış ada. İkincisi bilerek yanlıştır.
  • SN40 — “Örnek sözlüğü” sütunu örneğin ayrı bir öznitelik sözlüğü taşıyıp taşımadığını bildirir; içeriği ve büyüklüğü basılmaz.
  • SN41 — “Engellenen” sütunu iki yazmadan kaçının istisnayla düştüğünü sayar.
class Serbest:
    """Oznitelik kumesini bildirmez: her ad yazilabilir."""

    def __init__(self, ad):
        self.ad = ad


class Kisitli:
    """Oznitelik kumesini bildirir: yalniz bildirilen adlar yazilabilir."""

    __slots__ = ("ad",)

    def __init__(self, ad):
        self.ad = ad


def yaz(nesne, alan):
    try:
        setattr(nesne, alan, "yeni")
    except AttributeError as e:
        return f"!{type(e).__name__}"
    return "geçti"


print(f"{'sınıf':<10s} {'ad yazımı':>16s} {'adi yazımı':>16s} "
      f"{'örnek sözlüğü':>14s} {'engellenen':>11s}")
for s in (Serbest, Kisitli):
    n = s("k")
    sonuc = [yaz(n, "ad"), yaz(n, "adi")]
    print(f"{s.__name__:<10s} {sonuc[0]:>16s} {sonuc[1]:>16s} "
          f"{('var' if hasattr(n, '__dict__') else 'yok'):>14s} "
          f"{sum(1 for c in sonuc if c.startswith('!')):>11d}")
sınıf             ad yazımı       adi yazımı  örnek sözlüğü  engellenen
Serbest               geçti            geçti            var           0
Kisitli               geçti  !AttributeError            yok           1

İlk satır bildirimsiz durumun sessizliğini gösteriyor: yanlış yazılmış ad da geçiyor ve nesneye yeni bir öznitelik ekliyor. Program hata vermez; yalnız ad eski değerinde kalır ve kimse bunu fark etmez. Alt satırda aynı yazma düşüyor; engellenen yazma sayısı 1’e çıkıyor.

Bu, dersin tek gerçek engelidir ve dikkat çekici biçimde bir görünürlük engeli değildir. __slots__ bildirimi hangi adların yazılabileceğini söyler; hangi adların dışarıdan okunabileceğini yine söylemez. Bildirilmiş bir ad, alt çizgili de olsa dışarıdan okunmaya açıktır. Yani Python’da uygulanan tek kapsülleme kuralı, kümenin kapalılığıdır, kümenin gizliliği değildir.

Örnek sözlüğünün yokluğu bunun düzeneğini de açıklıyor. Bildirim yapıldığında örnek adları serbest bir sözlükte tutmaz; her ad için sınıf üzerinde sabit bir yuva kurulur ve o yuvalar dışında yazılacak yer kalmaz. Engelin kaynağı bir yasak değil, yerin yokluğudur.

Özet

  • Python’da görünürlük dilin uyguladığı bir kural değil, adlandırmayla yapılan bir bildirimdir; dört erişim biçiminin 3’ü değeri verir ve adın engellediği erişim 0’dır.
  • Tek alt çizgili adın erişime hiçbir etkisi yoktur; sınıfın iç düzenine ait olduğunu ve haber verilmeden değişebileceğini bildirir.
  • Çift alt çizgi bir kilit değil, sınıf adını öneke koyan bir ad değişimidir; d.__kapali yazımının düşmesinin nedeni erişimin kapatılması değil, o adın hiç var olmamasıdır.
  • Ad değişiminin ölçülebilir işi çakışmayı önlemektir: taban ile türevin aynı adı için tek alt çizgide 1 giriş açılır ve tabanın değeri kaybolur, çift alt çizgide 2 giriş açılır ve her gövde kendi değerini okur.
  • Değişmezi koruyan tek yer yöntem gövdesidir; beş yazmanın 3’ü gövdeden geçti ve değişmez 3’ünde ayakta kaldı, geçmeyen 2 yazma onu bozdu.
  • Uygulanan tek bildirim öznitelik kümesini kapatan __slots__’tur: bildirimsiz sınıfta yanlış yazılmış ad sessizce geçer, bildirimli sınıfta düşer. Kapatılan şey kümedir, görünürlük değildir.

Sonraki Adım

Buraya kadar yazdığımız sınıflar kendi yöntemleriyle çağrıldı: ekle, bicimle, kaynak. Oysa önceki iki kursta ölçülen protokollerin çoğu yöntem adıyla değil, sözdizimle çağrılıyordu — len(n), n == m, with n, for x in n. O sözdizimleri dilin verdiği nesneler karşılıyordu ve karşılayan taraf hep hazırdı. Sonraki ders karşılayan tarafı bizim yazdığımız durumu ölçer: kendi sınıfımız hiçbir özel yöntem yazmadan bu sözdizimlerin kaçına katılabiliyor, katılamadıklarında ne oluyor, ve yazdığımız her yöntem hangi sözdizimi açıyor?

İ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