.. meta:: :description: Bu bölümde nesne tabanlı programlamadan söz edeceğiz.
          :keywords: python, python3, nesne, oop, sınıf, class, miras alma,
           inheritance, nesne yönelimli programlama, nesne tabanlı programlama,
           object oriented programming, self, instantiation, instance, örnek,
           örneklendirme, örnekleme

.. highlight:: py3

*******************************************
Nesne Tabanlı Programlama (Devamı)
*******************************************

Geçen bölümlerde, nesne tabanlı programlamaya ilişkin hem temel, hem orta, hem
de ileri düzey sayılabilecek pek çok konuya değindik. Şimdiye kadar
öğrendiklerimiz, nesne tabanlı programlama yaklaşımı çerçevesinde yazılım
üretirken yönümüzü bulabilmemiz açısından büyük ölçüde yeterlidir. Ancak daha
önce de söylediğimiz gibi, nesne tabanlı programlama çok geniş kapsamlı bir
konudur ve içinde şimdiye kadar adını bile anmadığımız daha pek çok kavram
barındırır. İşte bu bölümde, geçen derslerimizde incelemeye fırsat
bulamadığımız, ancak nesne tabanlı programlamayı daha derinlemesine tanımak
bakımından bilmemizin iyi olacağı birtakım ileri düzey kavramlardan söz
edeceğiz.

Bu bölümde inceleyeceğimiz ilk konu 'sınıf üyeleri'.

Sınıf Üyeleri
*************

Python'da bir sınıf içinde bulunan nitelikler, değişkenler, metotlar,
fonksiyonlar ve buna benzer başka veri tipleri, o sınıfın üyelerini meydana
getirir. Bir sınıfın üyelerini genel olarak üçe ayırarak inceleyebiliriz:

+ Aleni üyeler (*public members*)
+ Gizli üyeler (*private members*)
+ Yarı-gizli üyeler (*semi-private members*).

Bu bölümde bu üç üye türünü ve bunların birbirinden farkını ele alacağız.
Öncelikle aleni üyelerden başlayalım.

Aleni Üyeler
==============

Eğer bir sınıf üyesi dışarıya açıksa, yani bu üyeye sınıf dışından *normal
yöntemlerle* erişilebiliyorsa bu tür üyelere 'aleni üyeler' adı verilir.
Programlama maceranız boyunca karşınıza çıkacak veri üyelerinin tamamına yakını
alenidir. Biz de bu kitapta şimdiye kadar yalnızca aleni üyeleri gördük.

Eğer bildiğiniz tek programlama dili Python ise, şu anda tam olarak neden
bahsediyor olduğumuza anlam verememiş olabilirsiniz. Dilerseniz durumu
zihninizde biraz olsun netleştirebilmek için basit bir örnek verelim.

Diyelim ki elimizde şöyle bir sınıf var::

    class Sınıf():
        sınıf_niteliği = 'sınıf niteliği'

        def örnek_metodu(self):
            print('örnek metodu')

        @classmethod
        def sınıf_metodu(cls):
            print('sınıf metodu')

        @staticmethod
        def statik_metot():
            print('statik metot')

Bu kodların `sinif.py` adlı bir dosya içinde yer aldığını varsayarsak şöyle bir
şeyler yazabiliriz::

    >>> import sinif
    >>> s = sinif.Sınıf()
    >>> dir(s)

    ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__',
     '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__',
     '__hash__', '__init__', '__le__', '__lt__', '__module__',
     '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
     '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__',
     'statik_metot', 'sınıf_metodu', 'sınıf_niteliği', 'örnek_metodu']

Burada öncelikle kodlarımızı barındıran modülü içe aktardık. Daha sonra, içe
aktardığımız modülün içindeki ``Sınıf()`` adlı sınıfımızı `s` örneğine atadık ve
ardından ``dir()`` komutunu kullanarak, içe aktardığımız bu sınıfın içeriğini
sorguladık.

Gördüğünüz gibi, içe aktardığımız sınıfın bütün öğeleri listede var. Yani biz bu
sınıf içindeki bütün öğelere normal yollardan erişme imkanına sahibiz::

    >>> s.statik_metot()

    'statik metot'

    >>> s.örnek_metodu()

    'örnek metodu'

    >>> s.sınıf_metodu()

    'sınıf metodu'

    >>> s.sınıf_niteliği

    'sınıf niteliği'

İşte ``dir()`` komutunun çıktısında görünen ve normal yollardan erişebildiğimiz
bütün bu öğeler birer aleni üyedir.

Yukarıda da ifade ettiğimiz gibi, program yazarken çoğu zaman yalnızca aleni
üyelerle muhatap olacaksınız. Ancak bazı durumlarda, yazdığınız bir sınıftaki
bütün sınıf üyelerinin dışarıya açık olmasını istemeyebilirsiniz. Eğer
kodlarınızda, sınıfın yalnızca iç işleyişini ilgilendiren, bu yüzden de
dışarıdan erişilmesine gerek olmadığını veya erişilirse problem çıkacağını
düşündüğünüz birtakım öğeler varsa bunları dışarıya kapatarak bir 'gizli üye'
haline getirmek isteyebilirsiniz. Peki ama nasıl?

Gizli Üyeler
=============

Python'da şimdiye kadar gördüğümüz ve yukarıda andığımız aleni üyelerin dışında,
bir de gizli üyeler bulunur. Aleni üyelerin aksine gizli üyeler dışarıya açık
değildir. Gizli üyelere, normal yöntemleri kullanarak sınıf dışından erişemeyiz.

Konuyu açıklığa kavuşturmak için, aleni üyeleri anlatırken verdiğimiz sınıf
örneğinde şu değişikliği yapalım::

    class Sınıf():
        __gizli = 'gizli'

        def örnek_metodu(self):
            print(self.__gizli)
            print('örnek metodu')

        @classmethod
        def sınıf_metodu(cls):
            print('sınıf metodu')

        @staticmethod
        def statik_metot():
            print('statik metot')

