Kurs
Python tanımlayıcıları (descriptors) ya da daha genel adıyla tanımlayıcılar, sınıflar arasında paylaşılabilen, yeniden kullanılabilir kod yazmanız için güçlü bir teknik sunar. Kalıtıma benziyor gibi görünse de teknik olarak aynı şey değildir. Nitelik erişimini yakalamanın genel amaçlı bir yoludur. Tanımlayıcılar; property’lerin, static method’ların, class method’ların, super metodlarının vb. arkasındaki mekanizmadır.
Tanımlayıcılar Python 2.2 sürümünde eklendi ve o zamandan beri geleneksel sınıflara yeni bir tarz kazandıran sihirli yapılar olarak görüldü. Başka bir sınıfta yönetilen özellikler (managed properties) oluşturmanıza olanak tanıyan sınıflardır. Özellikle, birçok nedenle ilgi çekici hale gelen __get__(), __set__() ve __delete__() yöntemlerinin arayüzünü uygularlar. Örneğin, Python’da görmüş olabileceğiniz sınıf dekoratörleri ve property dekoratörleri gibi.
Basitçe söylemek gerekirse, bir nesne için __get__(), __set()__ veya __delete()__ yöntemlerini uygulayan bir sınıfa "Tanımlayıcı" denir. Python’un resmi belgelerinden doğrudan alıntıyla, bir tanımlayıcı, öznitelik erişimi tanımlayıcı protokolündeki yöntemlerle geçersiz kılınmış olan, bağlama davranışına sahip bir nesne özniteliğidir. Bu yöntemler __get__(), __set__() ve __delete__()’dir (Kaynak).
Tanımlayıcılar bağlamında bağlama davranışı, belirli bir değişken, nesne veya veri kümesi için değerin nasıl ayarlanabileceğinin, sorgulanabileceğinin (get) veya silinebileceğinin bağlanması anlamına gelir. Bu etkileşimin tamamı, yalnızca üzerinde ayarladığınız veriye uygulandığı için bu veri parçasına bağlanır ve sadece ilgili veriye özgü olur.
Tanımlayıcılar veri tanımlayıcıları ve veri olmayan tanımlayıcılar olarak sınıflandırılabilir. Yazdığınız tanımlayıcı yalnızca __get__() yöntemine sahipse buna veri olmayan (non-data) tanımlayıcı denir; buna karşılık __set__() ve __delete__() yöntemlerini içeren uygulama veri tanımlayıcısı (data descriptor) olarak adlandırılır. Veri olmayan tanımlayıcılar yalnızca okunabilirdir; veri tanımlayıcıları ise hem okunabilir hem yazılabilir.
Tanımlayıcıların bir sınıfa atandığını, bir sınıfın örneğine atanmadığını unutmamak önemlidir. Sınıfı değiştirmek, kodunu tetiklemek yerine tanımlayıcının kendisini geçersiz kılar veya siler (IBM Developer).
Son olarak, tanımlayıcı sınıf yalnızca bu üç yöntemle sınırlı değildir; yani get, set ve delete yöntemlerinin yanı sıra başka öznitelikler de içerebilir.
Haydi IBM Developer sayfasından ilhamla get, set ve delete yöntemlerini biraz daha ayrıntılı inceleyelim:
self, oluşturduğunuz tanımlayıcının örneğidir (Real Python).object, tanımlayıcınızın eklendiği nesnenin örneğidir (Real Python).type, tanımlayıcının eklendiği nesnenin türüdür (Real Python).value, tanımlayıcının özniteliğine atanan değerdir. get(self, object, type) set(self, object, value) delete(self, object)-
__get__(), bir özniteliğe eriştiğinizde ya da bazı bilgileri çıkarmak istediğinizde çalışır. Özniteliğin değerini döndürür; istenen öznitelik yoksa AttributeError istisnası yükseltir. -
__set__(), bir öznitelik atama işlemi sırasında çağrılır ve özniteliğin değerini ayarlar. Bir şey döndürmez; ancak AttributeError istisnası yükseltebilir. -
__delete__(), bir silme işlemini kontrol eder; yani bir özniteliği bir nesneden silmek istediğinizde çağrılır. Bir şey döndürmez.
Şimdi Tanımlayıcıların amacını birkaç örnekle anlayalım!
Tanımlayıcıların Amacı
car adında, make, model ve fuel_cap olmak üzere üç özniteliği olan bir sınıf tanımlayalım. Sınıfın özniteliklerini başlatmak için __init__() yöntemini kullanacaksınız. Ardından, nesneyi oluştururken sınıfa ilettiğiniz üç özniteliğin çıktısını döndüren sihirli __str__() fonksiyonunu kullanacaksınız.
__str__() yönteminin nesnenin dize gösterimini döndürdüğünü unutmayın. Bir sınıfın nesnesi üzerinde print() veya str() fonksiyonu çağrıldığında çalışır.
class Car:
def __init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
def __str__(self):
return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.
Gördüğünüz gibi, çıktı gayet iyi görünüyor!
Şimdi arabanın yakıt kapasitesini eksi 40 olarak değiştirelim.
car2 = Car("BMW","X7",-40)
print(car2)
BMW model X7 with a fuel capacity of -40 ltr.
Bir dakika, burada bir terslik var, değil mi? Arabanın yakıt kapasitesi asla negatif olamaz. Ancak Python bunu hatasız bir girdi olarak kabul ediyor. Bunun nedeni, Python’un açık tip denetimini desteklemeyen dinamik bir programlama dili olmasıdır.
Bu sorunu önlemek için __init__() yöntemine bir if koşulu ekleyelim ve girilen yakıt kapasitesinin geçerli olup olmadığını kontrol edelim. Girilen yakıt kapasitesi geçersizse, bir ValueError istisnası yükseltelim.
class Car:
def __init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
if self.fuel_cap < 0:
raise ValueError("Fuel Capacity can never be less than zero")
def __str__(self):
return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car1 = Car("BMW","X7",40)
print(car1)
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car("BMW","X7",-40)
print(car2)
----------------------------------------
ValueErrorTraceback (most recent call last)
<ipython-input-22-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
2 print(car2)
<ipython-input-20-1e154783588d> in __init__(self, make, model, fuel_cap)
5 self.fuel_cap = fuel_cap
6 if self.fuel_cap < 0:
----> 7 raise ValueError("Fuel Capacity can never be less than zero")
8
9 def __str__(self):
ValueError: Fuel Capacity can never be less than zero
Yukarıdaki çıktıda gördüğünüz gibi, yakıt kapasitesi sıfırın altında olduğunda program bir ValueError yükselttiği için şu an her şey yolunda.
Ancak başka bir sorun daha olabilir; örneğin girilen yakıt kapasitesi ondalık bir değer ya da bir string olabilir. Sadece yakıt kapasitesi değil, arabanın make ve model değerleri de bir tamsayı olabilir. Bu durumların hiçbirinde program bir istisna yükseltmeyecektir.
class Car:
def __init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
if self.fuel_cap < 0:
raise ValueError("Fuel Capacity can never be less than zero")
def __str__(self):
return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car(-40,"X7",40)
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.
Yukarıdaki durumu da ele almak için bir başka if koşulu eklemeyi ya da tip denetimi için isinstance yönteminden yararlanmayı düşünebilirsiniz.
Bu kez hatayı ele almak için yerleşik isinstance yöntemini kullanalım.
class Car:
def __init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
if isinstance(self.make, str):
print(self.make)
else:
raise ValueError("Make of the car can never be an integer")
if self.fuel_cap < 0:
raise ValueError("Fuel Capacity can never be less than zero")
def __str__(self):
return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2 = Car(-40,"X7",40)
print(car2)
----------------------------------------
ValueErrorTraceback (most recent call last)
<ipython-input-34-75b08cba454f> in <module>
----> 1 car2 = Car(-40,"X7",40)
2 print(car2)
<ipython-input-31-175690bf3b98> in __init__(self, make, model, fuel_cap)
7 print(self.make)
8 else:
----> 9 raise ValueError("Make of the car can never be an integer")
10
11 if self.fuel_cap < 0:
ValueError: Make of the car can never be an integer
Harika! Böylece bu hatayı da ele alabildiniz.
Ancak ya daha sonra yakıt kapasitesi özniteliğini bilerek eksi 40’a değiştirmek isterseniz? Bu durumda çalışmayacaktır; çünkü tip denetimi yalnızca bir kez, __init__() yönteminde yapılır. Bildiğiniz gibi __init__() yöntemi bir kurucudur ve yalnızca sınıfın bir nesnesini oluşturduğunuzda bir kez çağrılır. Dolayısıyla özel tip denetimi daha sonra başarısız olacaktır.
Bunu bir örnekle anlayalım.
class Car:
def __init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
if isinstance(self.make, str):
print(self.make)
else:
raise ValueError("Make of the car can never be an integer")
if self.fuel_cap < 0:
raise ValueError("Fuel Capacity can never be less than zero")
def __str__(self):
return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
BMW
BMW model X7 with a fuel capacity of 40 ltr.
car2.make = -40
print(car2)
-40 model X7 with a fuel capacity of 40 ltr.
İşte böyle! Tip denetimini baypas edebildiniz.
Şimdi şöyle düşünün: Yakıt tüketimi, fiyat, aksesuarlar vb. tip denetimi gerektiren birçok başka özniteliğiniz de olabilir ve bu özniteliklerin bazılarının yalnızca okuma erişimine sahip olmasını da isteyebilirsiniz. Bu sinir bozucu olmaz mıydı?
İşte tüm bu sorunları ele almak için Python’un kurtarıcısı Tanımlayıcılar devreye girer!
Yukarıda öğrendiğiniz gibi, bir nesne için __get__(), __set()__ veya __delete()__ sihirli yöntemlerini uygulayan herhangi bir sınıfa Tanımlayıcı denir. Ayrıca bir özniteliğin nasıl davranması gerektiği üzerinde, örneğin okuma ya da yazma erişimine sahip olup olmayacağı gibi, ek kontrol sağlarlar.
Şimdi yukarıdaki örneği Python Tanımlayıcı yöntemlerini ekleyerek genişletelim.
class Descriptor:
def __init__(self):
self.__fuel_cap = 0
def __get__(self, instance, owner):
return self.__fuel_cap
def __set__(self, instance, value):
if isinstance(value, int):
print(value)
else:
raise TypeError("Fuel Capacity can only be an integer")
if value < 0:
raise ValueError("Fuel Capacity can never be less than zero")
self.__fuel_cap = value
def __delete__(self, instance):
del self.__fuel_cap
class Car:
fuel_cap = Descriptor()
def __init__(self,make,model,fuel_cap):
self.make = make
self.model = model
self.fuel_cap = fuel_cap
def __str__(self):
return "{0} model {1} with a fuel capacity of {2} ltr.".format(self.make,self.model,self.fuel_cap)
car2 = Car("BMW","X7",40)
print(car2)
40
BMW model X7 with a fuel capacity of 40 ltr.
Tanımlayıcı sınıfı ilk bakışta anlaşılmaz geliyorsa endişelenmeyin. Parçalara ayıralım ve her bir yöntemin ne yaptığını anlayalım.
- Descriptor sınıfının
__init__()yöntemi yerel değişken__fuel_cap’i sıfıra ayarlar. Başındakidunder(çift alt çizgi) bu değişkenin özel (private) olduğunu belirtir. Başta dunder kullanmanın amacı, Descriptor sınıfının yakıt kapasitesi özniteliği ile Car sınıfındakini ayırt etmektir.
- Artık bildiğiniz gibi
__get__()yöntemi öznitelikleri almak için kullanılır ve yakıt kapasitesi değişkenini döndürür. Üç argüman alır: tanımlayıcı nesnenin kendisi, tanımlayıcı nesne örneğini içeren sınıfın örneği (yanicar2) ve son olarak örneğin ait olduğu sınıf (yaniCarsınıfı). Bu yöntemde,__set__()yönteminde ayarlananfuel_capdeğerini döndürürsünüz.
__set__()yöntemi, bir özniteliğe değer atandığında çağrılır ve__get__()yönteminden farklı olarak bir şey döndürmez. Tanımlayıcı nesnenin yanı sıra iki argümanı vardır:__get__()ile aynı olan örnek (instance) ve özniteliğe atadığınızvalue. Bu yöntemde,fuel_capözniteliğine atamak istediğiniz değerin tamsayı olup olmadığını kontrol edersiniz. Değilse birTypeErroristisnası yükseltirsiniz. Ardından aynı yöntemde değerin sıfırdan küçük olup olmadığını kontrol eder, küçükse bu kez birValueErroristisnası yükseltirsiniz. Hataları kontrol ettikten sonrafuel_capözniteliğini bu değere güncellersiniz.
- Son olarak
__delete__()yöntemi, bir öznitelik bir nesneden silindiğinde çağrılır ve__set__()yöntemi gibi bir şey döndürmez.
Car sınıfı öncekiyle aynı kalır. Ancak yaptığınız tek değişiklik, Descriptor() sınıfının fuel_cap örneğini eklemektir. Daha önce belirtildiği gibi, tanımlayıcı sınıfının örneğinin bir örnek özniteliği olarak değil, sınıf özniteliği olarak eklenmesi gerektiğine dikkat edin.
__init__() yönteminde yerel fuel_cap değişkenini fuel_cap örneğine ayarladığınız anda, Descriptor sınıfının __set__() yöntemi çağrılır.
Şimdi yakıt kapasitesini negatif bir değere değiştirelim ve programın bir ValueError istisnası yükseltip yükseltmediğine bakalım.
car2 = Car("BMW","X7",-40)
print(car2)
-40
----------------------------------------
ValueErrorTraceback (most recent call last)
<ipython-input-115-1c3d23be72f7> in <module>
----> 1 car2 = Car("BMW","X7",-40)
2 print(car2)
<ipython-input-107-3e1f3e97d3c7> in __init__(self, make, model, fuel_cap)
4 self.make = make
5 self.model = model
----> 6 self.fuel_cap = fuel_cap
7
8 def __str__(self):
<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
11
12 if value < 0:
---> 13 raise ValueError("Fuel Capacity can never be less than zero")
14
15 self.__fuel_cap = value
ValueError: Fuel Capacity can never be less than zero
burada hatırlarsanız, öznitelik değerini daha sonra negatif bir sayıya değiştirdiğinizde tip denetimi başarısız olmuştu; çünkü tip denetimi yalnızca __init__() yönteminde bir kez uygulanıyordu. Şimdi fuel_cap değerini bir string’e güncelleyelim ve hata verip vermediğine bakalım.
car2.fuel_cap = -1
-1
----------------------------------------
ValueErrorTraceback (most recent call last)
<ipython-input-120-dea9dbe96ebe> in <module>
----> 1 car2.fuel_cap = -1
<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
11
12 if value < 0:
---> 13 raise ValueError("Fuel Capacity can never be less than zero")
14
15 self.__fuel_cap = value
ValueError: Fuel Capacity can never be less than zero
car2.fuel_cap = "BMW"
----------------------------------------
TypeErrorTraceback (most recent call last)
<ipython-input-121-0b316a9872c6> in <module>
----> 1 car2.fuel_cap = "BMW"
<ipython-input-106-0b252695aeed> in __set__(self, instance, value)
8 print(value)
9 else:
---> 10 raise TypeError("Fuel Capacity can only be an integer")
11
12 if value < 0:
TypeError: Fuel Capacity can only be an integer
Mükemmel! Gördüğünüz gibi, yakıt kapasitesi özniteliğini daha sonra güncellediğinizde de çalışıyor.
Ancak Tanımlayıcılarda küçük bir sorun var: Sınıfın yeni bir örneğini (ikinci bir örnek) oluşturduğunuzda, önceki örneğin değeri üzerine yazılıyor. Bunun nedeni, Tanımlayıcıların örneğe değil sınıfa bağlı olmasıdır.
Bunu aşağıdaki örnekle anlayalım.
car3 = Car("BMW","X7",48) #created a new instance 'car3' with different values
48
car2 örneğini yazdırdığınızda, değerlerin car3 tarafından üzerine yazıldığını gözlemleyeceksiniz.
print(car2)
BMW model X7 with a fuel capacity of 48 ltr.
Sonuç
Eğitimi tamamladığınız için tebrikler.
Bu eğitim, Python’a hâkim olan ve ileri seviyede ustalaşmayı hedefleyenler içindi. Güzel bir alıştırma olarak, bugün bahsedilen örnek üzerine yazma (instance overriding) sorununu nasıl çözebileceğinizi araştırmak isteyebilirsiniz.
Bu eğitimle ilgili tüm sorularınızı aşağıdaki yorumlar bölümünde sormaktan çekinmeyin.
Python hakkında daha fazla bilgi edinmek isterseniz, DataCamp’in kapsamlı Python Programming beceri yolunu inceleyin.
Python Fonksiyonları Eğitimimize göz atın.
