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.

Yorumlar

Önemli Not

Sorularınızı yorumlarda dile getirmek yerine Yazbel Forumunda sorarsanız çok daha hızlı cevap alabilirsiniz.
Belgelerdeki bir hata veya eksiği dile getirecekseniz lütfen yorumları kullanmak yerine Github'da bir konu (issue) açın.
Eğer yazdığınız yorum içinde kod kullanacaksanız kodlarınızı <pre><code> etiketleri içine alın. Örneğin:
        <pre><code class="python">
        print("Merhaba Dünya!")
        </code></pre>