Burada `__gizli` adlı bir gizli sınıf niteliği tanımladık. Bu değişkenin
yalnızca baş tarafında iki adet alt çizgi olduğuna, ancak uç tarafında alt
çizgi bulunmadığına dikkat edin. İşte Python'da baş tarafında yukarıdaki gibi
iki adet alt çizgi olan, ancak uç tarafında alt çizgi bulunmayan (veya yalnızca
tek bir alt çizgi bulunan) bütün öğeler birer gizli üyedir. Dışarıya kapalı olan
bu gizli üyelere, normal yöntemleri kullanarak sınıf dışından erişemezsiniz.

İsterseniz deneyelim::

    >>> import sinif
    >>> s = sinif.Sınıf()
    >>> s.__gizli

    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'Sınıf' object has no attribute '__gizli'

Gördüğünüz gibi, örnek adı üzerinden `__gizli` niteliğine erişemiyoruz. Bir de
sınıf adı üzerinden erişmeyi deneyelim::

    >>> sinif.Sınıf.__gizli

    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: type object 'Sınıf' has no attribute '__gizli'

Bu şekilde de erişemedik. Çünkü dediğimiz gibi, başında çift alt çizgi olan,
ancak ucunda herhangi bir çizgi bulunmayan (veya tek bir alt çizgi bulunan) bu
gizli öğelere **normal yollardan** erişemeyiz.

Dilerseniz gizli üye oluşturma kurallarını şöyle bir netleştirelim:

Bir üyenin gizli olabilmesi için başında **en az** iki adet, ucunda da **en
fazla** bir adet alt çizgi bulunmalıdır. Yani şunlar birer gizli üyedir::

    >>> __gizli = 'gizli'
    >>> __gizli_ = 'gizli'
    >>> __gizli_üye = 'gizli'
    >>> __gizli_üye_ = 'gizli'

Burada önemli bir noktaya dikkatinizi çekmek istiyorum: Gizli üyeler yalnızca
**sınıf dışına** kapalıdır. Bu üyelere **sınıf içinden** rahatlıkla
erişebiliriz. Mesela yukarıdaki örnekte bu durumu görüyorsunuz. `__gizli` adlı
değişkene ``örnek_metodu()`` içinden normal bir şekilde erişebiliyoruz::

    def örnek_metodu(self):
        print(self.__gizli)
        print('örnek metodu')

Bu durumda sınıf dışından bu ``örnek_metodu()``'na eriştiğimizde gizli üye olan
`__gizli`'ye de erişmiş oluyoruz::

    >>> import sinif
    >>> s = sinif.Sınıf()
    >>> s.örnek_metodu()

    'gizli'
    'örnek metodu'

Burada ``örnek_metodu()``, `__gizli` adlı gizli üyeye erişmemiz için bize
aracılık etmiş oluyor.

Peki ama bir insan neden bu şekilde birtakım gizli üyeler tanımlamak istiyor
olabilir?

Hatırlarsanız geçen bölümde şöyle bir örnek vermiştik::

    class Çalışan():
        personel = []

        def __init__(self, isim):
            self.isim = isim
            self.kabiliyetleri = []
            self.personele_ekle()

        @classmethod
        def personel_sayısını_görüntüle(cls):
            print(len(cls.personel))

        def personele_ekle(self):
            self.personel.append(self.isim)
            print('{} adlı kişi personele eklendi'.format(self.isim))

        @classmethod
        def personeli_görüntüle(cls):
            print('Personel listesi:')
            for kişi in cls.personel:
                print(kişi)

        def kabiliyet_ekle(self, kabiliyet):
            self.kabiliyetleri.append(kabiliyet)

        def kabiliyetleri_görüntüle(self):
            print('{} adlı kişinin kabiliyetleri:'.format(self.isim))
            for kabiliyet in self.kabiliyetleri:
                print(kabiliyet)

Burada `personel` adlı bir sınıf niteliğimiz var. Bu niteliğe sınıf içinde hem
``personele_ekle()`` adlı örnek metodundan hem de
``personel_sayısını_görüntüle()`` ve ``personeli_görüntüle()`` adlı sınıf
metotlarından erişmek suretiyle bu nitelik üzerinde çeşitli işlemler yapıyoruz.

Esasında şöyle bir düşününce, `personel` adlı niteliğin yalnızca sınıfın iç
işleyişi açısından önem taşıdığını rahatlıkla söyleyebiliriz. Bu niteliğe sınıf
dışından doğrudan erişilerek personel üzerinde işlem yapılmaya çalışılması çok
mantıksız. Yani sınıfımızı kullanacak kişilerin şu tür bir kod yazması biraz
abes kaçacaktır::

    >>> from calisan import Çalışan
    >>> Çalışan.personel.append('Ahmet')

Zira biz, kodlarımızın yapısı gereği, personel üzerindeki işlemlerin yalnızca
çeşitli fonksiyonlar/metotlar aracılığıyla yapılmasını istiyoruz.

Personele eleman ekleyecek kişilerin doğrudan `personel` listesine erişmesi,
kodlarımızın kullanım kurallarının bir bakıma ihlal edilmesi anlamına geliyor.
Çünkü biz personele eleman ekleme işlemleri için halihazırda ayrı bir metot
tanımlamış durumdayız. Eğer personele adam eklenecekse, bu işlem doğrudan
`personel` listesi üzerinden değil, ``personele_ekle()`` adlı örnek metodu
üzerinden gerçekleştirilmeli. Yukarıdaki kodlarda bu ``personele_ekle()`` metodu
doğrudan sınıfın kendi ``__init__()`` metodu tarafından kullanılıyor.
Dolayısıyla yukarıdaki sınıfı kullanmanın doğru yolu, ilgili sınıfı
örneklemektir::

    >>> from calisan import Çalışan
    >>> ahmet = Çalışan('Ahmet')

Aynı şekilde personel listesini görüntülemek için de doğrudan `personel`
listesine erişmeye çalışmayacağız. Yani şöyle bir şey yazmayacağız::

    >>> Çalışan.personel

