Məzmuna keç
Educora
İrəli25 dəq19 / 42

Qabaqcıl OYP: xüsusi metodlar, xassələr və MRO

Siniflərini daxili tiplər kimi işlət: `__repr__`/`__str__`, `__eq__`/`__lt__`/`__hash__`, operatorların yüklənməsi, konteyner protokolu, `@property`, sinif və statik metodlar, MRO, `super()` və `abc.ABC`.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • __repr__, __str__, __eq__, __lt__, __hash__ və operator metodlarını düzgün yazmaq
  • @property ilə yoxlama əlavə etmək, sinif və statik metodları yerində işlətmək
  • Çoxlu varislikdə MRO-nu və super()-in işini izah etmək
  • abc.ABC ilə abstrakt baza sinfi yaratmaq

Nə üçün + ədədləri toplayır, sətirləri birləşdirir, siyahıları isə calayır? Nə üçün len(), in, sorted() və print() tamamilə fərqli tipli obyektlərlə işləyir? Çünki Python hər belə əməliyyatı xüsusi metodun çağırışına çevirir: a + b yazılışı a.__add__(b), len(x) isə x.__len__() olur. Bu metodlara dunder metodlar da deyilir (ingiliscə double underscore — ikiqat alt xətt). Onları öz sinfində yazsan, obyektləri daxili tiplər kimi davranacaq. Bu qaydalar toplusu Python verilənlər modeli adlanır.

__repr__ və __str__: obyekt özünü necə təqdim edir

Hər sinif <__main__.Money object at 0x7f…> kimi bir şey çap edən __repr__ metodunu irsən alır — sazlama üçün faydasızdır. __repr__ birmənalı olmalı və proqramçılar üçün yazılmalıdır; ideal halda obyekti yaradan koda oxşayır. __str__ isə istifadəçilər üçün oxunaqlı variantdır. print() və f-sətirlər __str__-dən, interaktiv konsol, siyahılar və !r çevirməsi isə __repr__-dən istifadə edir. Yalnız __repr__ yazılıbsa, str() da onu işlədir.

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}')
▸ Gözlənilən nəticə
12.50 AZN
Money(12.5, 'AZN')
[Money(12.5, 'AZN'), Money(3, 'USD')]
12.50 AZN | Money(12.5, 'AZN')

Bərabərlik, sıralama və heş

__eq__ olmasa, == obyektlərin eyniliyini yoxlayır: eyni verilənli iki obyekt bərabər sayılmır. Digər operandın tipi dəstəklənmirsə, __eq__ xəta qaldırmamalı, xüsusi **NotImplemented qiymətini qaytarmalıdır: onda Python əks əməliyyatı digər obyektdə sınayır, o da alınmasa, eyniliyi müqayisə edir. functools.total_ordering** dekoratoru __eq__ və __lt__ əsasında <=, > və >= operatorlarını özü yaradır:

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)}))
▸ Gözlənilən nəticə
[2.5, 5, 10]
True True False
1

Operatorların yüklənməsi və konteyner protokolu

Hesab əməlləri də eyni cür işləyir. a + b üçün Python əvvəlcə a.__add__(b) çağırır; o, NotImplemented qaytarsa, b.__radd__(a) — əks metod sınanır. Aşağıda 2 * v yazılışına məhz buna görə __rmul__ lazımdır: int vektorlar haqqında heç nə bilmir. __bool__ if v: yazılışının mənasını, __abs__ isə daxili abs() funksiyasının davranışını müəyyən edir:

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)))
▸ Gözlənilən nəticə
Vector(4, 5) Vector(6, 8) Vector(6, 8)
5.0 False
YazılışÇağırılan metod
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 üçün Python əvvəlcə __iadd__-i (obyekti yerindəcə dəyişə bilən variantı) axtarır, tapmasa, __add__-ə keçir. Buna görə += siyahını yerində dəyişir, kortej və sətir üçün isə yeni obyekt yaradır. __call__ metodu obyekti funksiya kimi çağırmağa imkan verir: obj().

Sinfin kolleksiya kimi də davrana bilər. __len__ len()-ə, __getitem__ indekslərə və kəsiklərə (kəsik obyekti birbaşa siyahıya ötürülür), __contains__ isə in operatoruna xidmət edir. Hətta for da işləyir: sinifdə __iter__ yoxdursa, Python IndexError görünənə qədər __getitem__-i 0, 1, 2… ilə ç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)
▸ Gözlənilən nəticə
3 Morning Finale ['Rain', 'Finale']
True
> Morning
> Rain
> Finale

@property, @classmethod və @staticmethod

