Magiczne metody pozwalają nam rozszerzyć funkcjonalność klas. Zazwyczaj nie są one wywoływane bezpośrednio przez programistę, tylko przy wykonywaniu operacji na klasie. Załóżmy że chcielibyśmy dodać lub porównać do siebie dwie instancje zdefiniowanej przez nas klasy. Normalnie dostalibyśmy błąd, ale wystarczy, że zaimplementujemy odpowiednie magiczne metody i będziemy mogli te operacje wykonać.
Wbudowane w pythonie klasy implementują wiele magicznych metod. Dla przykładu spójrzmy na klasę int
.
>>> dir(int)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__',
'__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__',
'__float__', '__floor__', '__floordiv__', '__format__', '__ge__',
'__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__',
'__init__', '__init_subclass__', '__int__', '__invert__', '__le__',
'__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__',
'__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__',
'__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__',
'__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__',
'__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__',
'__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__',
'__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__',
'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag',
'numerator', 'real', 'to_bytes']
Definiując zmienne x
oraz y
oraz wykonując operację dodawania x + y
, tak naprawdę wykonywane jest x.__add__(y)
. Innym przykładem jest metoda __abs__
zwracająca wartość bezwzględną liczby. Wywołanie abs(x)
tak naprawdę wykonuje x.__abs__()
.
Trochę praktyki
Spróbujmy napisać klasę przechowującą ułamek oraz zaimplementować metody pozwalające na wykonywanie podstawowych operacji matematycznych. Zacznijmy od utworzenia klasy i zaimplementowania metody __init__.
class Fraction:
def __init__(self, numerator, denominator):
gcd = Fraction.gcd(numerator, denominator)
self.numerator = numerator // gcd
self.denominator = denominator // gcd
@staticmethod
def gcd(a, b):
if b > 0:
return Fraction.gcd(b, a % b)
else:
return a
Powyższa klasa przyjmuje jako argumenty licznik oraz mianownik, szuka największego wspólnego dzielnika, a następnie skraca ułamek (jeżeli oczywiście jest to możliwe).
Aktualnie gdybyśmy chcieli dodać do siebie dwa obiekty Fraction w taki sposób:
f1 = Fraction(2, 3)
f2 = Fraction(3, 5)
fsum = f1 + f2
print(fsum.numerator, fsum.denominator)
To dostaniemy błąd, mówiący o tym, że operacja dodawania jest niewspierana.
Traceback (most recent call last):
File "/home/ksanek/.PyCharmCE2019.3/config/scratches/scratch_8.py", line 9, in <module>
fsum = f1 + f2
TypeError: unsupported operand type(s) for +: 'Fraction' and 'Fraction'
Zaimplementujmy metodę __add__
i spróbujmy dodać do siebie obiekty ponownie.
def __add__(self, other):
lcm = self.lcm(self.denominator, other.denominator)
new_numerator = self.numerator * (lcm // self.denominator) + other.numerator * (lcm // other.denominator)
gcd = Fraction.gcd(new_numerator, lcm)
return Fraction(new_numerator // gcd, lcm // gcd)
@staticmethod
def lcm(a, b):
return a * b // Fraction.gcd(a, b)
Do naszej klasy dodaliśmy metodę lcm, zwracającą najmniejszą wspólną wielokrotność przesłanych liczb. Podczas dodawania ułamków używamy tej metody, a następnie na podstawie wyniku liczymy nowy licznik dla ułamka. Nowym mianownikiem będzie oczywiście najmniejsza wspólna wielokrotność mianowników tych ułamków. Może się zdarzyć, że nowy ułamek będzie się dało skrócić, więc dzielimy zarówno licznik jak i mianownik przez największy wspólny dzielnik, a na koniec zwracamy nowy obiekt ułamka.
Teraz wykonując powyższą operację dodawania powinniśmy ujrzeć wartości 19 oraz 15, reprezentujące ułamek 19/15.
A co jeżeli chcielibyśmy za pomocą funkcji print
wyświetlić ładną reprezentację obiektu zamiast czegoś takiego <main.Fraction object at 0x7f319f664c88>
? Musimy zaimplementować metodę __repr__
, która powinna zwrócić ciąg znaków:
def __repr__(self):
return f"Fraction({self.numerator}, {self.denominator})"
Teraz wykonanie print(fsum)
powinno nam wyświelić Fraction(19, 15)
.
Dodawanie in-place
Oprócz zwykłego dodawania z użyciem metody __add__
, możemy również zaimplementować dodawanie „w miejscu”, czyli bez tworzenia nowego obiektu ułamka. Następujący kod: f1 += f2
wywoła metodę f1.__iadd__(f2)
. Metoda ta mogłaby wyglądać tak:
def __iadd__(self, other):
lcm = self.lcm(self.denominator, other.denominator)
self.numerator = self.numerator * (lcm // self.denominator) + other.numerator * (lcm // other.denominator)
self.denominator = lcm
gcd = Fraction.gcd(self.numerator, self.denominator)
self.numerator = self.numerator // gcd
self.denominator = self.denominator // gcd
return self
Oblicza ona licznik i mianownik po dodaniu innego ułamka oraz przypisuje te wartości do pól numerator oraz denominator.
Porównywanie obiektów
Implementując metody __lt__
, __gt__
, __le__
, __ge__
, __eq__
, __ne__
jesteśmy w stanie dodać możliwość używania operatorów porównania na naszym obiekcie. Przykładowa implementacja mogłaby wyglądać następująco:
def __lt__(self, other):
return self.numerator / self.denominator < other.numerator / other.denominator
def __le__(self, other):
return self.numerator / self.denominator <= other.numerator / other.denominator
def __gt__(self, other):
return self.numerator / self.denominator > other.numerator / other.denominator
def __ge__(self, other):
return self.numerator / self.denominator >= other.numerator / other.denominator
def __eq__(self, other):
return self.numerator / self.denominator == other.numerator / other.denominator
def __ne__(self, other):
return self.numerator / self.denominator != other.numerator / other.denominator
Po tym powinniśmy bez problemu móc używać operatorów <
, <=
, >
, >=
, ==
oraz !=
.
Rzutowanie na liczbę
Jeżeli chcielibyśmy nasz ułamek zamienić na liczbę wykorzystując funkcje int
oraz float
, musimy zaimplementować metody __int__
oraz __float__
. Metoda __float__
powinna zwracać wynik dzielenia pola numerator przez pole denominator, a metoda __int__
powinna dodatkowo rzutować ten wynik na liczbę całkowitą.
def __int__(self):
return int(self.numerator / self.denominator)
def __float__(self):
return self.numerator / self.denominator
__add__ vs __radd__
Czym różnią się powyższe metody? Przypuśćmy że zaktualizowaliśmy kod funkcji __add__
tak, aby możliwe było dodanie do ułamka liczby całkowitej:
def __add__(self, other):
if isinstance(other, Fraction):
return self._add_fraction(other)
elif isinstance(other, int):
return self._add_int(other)
raise TypeError("Operation not supported")
def _add_fraction(self, fraction):
lcm = self.lcm(self.denominator, fraction.denominator)
new_numerator = self.numerator * (lcm // self.denominator) + fraction.numerator * (lcm // fraction.denominator)
gcd = Fraction.gcd(new_numerator, lcm)
return Fraction(new_numerator // gcd, lcm // gcd)
def _add_int(self, value):
return Fraction(self.numerator + value * self.denominator, self.denominator)
Powinniśmy mieć możliwość dodania liczby całkowitej do ułamka, tak jak przedstawiono poniżej:
>>> f1 = Fraction(2, 5)
>>> print(f1 + 2)
Fraction(12, 5)
Spróbujmy jednak zamienić kolejnością dodawanie i wyświetlić wynik 2 + f1
. Dostaniemy następujący błąd: TypeError: unsupported operand type(s) for +: 'int' and 'Fraction'
. Dzieje się tak, ponieważ zaimplementowaliśmy lewostronny operator dodawania, a brakuje nam implementacji prawostronnego operatora dodawania.
Python najpierw sprawdza czy typ int
implementuje dodawanie obiektu typu Fraction
, a następnie sprawdza czy typ Fraction
implementuje metodę __radd__. Jeżeli mu się nie uda znaleźć odpowiedniej metody, to rzuca błędem TypeError. Jako że dodawanie jest działaniem przemiennym, to funkcję __radd__ możemy dodać w prosty sposób, tak aby wykonywała funkcję __add__.
def __radd__(self, other):
return self.__add__(other)
Podsumowanie
Omówiliśmy sobie kilka podstawowych magicznych metod oraz zaimplementowaliśmy klasę ułamka, bazującą na tych metodach. Klasę dalej można rozszerzać o kolejne magiczne metody jak __abs__
(wartość bezwzględna), __sub__
, __mul__
, __truediv__
, __pow__
(operacje matematyczne).
Python zawiera również bardziej zaawansowane magiczne metody jak np. __new__, której zadaniem jest zwrócenie nowej instancji klasy. Metodę __new__ omówimy przy okazji omawiania metaprogramowania w języku Python.
Jeżeli zaciekawił Cię ten temat, to polecam zapoznać się z dokumentacją, a jeżeli nie czytałeś poprzedniego wpisu omawiającego dekoratory, to zachęcam go do przeczytania tutaj.