Bunun yerine, bu iş için özel olarak tasarladığımız ``personeli_görüntüle()``
fonksiyonunu kullanacağız::

    >>> Çalışan.personeli_görüntüle()

İşte yukarıdaki kodlarda yer alan `personel` listesinin usulsüz bir şekilde
kullanılmasını önlemek amacıyla bu listeyi bir gizli üye haline
getirebilirsiniz::

    class Çalışan():
        __personel = []

        def __init__(self, isim):
            self.isim = isim
            self.kabiliyetleri = []
            self.personele_ekle()

        @classmethod
        def personel_sayısını_görüntüle(cls):
            print(len(cls.__personel))

        def personele_ekle(self):
            self.__personel.append(self.isim)
            print('{} adlı kişi personele eklendi'.format(self.isim))

        def personeli_görüntüle(self):
            print('Personel listesi:')
            for kişi in self.__personel:
                print(kişi)

        def kabiliyet_ekle(self, kabiliyet):
            self.kabiliyetleri.append(kabiliyet)

        def kabiliyetleri_görüntüle(self):
            print('{} adlı kişinin kabiliyetleri:'.format(self.isim))
            for kabiliyet in self.kabiliyetleri:
                print(kabiliyet)

Burada `personel` listesinin baş tarafına iki alt çizgi ekleyerek bunu sınıf
dışından, normal yollarla erişilmez hale getirdik::

    >>> Çalışan.__personel

    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: type object 'Çalışan' has no attribute '__personel'

Gördüğünüz gibi, aslında sınıfımız içinde `__personel` adlı bir nitelik olmasına
rağmen, Python bu niteliğe sınıf dışından erişilmesine izin vermiyor. Eğer
amacımız personel üzerinde çeşitli işlemler yapmaksa, bu iş için sınıfın bize
sunduğu metotları kullanmamız gerekiyor::

    >>> Çalışan.personel_sayısını_görüntüle()

Bu tip durumlarda gizli üyeler epey işinize yarayabilir...

Bir örnek daha verelim.

Yukarıdaki kodlarda, tıpkı `personel` listesi gibi, aslında ``personele_ekle()``
fonksiyonu da dışarıdan erişilmesine gerek olmayan, hatta dışarıdan erişilirse
kafa karıştırıcı olabilecek bir sınıf üyesidir.

``personele_ekle()`` adlı örnek metodu, sınıfımız içinde ``__init__()``
fonksiyonu tarafından kullanılıyor. Dolayısıyla sınıfımız örneklendiğinde
``personele_ekle()`` metodu devreye girerek yeni elemanı personel listesine
ekliyor::

    >>> ayşe = Çalışan('Ayşe')

    'Ayşe adlı kişi personele eklendi'

Öte yandan, bu fonksiyon aleni bir üye olduğu için, buna dışarıdan erişmemizin
önünde herhangi bir engel yok::

    >>> ayşe.personele_ekle()

    'Ayşe adlı kişi personele eklendi'

Bu fonksiyon sınıf dışından çağrıldığında, kendisini çağıran örnek adını
personel listesine tekrar ekleyecektir::

    >>> Çalışan.personeli_görüntüle()

    Ayşe
    Ayşe

Yani yukarıdaki komut Ayşe adlı kişiyi personel listesine tekrar ekler.
Dolayısıyla bu fonksiyona sınıf dışından erişilmesi son derece mantıksız, son
derece yanlış ve hatta son derece kafa karıştırıcıdır. O yüzden, herhangi bir
sıkıntı yaşanmasını engellemek amacıyla bu fonksiyonu da bir gizli üye olarak
tanımlayabiliriz::

    class Çalışan():
        __personel = []

        def __init__(self, isim):
            self.isim = isim
            self.kabiliyetleri = []
            self.__personele_ekle()

        @classmethod
        def personel_sayısını_görüntüle(cls):
            print(len(cls.__personel))

        def __personele_ekle(self):
            self.__personel.append(self.isim)
            print('{} adlı kişi personele eklendi'.format(self.isim))

        @classmethod
        def personeli_görüntüle(cls):
            print('Personel listesi:')
            for kişi in cls.__personel:
                print(kişi)

        def kabiliyet_ekle(self, kabiliyet):
            self.kabiliyetleri.append(kabiliyet)

        def kabiliyetleri_görüntüle(self):
            print('{} adlı kişinin kabiliyetleri:'.format(self.isim))
            for kabiliyet in self.kabiliyetleri:
                print(kabiliyet)

Bu şekilde ``personele_ekle()`` fonksiyonunu da dışarıya kapatmış olduk. Artık
bu fonksiyon da, olması gerektiği gibi, yalnızca sınıf içinde kullanılabilecek.

Yukarıdaki örnekler, bazı durumlarda veri gizlemenin epey işimize
yarayabileceğini bariz bir biçimde gösteriyor. Ama elbette, yukarıdaki
işlemlerin hiçbiri zorunlu değildir. Yani siz, yazdığınız kodlarda hiçbir sınıf
üyesini gizlemek mecburiyetinde değilsiniz. Yukarıda gösterdiğimiz kullanımlar
tamamen tercih meselesidir. Zaten birkaç nadir durum dışında, Python'da
verilerinizi gizlemek zorunda da kalmazsınız. Ama tabii kendiniz Python'ın bu
özelliğinden yararlanmasanız da, sırf bu özellikten yararlanan başka
programcıların yazdığı kodları anlayabilmek için bile olsa bu özellikten
haberdar olmalısınız.

İsim Bulandırma
=================

Gelin isterseniz gizli üyelere ilişkin ilginç bir özellikten söz edelim.

Python'da 'gizli' olarak adlandırdığımız öğeler aslında o kadar da gizli
değildir... Çünkü Python'da gerçek anlamda gizli ve dışarıya tamamen kapalı
üyeler bulunmaz. Peki bu ne anlama geliyor?