Xassə (property) çöldən adi atribut kimi görünür, amma onu oxuyanda və ya ona qiymət verəndə metod işləyir. Bu, sinifdən istifadə edən kodu dəyişmədən sonradan yoxlama əlavə etməyə imkan verir — t.celsius = -300 yenə də adi mənimsətmədir. Diqqət et ki, __init__ də setter-dən keçir, ona görə yeni obyekt belə səhv vəziyyətdə yarana bilməz. fahrenheit kimi setter-i olmayan xassə yalnız oxunur və hər dəfə yenidən hesablanı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)
▸ Gözlənilən nəticə
25 77.0
error: below absolute zero

Sinif metodu (@classmethod) nüsxə yox, sinfin özünü cls kimi alır. Onun əsas işi from_csv kimi alternativ konstruktorlardır: metod cls(...) çağırdığı üçün törəmə sinif avtomatik olaraq öz tipində obyekt alır. Statik metod (@staticmethod) nə self, nə də cls alır — məntiqcə sinfə aid olduğu üçün onun ad sahəsində yaşayan adi funksiyadır. Bütün nüsxələrin paylaşdığı count sinif atributuna da diqqət 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))
▸ Gözlənilən nəticə
Student('Aysel', 9) Graduate('Murad', 11)
2 False

Varislik, MRO və super()

Çoxlu varislikdə Python-a metodları axtarmaq üçün qayda lazımdır. O, MRO-nu (method resolution order — metodların axtarış sırası) C3 alqoritmi ilə hesablayır: törəmə sinif valideynlərindən əvvəl gəlir, valideynlər yazıldıqları sıranı saxlayır, hər sinif isə yalnız bir dəfə görünür. Onu __mro__-da görmək olar. Əsas məqam: **super() «mənim valideynim» yox, «real obyektin MRO-sunda növbəti sinif» deməkdir**:

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()
▸ Gözlənilən nəticə
['Order', 'Logged', 'Validated', 'Base', 'object']
Logged: before
Validated: checking
Base.save

Logged birbaşa Base-dən törəyib, amma onun super().save() çağırışı Validated.save()-i işə saldı, çünki Order-in MRO-sunda növbəti sinif odur. Bunun sayəsində Base.save() düz bir dəfə icra olundu. Miksin (mixin) adlanan belə «əməkdaşlıq edən» siniflər yalnız hər metod super() çağıranda və heç biri valideyni Base.save(self) kimi birbaşa çağırmayanda işləyir.

Abstrakt baza sinfi törəmə siniflərin yerinə yetirməli olduğu müqaviləni təyin edir. abc.ABC-dən törəyən və @abstractmethod ilə işarələnmiş metodları olan sinfin nüsxəsini, törəmə sinif onların hamısını reallaşdırmayınca, yaratmaq olmur — xəta işin ortasında yox, obyekt yaradılanda üzə çıxır:

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)
▸ Gözlənilən nəticə
Rect with area 13.50
Can't instantiate abstract class Shape without an implementation for abstract method 'area'
Tapşırıq

Money sinfinə __add__, __radd__ və __eq__ əlavə et ki, iki məbləği toplamaq, siyahını sum() ilə cəmləmək və nəticəni müqayisə etmək mümkün olsun. Yad tiplər üçün __add__ NotImplemented qaytarmalıdır.

Tapşırıq · 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))
▸ Gözlənilən nəticə
Money(3)
Money(17)
True
Tapşırıq

Stack sinfinə __len__ və __repr__ əlavə et. bool(s) üçün ayrıca metod yazma — boş stek özü False sayılmalıdır.

Tapşırıq · 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)
▸ Gözlənilən nəticə
False
Stack(['a', 'b']) 2 True
b Stack(['a'])

Əsas fikirlər

  • Operatorlar və daxili funksiyalar xüsusi metodları çağırır: a + b → __add__, len(x) → __len__, x in c → __contains__.
  • __repr__ proqramçılar, __str__ istifadəçilər üçündür; konteynerlər __repr__-i göstərir.
  • __eq__ yad tiplər üçün NotImplemented qaytarmalıdır; __eq__ təyin etmək, əlavə etməsən, __hash__-ı silir.
  • @property atribut yazılışının arxasında yoxlama gizlədir; @classmethod cls alır (alternativ konstruktorlar), @staticmethod heç birini almır.
  • super() real obyektin MRO-su ilə hərəkət edir; ABC-nin abstrakt metodları nüsxə yaradılmazdan əvvəl reallaşdırılmalıdır.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
print([obj]) obyekti siyahının içində göstərəndə hansı metoddan istifadə edir?