__repr__,__str__,__eq__,__lt__,__hash__ve işleç metotlarını doğru yazmak@propertyile doğrulama eklemek, sınıf ve statik metotları yerinde kullanmak- Çoklu kalıtımda MRO'yu ve
super()'in çalışmasını açıklamak abc.ABCile soyut bir temel sınıf oluşturmak
+ neden sayıları toplar, karakter dizilerini ve listeleri birleştirir? len(), in, sorted() ve print() neden tamamen farklı tipteki nesnelerle çalışır? Çünkü Python bu tür her işlemi bir özel metot çağrısına dönüştürür: a + b, a.__add__(b) olur; len(x) ise x.__len__() olur. Bunlara dunder metotlar da denir (İngilizce double underscore, yani çift alt çizgi). Onları kendi sınıfında yazarsan nesneleri yerleşik tipler gibi davranır. Bu kurallar bütününe Python veri modeli denir.
__repr__ ve __str__: nesne kendini nasıl tanıtır
Her sınıf, <__main__.Money object at 0x7f…> gibi bir şey yazdıran bir __repr__ metodunu kalıtımla alır; hata ayıklamada işe yaramaz. __repr__ tek anlamlı olmalı ve geliştiriciler için yazılmalıdır; ideal olarak nesneyi oluşturan koda benzer. __str__ ise kullanıcılar için okunaklı sürümdür. print() ve f-string'ler __str__'u, etkileşimli konsol, listeler ve !r dönüşümü ise __repr__'ı kullanır. Yalnızca __repr__ tanımlıysa str() da onu kullanır.
class Money:
def __init__(self, amount, currency='AZN'):
self.amount = amount
self.currency = currency
def __repr__(self):
return f'Money({self.amount!r}, {self.currency!r})'
def __str__(self):
return f'{self.amount:.2f} {self.currency}'
price = Money(12.5)
print(price)
print(repr(price))
print([price, Money(3, 'USD')])
print(f'{price} | {price!r}')▸ Beklenen çıktı
12.50 AZN Money(12.5, 'AZN') [Money(12.5, 'AZN'), Money(3, 'USD')] 12.50 AZN | Money(12.5, 'AZN')
Eşitlik, sıralama ve hash
__eq__ yoksa == kimliği karşılaştırır: aynı verilere sahip iki nesne eşit sayılmaz. Diğer işlenenin tipi desteklenmiyorsa __eq__ hata fırlatmamalı, özel **NotImplemented değerini döndürmelidir: o zaman Python yansıtılmış işlemi diğer nesnede dener, o da olmazsa kimliği karşılaştırır. functools.total_ordering** dekoratörü <=, > ve >= işlemlerini __eq__ ve __lt__'ten türetir:
from functools import total_ordering
@total_ordering
class Money:
def __init__(self, amount, currency='AZN'):
self.amount, self.currency = amount, currency
def __eq__(self, other):
if not isinstance(other, Money):
return NotImplemented
return (self.amount, self.currency) == (other.amount, other.currency)
def __lt__(self, other):
if not isinstance(other, Money) or other.currency != self.currency:
return NotImplemented
return self.amount < other.amount
def __hash__(self):
return hash((self.amount, self.currency))
prices = [Money(5), Money(2.5), Money(10)]
print([m.amount for m in sorted(prices)])
print(Money(5) == Money(5), Money(5) >= Money(2.5), Money(5) == 5)
print(len({Money(1), Money(1)}))▸ Beklenen çıktı
[2.5, 5, 10] True True False 1
İşleç aşırı yükleme ve kapsayıcı protokolü
Aritmetik de aynı şekilde çalışır. a + b için Python önce a.__add__(b)'yi çağırır; bu NotImplemented döndürürse yansıtılmış metot olan b.__radd__(a) denenir. Aşağıdaki 2 * v bu yüzden __rmul__ ister: int vektörler hakkında hiçbir şey bilmez. __bool__, if v: ifadesinin anlamını; __abs__ ise yerleşik abs()'nin davranışını belirler:
class Vector:
def __init__(self, x, y):
self.x, self.y = x, y
def __repr__(self):
return f'Vector({self.x}, {self.y})'
def __add__(self, other):
return Vector(self.x + other.x, self.y + other.y)
def __mul__(self, k):
return Vector(self.x * k, self.y * k)
__rmul__ = __mul__
def __abs__(self):
return (self.x ** 2 + self.y ** 2) ** 0.5
def __bool__(self):
return bool(self.x or self.y)
v = Vector(3, 4)
print(v + Vector(1, 1), v * 2, 2 * v)
print(abs(v), bool(Vector(0, 0)))▸ Beklenen çıktı
Vector(4, 5) Vector(6, 8) Vector(6, 8) 5.0 False
| İfade | Çağrılan metot |
|---|---|
a + b, a - b, a * b | __add__, __sub__, __mul__ (__radd__, …) |
a == b, a < b | __eq__, __lt__ |
len(a), a[i], x in a | __len__, __getitem__, __contains__ |
for x in a | __iter__ (__getitem__) |
abs(a), bool(a), hash(a) | __abs__, __bool__, __hash__ |
str(a), repr(a), a() | __str__, __repr__, __call__ |
a += b için Python önce __iadd__'i (nesneyi yerinde değiştirebilen sürümü) arar, bulamazsa __add__'e geçer. Bu yüzden += bir listeyi yerinde değiştirir, demetler ve karakter dizileri için ise yeni bir nesne oluşturur. __call__ metodu bir nesneyi fonksiyon gibi çağırmayı sağlar: obj().
Sınıfın bir koleksiyon gibi de davranabilir. __len__, len()'e; __getitem__ indekslemeye ve dilimlemeye (dilim nesnesi doğrudan listeye aktarılır); __contains__ ise in işlecine hizmet eder. for bile çalışır: sınıfta __iter__ yoksa Python, IndexError görünene kadar __getitem__'ı 0, 1, 2… ile çağırır:
class Playlist:
def __init__(self, *songs):
self._songs = list(songs)
def __len__(self):
return len(self._songs)
def __getitem__(self, index):
return self._songs[index]
def __contains__(self, song):
return song.lower() in (s.lower() for s in self._songs)
p = Playlist('Morning', 'Rain', 'Finale')
print(len(p), p[0], p[-1], p[1:])
print('rain' in p)
for song in p:
print('>', song)▸ Beklenen çıktı
3 Morning Finale ['Rain', 'Finale'] True > Morning > Rain > Finale
@property, @classmethod ve @staticmethod
Bir özellik (property) dışarıdan sıradan bir nitelik gibi görünür, ama onu okumak ya da ona değer atamak bir metodu çalıştırır. Bu sayede sınıfı kullanan kodu değiştirmeden sonradan doğrulama ekleyebilirsin; t.celsius = -300 yine sıradan bir atamadır. __init__'in de setter'dan geçtiğine dikkat et; bu yüzden yeni bir nesne bile geçersiz bir durumda başlayamaz. fahrenheit gibi setter'ı olmayan bir özellik salt okunurdur ve her seferinde yeniden hesaplanır:
class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError('below absolute zero')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
t = Temperature(25)
print(t.celsius, t.fahrenheit)
try:
t.celsius = -300
except ValueError as e:
print('error:', e)▸ Beklenen çıktı
25 77.0 error: below absolute zero
Bir sınıf metodu (@classmethod), bir örnek yerine sınıfın kendisini cls olarak alır. Başlıca kullanımı from_csv gibi alternatif yapıcılardır: metot cls(...) çağırdığı için alt sınıf otomatik olarak kendi tipinde nesneler alır. Statik metot (@staticmethod) ne self ne de cls alır; mantıksal olarak sınıfa ait olduğu için onun ad alanında yaşayan sıradan bir fonksiyondur. Tüm örneklerin paylaştığı count sınıf niteliğine de dikkat et:
class Student:
count = 0
def __init__(self, name, grade):
self.name, self.grade = name, grade
Student.count += 1
@classmethod
def from_csv(cls, line):
name, grade = line.split(',')
return cls(name, int(grade))
@staticmethod
def is_valid_grade(grade):
return 1 <= grade <= 11
def __repr__(self):
return f'{type(self).__name__}({self.name!r}, {self.grade})'
class Graduate(Student):
pass
print(Student.from_csv('Aysel,9'), Graduate.from_csv('Murad,11'))
print(Student.count, Student.is_valid_grade(12))▸ Beklenen çıktı
Student('Aysel', 9) Graduate('Murad', 11)
2 FalseKalıtım, MRO ve super()
Çoklu kalıtımda Python'un metotları aramak için bir kurala ihtiyacı vardır. MRO'yu (method resolution order, metot çözümleme sırası) C3 algoritmasıyla hesaplar: alt sınıf, üst sınıflarından önce gelir; üst sınıflar yazıldıkları sırayı korur ve her sınıf yalnızca bir kez görünür. Bunu __mro__ içinde görebilirsin. Asıl nokta: **super() “benim üst sınıfım” değil, “gerçek nesnenin MRO'sundaki bir sonraki sınıf” demektir**:
class Base:
def save(self):
print('Base.save')
class Logged(Base):
def save(self):
print('Logged: before')
super().save()
class Validated(Base):
def save(self):
print('Validated: checking')
super().save()
class Order(Logged, Validated):
pass
print([c.__name__ for c in Order.__mro__])
Order().save()▸ Beklenen çıktı
['Order', 'Logged', 'Validated', 'Base', 'object'] Logged: before Validated: checking Base.save
Logged doğrudan Base'den türer, ama onun super().save() çağrısı Validated.save()'i çalıştırdı, çünkü Order'ın MRO'sunda sıradaki sınıf odur. Bu sayede Base.save() tam bir kez çalıştı. Mixin denen bu tür “iş birliği yapan” sınıflar, ancak her metot super() çağırırsa ve hiçbiri üst sınıfı Base.save(self) gibi doğrudan çağırmazsa çalışır.
Soyut temel sınıf, alt sınıfların yerine getirmesi gereken bir sözleşme tanımlar. abc.ABC'den türeyen ve @abstractmethod ile işaretlenmiş metotları olan bir sınıftan, bir alt sınıf bunların hepsini gerçeklemedikçe nesne oluşturulamaz; hata işin ortasında değil, nesne oluşturulurken ortaya çıkar:
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self): ...
def describe(self):
return f'{type(self).__name__} with area {self.area():.2f}'
class Rect(Shape):
def __init__(self, w, h):
self.w, self.h = w, h
def area(self):
return self.w * self.h
print(Rect(3, 4.5).describe())
try:
Shape()
except TypeError as e:
print(e)▸ Beklenen çıktı
Rect with area 13.50 Can't instantiate abstract class Shape without an implementation for abstract method 'area'
Money sınıfına __add__, __radd__ ve __eq__ ekle; böylece iki tutar toplanabilsin, bir liste sum() ile toplanabilsin ve sonuç karşılaştırılabilsin. Yabancı tipler için __add__ NotImplemented döndürmelidir.
class Money:
def __init__(self, amount):
self.amount = amount
def __repr__(self):
return f'Money({self.amount})'
# add __add__, __radd__ and __eq__ here
wallet = [Money(5), Money(10), Money(2)]
print(Money(1) + Money(2))
print(sum(wallet))
print(sum(wallet) == Money(17))▸ Beklenen çıktı
Money(3) Money(17) True
Stack sınıfına __len__ ve __repr__ ekle. bool(s) için ayrı bir metot yazma; boş yığın kendiliğinden False sayılmalıdır.
class Stack:
def __init__(self):
self._items = []
def push(self, item):
self._items.append(item)
def pop(self):
return self._items.pop()
# add __len__ and __repr__ here
s = Stack()
print(bool(s))
s.push('a')
s.push('b')
print(s, len(s), bool(s))
print(s.pop(), s)▸ Beklenen çıktı
False Stack(['a', 'b']) 2 True b Stack(['a'])
Önemli noktalar
- İşleçler ve yerleşik fonksiyonlar özel metotları çağırır:
a + b→__add__,len(x)→__len__,x in c→__contains__. __repr__geliştiriciler,__str__kullanıcılar içindir; kapsayıcılar__repr__'ı gösterir.__eq__, yabancı tipler içinNotImplementeddöndürmelidir;__eq__tanımlamak, geri eklemediğin sürece__hash__'ı kaldırır.@property, doğrulamayı nitelik sözdiziminin arkasına gizler;@classmethodclsalır (alternatif yapıcılar),@staticmethodikisini de almaz.super(), gerçek nesnenin MRO'sunu izler; birABC'nin soyut metotları nesne oluşturulmadan önce gerçeklenmelidir.
Kendini test et
10 soru. Her doğru cevap XP kazandırır.
print([obj]), nesneyi listenin içinde göstermek için hangi metodu kullanır?