Bu şu anlama geliyor: Her ne kadar yukarıdaki örneklerde üyeleri dışarıya
kapatmak için kullandığımız alt çizgi işaretleri ilgili değişkeni gizlese de,
bunu tamamen erişilmez hale getirmez. Dediğimiz gibi, Python'da gerçek anlamda
dışa kapalı sınıf üyeleri bulunmadığı için biz bu üyelere **bir şekilde** erişme
imkanına sahibiz. Peki ama nasıl?

Python, kodlar içinde gizli bir üye ile karşılaştığında özel bir 'isim
bulandırma' (*name mangling*) işlemi gerçekleştirir ve ilgili gizli üyenin
görünüşünü değiştirir. Eğer Python'ın arkaplanda neler çevirdiğini bilirseniz,
gizli üyeye de erişebilirsiniz.

Örnek sınıfımız şöyleydi::

    class Sınıf():
        __gizli = 'gizli'

        def örnek_metodu(self):
            print(self.__gizli)
            print('örnek metodu')

        @classmethod
        def sınıf_metodu(cls):
            print('sınıf metodu')

        @staticmethod
        def statik_metot():
            print('statik metot')

Şimdi, bu sınıf içindeki gizli üyeye erişeceğiz.

Dikkatlice bakın::

    >>> import sinif
    >>> s = sinif.Sınıf()
    >>> s._Sınıf__gizli

    'gizli'

Ne kadar da tuhaf, değil mi?

İşte Python, siz bir sınıf üyesini `__gizli` şeklinde tanımladığınızda, bu
öğe üzerinde şu işlemleri gerçekleştirir:

Öncelikle değişkenin baş tarafına bir alt çizgi ekler::

    _

Daha sonra, bu alt çizginin sağ tarafına bu gizli üyeyi barındıran sınıfın adını
iliştirir::

    _Sınıf

Son olarak da gizli üyeyi sınıf adının sağ tarafına yapıştırır::

    _Sınıf__gizli

Dolayısıyla ``_Sınıf__gizli`` kodunu kullanarak, `__gizli` adlı üyeye sınıf
dışından erişebilirsiniz.

Pratik olması bakımından bir örnek daha verelim. Mesela şu örneği ele alalım::

    class Çalışan():
        __personel = []

        def __init__(self, isim):
            self.isim = isim
            self.kabiliyetleri = []
            self.__personele_ekle()

        @classmethod
        def personel_sayısını_görüntüle(cls):
            print(len(cls.__personel))

        def __personele_ekle(self):
            self.__personel.append(self.isim)
            print('{} adlı kişi personele eklendi'.format(self.isim))

        @classmethod
        def personeli_görüntüle(cls):
            print('Personel listesi:')
            for kişi in cls.__personel:
                print(kişi)

        def kabiliyet_ekle(self, kabiliyet):
            self.kabiliyetleri.append(kabiliyet)

        def kabiliyetleri_görüntüle(self):
            print('{} adlı kişinin kabiliyetleri:'.format(self.isim))
            for kabiliyet in self.kabiliyetleri:
                print(kabiliyet)

Burada ``__personele_ekle()`` adlı fonksiyon bir gizli üyedir. Dolayısıyla buna
dışarıdan normal yöntemlerle erişemeyiz.

Bunu test etmek için önce gerekli verileri oluşturalım::

    >>> from calisan import Çalışan
    >>> ahmet = Çalışan('Ahmet')

    Ahmet adlı kişi personele eklendi.

Şimdi `ahmet` örneği üzerinden bu gizli üyeye erişmeye çalışalım::

    >>> ahmet.__personele_ekle()

    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'Çalışan' object has no attribute '__personele_ekle'

Gördüğünüz gibi, Python bu üyeye normal yollardan erişmemize izin vermiyor. Ama
biz biliyoruz ki, Python bu üyeyi gizlerken özel bir isim bulandırma işlemi
gerçekleştiriyor. Bu bulandırma işleminin nasıl gerçekleştirildiğini bildiğimize
göre gizli üyeye erişebiliriz.

Öncelikle örneğimizin adını yazalım. Zira gizli üyeye bu ad üzerinden
erişeceğiz::

    >>> ahmet.

Şimdi bulandırma işlemini uygulamaya geçebiliriz.

Öncelikle bir alt çizgi ekleyelim::

    >>> ahmet._

Daha sonra sınıf adını iliştirelim::

    >>> ahmet._Çalışan

Son olarak da gizli üyenin kendisini yazalım::

    >>> ahmet._Çalışan__personele_ekle()

    Ahmet adlı kişi personele eklendi.

Gayet başarılı...

Yalnız buraya şöyle bir not düşelim: Her ne kadar Python bize gizli üyelere
erişme imkanı sunsa da, başkasının yazdığı kodları kullanırken, o kodlardaki
gizli üyelere erişmeye çalışmamak çoğu zaman iyi bir fikirdir. Nihayetinde eğer
bir programcı, bir sınıf üyesini gizlemişse bunun bir nedeni vardır. Eğer
erişmenizin istenmediği bir üyeye erişirseniz ve bunun sonucunda birtakım
sorunlarla karşılaşırsanız bu durum o programı yazan programcının değil, tamamen
sizin kabahatinizdir. Python programcılarının da sık sık söylediği gibi:
'Neticede hepimiz, doğruyu yanlışı bilen, yetişkin insanlarız.'

Yarı-gizli Üyeler
====================

