İçeriğe geç
Educora
İleri25 dk19 / 42

İleri NYP: özel metotlar, özellikler ve MRO

Sınıflarını yerleşik tipler gibi davrandır: `__repr__`/`__str__`, `__eq__`/`__lt__`/`__hash__`, işleç aşırı yükleme, kapsayıcı protokolü, `@property`, sınıf ve statik metotlar, MRO, `super()` ve `abc.ABC`.

Kendini test et
Bu derste öğreneceklerin
  • __repr__, __str__, __eq__, __lt__, __hash__ ve işleç metotlarını doğru yazmak
  • @property ile 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.ABC ile 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.

Python
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:

Python
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:

Python
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:

Python
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:

Python
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:

Python
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 False

Kalı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**:

Python
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:

Python
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'
Alıştırma

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.

Alıştırma · Python
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
Alıştırma

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.

Alıştırma · Python
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çin NotImplemented döndürmelidir; __eq__ tanımlamak, geri eklemediğin sürece __hash__'ı kaldırır.
  • @property, doğrulamayı nitelik sözdiziminin arkasına gizler; @classmethod cls alır (alternatif yapıcılar), @staticmethod ikisini de almaz.
  • super(), gerçek nesnenin MRO'sunu izler; bir ABC'nin soyut metotları nesne oluşturulmadan önce gerçeklenmelidir.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
print([obj]), nesneyi listenin içinde göstermek için hangi metodu kullanır?