Buraya kadar Python’a dair anlattığımız şeylerden, yerleşmiş adetlerin ve
geleneklerin Python açısından ne kadar önemli olduğunu anlamış olmalısınız. Daha
önce verdiğimiz örnekler, bu dildeki pek çok meselenin uzlaşma esası üzerinden
çözüme kavuşturulduğunu bize açık ve seçik olarak gösterdi. Mesela geçen
bölümlerde ele aldığımız `self` ve `cls` kelimeleri tamamen uzlaşmaya dayalı
kavramlardır. Python topluluğu içinde, `self` kelimesinin örnek metotları için,
`cls` kelimesinin ise sınıf metotları için kullanılması tamamen bir alışkanlık,
adet, gelenek ve uzlaşı meselesidir. Python’ın kendisi bize bu kelimeleri
dayatmaz. Ancak topluluk içinde süregelen kuvvetli gelenekler bizi başka
kelimeleri değil de yukarıdaki kelimeleri kullanmaya teşvik eder. Aynı şekilde
kod yazarken girinti sayısının dört boşluk olarak belirlenmiş olması da bir
gelenekten ibarettir. Yazdığınız kodlarda, aynı program içinde hep aynı sayıda
olmak şartıyla, istediğiniz sayıda boşluktan oluşan girintiler
kullanabilirsiniz. Ama Python'ın topluluk içi gelenekleri bizi dört boşlukluk
bir girintileme sistemi kullanmaya yöneltir.

İşte tıpkı yukarıdakiler gibi, Python'daki sınıf üyelerinin dışa açık veya dışa
kapalı olup olmaması da hep belli birtakım gelenekler üzerinden belirlenen bir
durumdur.

Bunun bir örneğini, yukarıda gizli üyeleri anlatırken vermiştik. Bir sınıf
içindeki herhangi bir niteliğin başında çift alt çizgi gördüğümüzde, o sınıfı
yazan kişinin, bu niteliğe sınıf dışından erişilmesini istemediğini anlıyoruz.
Python her ne kadar nitelikleri gizlememiz için bize özel bir mekanizma sunmuş
olsa da bu niteliğe erişmemizi tamamen engellemiyor, ancak ilgili sınıfı yazan
kişinin niyetine saygı göstereceğimizi varsayıyor.

Python'da sınıf üyelerinin gizliliği, yukarıda da gördüğümüz gibi, hem özel bir
mekanizma ile hem de topluluk içi gelenekler tarafından korunur.

Python'da bir de yalnızca topluluk içi gelenekler tarafından korunan
'yarı-gizli' üyeler (*semi-private members*) vardır. İşte bu bölümde, bir gizli
üye türü olan yarı-gizli üyelerden söz edeceğiz.

Yarı-gizli üyeler, herhangi bir özel mekanizma aracılığıyla değil de yalnızca
topluluk içi gelenekler tarafından korunan niteliklerdir. Herhangi bir üyeyi
yarı-gizli olarak işaretlemek için yapmamız gereken tek şey başına bir adet alt
çizgi yerleştirmektir. Örneğin::

    class Falanca():
        _yarıgizli = 'yarıgizli'

Buradaki `_yarıgizli` adlı niteliğe sınıf içinden veya dışından erişmemizi
engelleyen veya zorlaştıran hiçbir mekanizma bulunmaz. Ama biz bir sınıf içinde
tek alt çizgi ile başlayan bir öğe gördüğümüzde, bunun sınıfın iç işleyişine
ilişkin bir ayrıntı olduğunu, sınıf dışından bu öğeyi değiştirmeye kalkışmamamız
gerektiğini anlarız.

@property Bezeyicisi
**********************

Yukarıda aleni, gizli ve yarı-gizli sınıf üyelerinden söz ettik. İsterseniz
özellikle yarı-gizli öğelerin kullanıldığı bir kod örneği vererek yukarıda
anlattıklarımızı somut bir örnek üzerinden netleştirmeye çalışalım.

Diyelim ki şöyle bir kod yazdık::

    class Çalışan():
        personel = []

        def __init__(self, isim):
            self.isim = isim
            self.personele_ekle()

        def personele_ekle(self):
            self.personel.append(self.isim)
            print('{} adlı kişi personele eklendi'.format(self.isim))

        @classmethod
        def personeli_görüntüle(cls):
            print('Personel listesi:')
            for kişi in cls.personel:
                print(kişi)

Burada personel veritabanına kişi eklememizi ve veritabanındaki kişileri
görüntülememizi sağlayan birtakım metotlar var.

Bu metotları şöyle kullanıyoruz::

    >>> from calisan import Çalışan
    >>> ç1 = Çalışan('Ahmet')

    Ahmet adlı kişi personele eklendi

    >>> ç2 = Çalışan('Mehmet')

    Mehmet adlı kişi personele eklendi

    >>> Çalışan.personeli_görüntüle()

    Personel listesi:
    Ahmet
    Mehmet

Peki eğer kodlarımızı kullananlar personel listesindeki bir kişinin ismini
sonradan değiştirmek isterse ne yapacak?

Kodlarımız içinde, isim değişikliği yapılmasını sağlayan özel bir metot yok.
Dolayısıyla kodlarımızı kullananlar, doğrudan `isim` adlı örnek değişkenine
erişerek isim değişikliğini şu şekilde yapabilir::

    >>> ç1.isim = 'Selim'

Bu şekilde 'Ahmet' adlı kişinin ismini değiştirdik. Bunu teyit edelim::

    >>> print(ç1.isim)

    Selim

Ancak burada şöyle bir sorun var. Bu isim değişikliği personel listesine
yansımadı. Kontrol edelim::

    >>> Çalışan.personeli_görüntüle()

    Personel listesi:
    Ahmet
    Mehmet

Gördüğünüz gibi, 'Ahmet' ismi hâlâ orada duruyor. Bu sorunu gidermek için,
personel listesine de müdahale edilmesi gerekir::

    >>> kişi = Çalışan.personel.index('Ahmet')
    >>> Çalışan.personel[kişi] = 'Selim'

Burada öncelikle listelerin ``index()`` metodunu kullanarak, değiştirmek
istediğimiz kişinin `personel` listesindeki sırasını bulduk. Daha sonra da bu
bilgiyi kullanarak listede gerekli değişikliği yaptık.

Personel listesini tekrar kontrol ettiğimizde her şeyin yolunda olduğunu
görebiliriz::

    >>> Çalışan.personeli_görüntüle()

    Personel listesi:
    Selim
    Mehmet

Ancak bunun hiç kullanışlı bir yöntem olmadığı çok açık. Basit bir isim
değişikliği için, kullanıcılarımız bir sürü kod yazmak zorunda kalıyor.
Kullanıcılarımızın hayatını kolaylaştırmak için onlara pratik bir metot
sunabiliriz::

    class Çalışan():
        personel = []

        def __init__(self, isim):
            self.isim = isim
            self.personele_ekle()

        def personele_ekle(self):
            self.personel.append(self.isim)
            print('{} adlı kişi personele eklendi'.format(self.isim))

        @classmethod
        def personeli_görüntüle(cls):
            print('Personel listesi:')
            for kişi in cls.personel:
                print(kişi)

        def isim_değiştir(self, yeni_isim):
            kişi = self.personel.index(self.isim)
            self.personel[kişi] = yeni_isim
            print('yeni isim:', yeni_isim)

Burada ``isim_değiştir()`` adlı yeni bir fonksiyon tanımladık. Artık
kodlarımızdan istifade edenler yalnızca bu yeni fonksiyonu kullanarak, personele
önceden ekledikleri kişilerin ismini kolayca değiştirebilir::

    >>> from calisan import Çalışan
    >>> ç1 = Çalışan('Ahmet')
    >>> ç2 = Çalışan('Mehmet')
    >>> ç3 = Çalışan('Selim')
    >>> Çalışan.personeli_görüntüle()

    Personel listesi:
    Ahmet
    Mehmet
    Selim

    >>> ç1.isim_değiştir('Emre')

    yeni isim: Emre

    >>> Çalışan.personeli_görüntüle()

    Personel listesi:

    Emre
    Mehmet
    Selim

Gördüğünüz gibi, kodlarımız gayet güzel çalışıyor. Bu noktadan sonra, **eğer
arzu ederseniz**, kullanıcılarınızın `personel` ve `self.isim` adlı değişkenlere
doğrudan erişmesini engellemek için bunları tek alt çizgi veya çift alt çizgi
kullanarak gizleyebilirsiniz.

Çift alt çizgi ile::

    class Çalışan():
        __personel = []

        def __init__(self, isim):
            self.__isim = isim
            self.personele_ekle()

        def personele_ekle(self):
            self.__personel.append(self.__isim)
            print('{} adlı kişi personele eklendi'.format(self.__isim))

        @classmethod
        def personeli_görüntüle(cls):
            print('Personel listesi:')
            for kişi in cls.__personel:
                print(kişi)

        def isim_değiştir(self, yeni_isim):
            kişi = self.__personel.index(self.__isim)
            self.__personel[kişi] = yeni_isim
            print('yeni isim: ', yeni_isim)

Tek alt çizgi ile::

    class Çalışan():
        _personel = []

        def __init__(self, isim):
            self._isim = isim
            self.personele_ekle()

        def personele_ekle(self):
            self._personel.append(self._isim)
            print('{} adlı kişi personele eklendi'.format(self._isim))

        @classmethod
        def personeli_görüntüle(cls):
            print('Personel listesi:')
            for kişi in cls._personel:
                print(kişi)

        def isim_değiştir(self, yeni_isim):
            kişi = self._personel.index(self._isim)
            self._personel[kişi] = yeni_isim
            print('yeni isim: ', yeni_isim)

`personel` ve `self.isim` adlı nitelikleri çift alt çizgi ile gizlediğimizde
Python'ın isim bulandırma mekanizmasını işleteceğini, tek alt çizgi ile
gizlediğimizde ise bu mekanizmanın işletilmeyeceğini biliyorsunuz.

Peki size şöyle bir soru sorayım:

Acaba, personel listesindeki bir ismi, mesela yalnızca şöyle bir komut vererek
değiştiremez miyiz?

::

    >>> ç1.isim = 'Emre'

Elbette değiştirebiliriz. Ancak bunun için özel bir araçtan yararlanmamız
gerekir. Bu iş için `@property` adlı özel bir bezeyiciyi kullanacağız.

Dikkatlice bakın::

    class Çalışan():
        _personel = []

        def __init__(self, isim):
            self._isim = isim
            self.personele_ekle()

        def personele_ekle(self):
            self._personel.append(self._isim)
            print('{} adlı kişi personele eklendi'.format(self._isim))

        @classmethod
        def personeli_görüntüle(cls):
            print('Personel listesi:')
            for kişi in cls._personel:
                print(kişi)

        @property
        def isim(self):
            return self._isim

        @isim.setter
        def isim(self, yeni_isim):
            kişi = self._personel.index(self.isim)
            self._personel[kişi] = yeni_isim
            print('yeni isim: ', yeni_isim)

Bu kodları çalıştırdığınızda, tıpkı yukarıda bahsettiğimiz gibi, herhangi bir
çalışanın ismini yalnızca şu şekilde değiştirebildiğinizi göreceksiniz::

    >>> ç1.isim = 'Emre'

Üstelik bu kod, isim değişikliğinin personel listesine de yansımasını sağlıyor::

    >>> Çalışan.personeli_görüntüle()

    Emre

Birazdan bu kodları derinlemesine inceleyeceğiz. Ama isterseniz öncelikle şu
`@property` bezeyicisinden biraz söz edelim. Böylelikle yukarıdaki kodları
anlamamız kolaylaşır.

Metottan Niteliğe
===================

Şimdiye kadar verdiğimiz örneklerden anlamış olabileceğiniz gibi, bir sınıf
içinde salt verileri tutan değişkenlere 'nitelik' adı veriyoruz. Mesela::

    class Falanca():
        nitelik = 'nitelik'

        def __init__(self):
            self.nitelik = 'nitelik'

Burada `nitelik` bir sınıf niteliği, `self.nitelik` ise bir örnek niteliğidir.

Buna karşılık, bir sınıf içinde fonksiyon biçiminde yer alan ve bir işlemi veya
prosedürü yerine getiren öğelere ise metot adı veriyoruz. Mesela::

    class Falanca():
        def __init__(self):
            pass

        def örnek_fonk(self):
            pass

        @classmethod
        def sınıf_fonk(cls):
            pass

        @staticmethod
        def statik_fonk():
            pass

Burada ``örnek_fonk()`` adlı fonksiyon bir örnek metodu, ``sınıf_fonk()`` adlı
fonksiyon bir sınıf metodu, ``statik_fonk()`` adlı fonksiyon ise bir statik
metottur. Metotlar ile niteliklerin gerçekleştirebilecekleri işlemlerin
karmaşıklığının birbirinden farklı olmasının yanı sıra, bunlar arasında kullanım
açısından da farklılık vardır. Mesela ``Falanca()`` sınıfı içindeki `nitelik`
adlı sınıf niteliğini şu şekilde kullanıyoruz::

    >>> Falanca.nitelik
    >>> Falanca.nitelik = 'yeni değer'

Aynı sınıf içindeki ``sınıf_fonk()`` adlı sınıf metoduna ise şöyle erişiyoruz::

    >>> Falanca.sınıf_fonk()

Niteliklerin aksine, metotlarda atama yoluyla değer değiştirme gibi bir şey söz
konusu değildir. Yani şuna benzer bir şey yazamayız::

    >>> Falanca.sınıf_fonk() = 'yeni değer'

Eğer metot bir parametre alıyorsa (yukarıdaki örneklerde metotlar parametre
almıyor), bu parametreyi kullanarak metotla iletişim kurabiliriz. Mesela::

    >>> Falanca.sınıf_fonk(yeni_değer)

*Property* kelimesi (*attribute* kelimesine benzer bir şekilde) İngilizcede
'özellik, nitelik' gibi anlamlara gelir. Kelime anlamına uygun olarak,
`@property` bezeyicisinin yaptığı en temel iş, bir metodu, nitelik gibi
kullanılabilecek hale getirmektir. Çok basit bir örnek verelim::

    class Program():
        def __init__(self):
            pass

        def versiyon(self):
            return '0.1'

Burada ``versiyon()`` adlı bir örnek metodu tanımladık. Bu programı şöyle
kullanıyoruz::

    >>> program = Program()
    >>> program.versiyon()

    '0.1'

Şimdi programımızda şu değişikliği yapalım::

    class Program():
        def __init__(self):
            pass

        @property
        def versiyon(self):
            return '0.1'

Burada ``versiyon()`` adlı metodu `@property` bezeyicisi ile 'bezedik'. Böylece
bu metodu bir 'nitelik' haline getirmiş olduk. Artık bunu şöyle kullanabiliriz::

    >>> program = Program()
    >>> program.versiyon

    '0.1'

``versiyon()`` fonksiyonunu, `@property` bezeyicisi yardımıyla bir niteliğe
dönüştürdüğümüz için, artık bu fonksiyonu parantezsiz kullandığımıza dikkat
edin.

Gördüğünüz gibi, `@property` bezeyicisinin ilk görevi bir metodu niteliğe
dönüştürmek. Peki acaba neden bir metodu niteliğe dönüştürmek istiyor
olabiliriz?

Şöyle bir program yazdığınızı düşünün::

    class Program():
        def __init__(self):
            self.data = 0

Yazdığınız bu programı kullananlar, sınıf içindeki `data` niteliğine şu şekilde
erişiyor::

    >>> p = Program()
    >>> p.data

    0

Hatta duruma göre bu niteliği şu şekilde değişikliğe de uğratıyor::

    >>> p.data = 1

Günün birinde, 'data' kelimesi yerine 'veri' kelimesinin daha uygun olduğunu
düşünerek, 'data' kelimesini 'veri' olarak değiştirmek istediğinizi varsayalım.
Bunun için kodlarınızda şu değişikliği yapabilirsiniz::

    class Program():
        def __init__(self):
            self.veri = 0

Ancak bu şekilde, programınızı eskiden beri kullananların, sizin yazdığınız bu
programı temel alarak oluşturdukları programları bozmuş oldunuz... Çünkü eğer bu
programdan faydalanan birisi, yazdığı kodda eski `self.data` değişkenini
kullanmışsa, yukarıdaki isim değişikliği yüzünden programı kullanılamaz hale
gelecektir. İşte bunu önlemek için `@property` bezeyicisini kullanabilirsiniz.

Dikkatlice bakın::

    class Program():
        def __init__(self):
            self.veri = 0

        @property
        def data(self):
            return self.veri

Bu şekilde, `self.data` niteliğine yapılan bütün çağrılar ``data()`` adlı metot
vasıtasıyla `self.veri` niteliğine yönlendirilecek. Böylece başkalarının bu
programı kullanarak yazdığı eski kodları bozmadan, programımızda istediğimiz
değişikliği yapmış olduk. Yani programımızda geriye dönük uyumluluğu (*backwards
compatibility*) sağlamış olduk.

Yukarıdaki kodlarda `@property` bezeyicisini kullanarak ``data()`` metodunu bir
niteliğe dönüştürdüğümüz için artık şöyle bir kullanım mümkün::

    >>> p = Program()
    >>> p.data

    0

    >>> p.veri

    0

Bu yapıda, `self.veri` üzerindeki değişiklikler `self.data` niteliğine de
yansıyacaktır::

    >>> p.veri = 5
    >>> p.data

    5

Salt Okunur Nitelikler
========================

`@property` bezeyicisinin bir başka kabiliyeti de salt okunur nitelikler
oluşturabilmesidir.

Mesela yukarıdaki programı temel alarak şöyle bir şey deneyelim::

    >>> p = Program()
    >>> p.data = 5

    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: can't set attribute

Gördüğünüz gibi, `data` niteliği üzerinde değişiklik yapamıyoruz. Dolayısıyla,
kodlarınızı kullananların değiştirmesini istemediğiniz, 'salt okunur' nitelikler
oluşturmak için `@property` bezeyicisinden yararlanabilirsiniz.

Veri Doğrulaması
==================

`@property` bezeyicisinin üç önemli işlevi bulunur:

+ Değer döndürmek
+ Değer atamak
+ Değer silmek

Yukarıdaki örneklerde bu bezeyicinin değer döndürme işlevini görmüştük. Şimdi
ise bu bezeyicinin değer atama işlevini anlamaya çalışalım.

Bildiğiniz gibi, `@property` bezeyicisinin 'değer döndürme' işlevini kullanarak,
bir niteliğe erişimi kısıtlayabiliyoruz. Örneğin, zamanında şöyle bir kod
yazdığımızı varsayalım::

    class Program():
        def __init__(self):
            self.sayı = 0

Daha sonra herhangi bir sebepten ötürü buradaki `self.sayı` niteliğine erişimi
kısıtlayıp bu niteliği üzerinde değişiklik yapılamaz hale getirmek istersek
`@property` bezeyicisinden yararlanabiliriz::

    class Program():
        def __init__(self):
            self._sayı = 0

        @property
        def sayı(self):
            return self._sayı

Gördüğünüz gibi, öncelikle `self.sayı` adlı niteliği, başına bir alt çizgi
getirerek normal erişime kapatmak istediğimizi belirttik. Bu kodları görenler,
`sayı` niteliğinin yarı-gizli bir üye olduğunu anlayıp ona göre davranacak.
Ayrıca biraz sonra tanımlayacağımız ``sayı()`` fonksiyonuyla bu değişkenin
adının birbirine karışmaması için de bir önlem almış olacağız. Python'da bir
değişkenin adını değiştirmeden o değişkene erişimi kontrol altına almak
istediğimizde tek alt çizgi kullanmak tercih edilen bir yöntemdir.

Daha sonra da ``sayı()`` fonksiyonumuzu tanımlıyoruz::

    @property
    def sayı(self):
        return self._sayı

Bu ``sayı()`` fonksiyonunu `@property` ile bezediğimiz için, fonksiyon bir
niteliğe dönüştü ve `sayı` değişkenini salt okunur hale getirdi. Eğer amacınız
değişkeni salt okunur hale getirmek değilse `@property` ile bezediğimiz
fonksiyon için bir `setter` parametresi tanımlayabilirsiniz. Nasıl mı?
Dikkatlice inceleyin::

    class Program():
        def __init__(self):
            self._sayı = 0

        @property
        def sayı(self):
            return self._sayı

        @sayı.setter
        def sayı(self, yeni_değer):
            self._sayı = yeni_değer
            return self._sayı

`@property` ile bezeyerek bir nitelik haline getirdiğiniz fonksiyonu yazılabilir
hale getirmek ve bu yazma işleminin nasıl olacağını belirlemek için özel bir
`.setter` bezeyicisi ile bezenmiş yeni bir fonksiyon tanımlayabilirsiniz.

Biz yukarıda, yine `sayı` adını taşıyan, `.setter` ile bezenmiş bir fonksiyon
daha tanımladık::

    @sayı.setter
    def sayı(self, yeni_değer):
        self._sayı = yeni_değer
        return self._sayı

Yukarıdaki kodları çalıştırdığımızda, `_sayı` değişkenine `sayı` adı ile normal
bir şekilde erişip istediğimiz değişikliği yapabiliyoruz::

    >>> p = Program()
    >>> p.sayı

    0

    >>> p.sayı = 5
    >>> p.sayı

    5

Gördüğünüz gibi, artık `sayı` değişkeni, kendisi için bir `.setter` bezeyicisi
tanımlamış olmamız sayesinde değişiklik kabul ediyor.

`.setter` bezeyicisini, bir niteliği yazılabilir hale getirmenin yanı sıra,
doğrulama işlemleri için de kullanabilirsiniz.

Basit bir örnek verelim::

    class Program():
        def __init__(self):
            self._sayı = 0

        @property
        def sayı(self):
            return self._sayı

        @sayı.setter
        def sayı(self, yeni_değer):
            if yeni_değer % 2 == 0:
                self._sayı = yeni_değer
            else:
                print('çift değil!')

            return self.sayı

Burada, `self.sayı` niteliğinin değerini çift sayılarla sınırlandırdık. Veri
doğrulama/kısıtlama işlemini `.setter` bezeyicisi içinden gerçekleştirdiğimize
dikkatinizi çekmek isterim. Buna göre, eğer `self.sayı` değişkenine girilen
değer bir çift sayı ise bu değişikliği kabul ediyoruz. Aksi halde 'çift değil!'
uyarısı gösteriyoruz::

    >>> p = Program()
    >>> p.sayı = 2
    >>> p.sayı = 5

    'çift değil!'

Bu arada, `.setter` dışında `.deleter` adlı özel bir `@property` bezeyicisi daha
bulunur. Bunu da bir değeri silmek için kullanıyoruz::

    class Program():
        def __init__(self):
            self._sayı = 0

        @property
        def sayı(self):
            return self._sayı

        @sayı.setter
        def sayı(self, yeni_değer):
            if yeni_değer % 2 == 0:
                self._sayı = yeni_değer
            else:
                print('çift değil!')

            return self.sayı

        @sayı.deleter
        def sayı(self):
            del self._sayı

Gördüğünüz gibi, `@property` bezeyicisini kullanırken üç ayrı metot
tanımlıyoruz:

+ İlgili niteliğe nasıl ulaşacağımızı gösteren bir metot: Bu metodu `@property`
  ile beziyoruz.

+ İlgili niteliği nasıl ayarlayacağımızı gösteren bir metot: Bu metodu
  `@metot_adı.setter` şeklinde beziyoruz.

+ İlgili niteliği nasıl sileceğimizi gösteren bir metot: Bu metodu
  `@metot_adı.deleter` şeklinde beziyoruz.

Bu bölümde nesne tabanlı programlamanın orta-ileri düzey sayılabilecek yönlerine
temas ettik. Artık nesne tabanlı programlamanın temellerinden biraz daha
fazlasını bildiğinizi rahatlıkla iddia edebilirsiniz.
