1/36
Metody magiczne - Magia Pythona

Wykład 5: Metody specjalne (Dunder Methods)

Dlaczego niektóre obiekty możemy dodawać za pomocą znaku +, a inne nie? Jak sprawić, by nasza klasa zachowywała się jak lista lub liczba? Odpowiedzią są metody magiczne.

  • Czym są metody magiczne? - Integracja z silnikiem języka.
  • Reprezentacja: Jak świat widzi nasz obiekt.
  • Arytmetyka: Przeciążanie operatorów matematycznych.
  • Porównania: Rankingowanie i sortowanie obiektów.
  • Protokoły: Tworzenie pojemników i iteracji.
  • Case Study: Klasa LiczbaRzymska.
2/36
Czym jest "Dunder"?

Double Underscore

Metody magiczne łatwo rozpoznać po charakterystycznej nazwie: zaczynają się i kończą dwoma podkreślnikami.

W slangu programistycznym nazywamy je Dunder Methods (skrót od Double Underscore).

  • Przykład: __init__, __str__, __add__.
  • Nie wywołujemy ich bezpośrednio (np. obj.__init__() to błąd w sztuce).
  • Wywołuje je silnik Pythona w odpowiedzi na konkretne zdarzenia (np. tworzenie obiektu, użycie operatora +).
Metody magiczne to sposób na przeciążanie operatorów (ang. operator overloading), co czyni nasz kod bardziej naturalnym ("pythonic").
3/36
Protokół a Interfejs

Zasady współpracy

W Pythonie zamiast sztywnych interfejsów (jak w Javie) używamy protokołów.

Jeśli klasa implementuje odpowiednie metody magiczne, Python uznaje, że "potrafi" ona pełnić daną rolę.

Akcja w kodzie Wymagana metoda
len(obj) __len__
print(obj) __str__
a + b __add__
obj[index] __getitem__
4/36
Reprezentacja obiektu: __str__

Dla użytkownika

Metoda __str__ powinna zwracać czytelny, tekstowy opis obiektu, przeznaczony dla końcowego użytkownika.

class Produkt:
    def __init__(self, nazwa, cena):
        self.nazwa = nazwa
        self.cena = cena

    def __str__(self):
        return f"{self.nazwa} za {self.cena} zł"

p = Produkt("Laptop", 3500)
print(p) # Laptop za 3500 zł
5/36
Reprezentacja obiektu: __repr__

Dla programisty

Metoda __repr__ (od representation) służy do debugowania. Powinna zwracać tekst, który wygląda jak instrukcja utworzenia tego obiektu.

class Produkt:
    def __repr__(self):
        return f"Produkt(nazwa='{self.nazwa}', cena={self.cena})"

p = Produkt("Myszka", 150)
print(repr(p)) # Produkt(nazwa='Myszka', cena=150)

Jeśli nie zdefiniujesz __str__, Python użyje __repr__ jako zamiennika.

6/36
Porównania: __eq__

Równość (Equality)

Domyślnie Python sprawdza, czy dwa obiekty to to samo miejsce w pamięci. Metoda __eq__ pozwala porównywać ich zawartość.

class Wektor:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __eq__(self, inny):
        if not isinstance(inny, Wektor):
            return False
        return self.x == inny.x and self.y == inny.y

w1 = Wektor(1, 2)
w2 = Wektor(1, 2)
print(w1 == w2) # True
7/36
Porównania: Ranking (LT, GT)

Większy czy mniejszy?

Aby móc sortować obiekty, musimy zdefiniować metody "bogatego porównania" (rich comparisons).

  • __lt__ (less than): <
  • __gt__ (greater than): >
  • __le__ (less or equal): <=
  • __ge__ (greater or equal): >=
Jeśli zdefiniujesz tylko __lt__, Python często potrafi wydedukować wynik dla > przez zamianę stron (jeśli a < b to False, to może b < a?).
8/36
Przykład: Sortowanie ocen

Porządkowanie obiektów

class Student:
    def __init__(self, imie, srednia):
        self.imie = imie
        self.srednia = srednia

    def __lt__(self, inny):
        return self.srednia < inny.srednia

s1 = Student("Anna", 4.5)
s2 = Student("Jan", 3.8)
print(s2 < s1) # True

Dzięki __lt__ możemy teraz użyć standardowej funkcji sorted([s1, s2]) bezpośrednio na liście studentów.

9/36
Arytmetyka: Dodawanie

Operator +

Metoda __add__ definiuje, co się stanie, gdy użyjemy znaku plus między dwoma obiektami.

class Portfel:
    def __init__(self, kwota):
        self.kwota = kwota

    def __add__(self, inny):
        return Portfel(self.kwota + inny.kwota)

p1 = Portfel(50)
p2 = Portfel(30)
p3 = p1 + p2
print(p3.kwota) # 80
10/36
Arytmetyka: Inne operatory

Pełna matematyka

Każdy operator ma swoją metodę magiczną:

  • Odejmowanie - : __sub__
  • Mnożenie * : __mul__
  • Dzielenie / : __truediv__
  • Dzielenie całkowite // : __floordiv__
  • Potęgowanie ** : __pow__

Zasada jest zawsze ta sama: a + b to w rzeczywistości wywołanie a.__add__(b).

11/36
Rozszerzone przypisanie (In-place)

Operatory +=, -= itp.

Domyślnie a += b to a = a + b (tworzony jest nowy obiekt). Jeśli chcemy zmodyfikować istniejący obiekt, używamy metod z literką "i" (od in-place).

class Licznik:
    def __init__(self, start=0):
        self.wartość = start

    def __iadd__(self, n):
        self.wartość += n
        return self # Ważne! iadd musi zwracać self

Użycie __iadd__ jest wydajniejsze przy dużych strukturach danych (np. listach).

12/36
Operatory odbite (Reflected)

Gdy lewa strona "nie wie"

Co jeśli wykonamy 10 + portfel? Standardowa liczba int nie wie, jak dodać do siebie nasz obiekt Portfel.

Wtedy Python sprawdza metodę __radd__ (reflected add) w obiekcie po prawej stronie.

class Portfel:
    def __radd__(self, kwota_liczba):
        # Obsługa sytuacji: 10 + Portfel
        return Portfel(self.kwota + kwota_liczba)

Dzięki temu operacje mogą być przemienne.

13/36
Protokół pojemnika (Container)

Obiekt jak lista

Możemy sprawić, by nasz obiekt zachowywał się jak kolekcja danych, implementując metody __len__ i __getitem__.

class Druzyna:
    def __init__(self):
        self._gracze = ["Adam", "Ewa", "Igor"]

    def __len__(self):
        return len(self._gracze)

    def __getitem__(self, index):
        return self._gracze[index]

Teraz możemy użyć len(druzyna) oraz druzyna[0].

14/36
Operator przynależności: __contains__

Słowo kluczowe 'in'

Metoda __contains__ pozwala na użycie operatora in.

class Druzyna:
    def __contains__(self, osoba):
        return osoba in self._gracze

d = Druzyna()
print("Adam" in d) # True

Zwraca True lub False. Bardzo przydatne przy tworzeniu własnych struktur bazodanowych lub zbiorów.

15/36
Konwersja typów: __int__, __float__

Zmiana natury

Możemy zdefiniować, jak obiekt powinien zostać rzutowany na typy podstawowe.

class Waga:
    def __init__(self, kg):
        self.kg = kg

    def __int__(self):
        return int(self.kg)

w = Waga(75.8)
print(int(w)) # 75
16/36
Prawda i Fałsz: __bool__

Truth value testing

Kiedy obiekt jest uważany za "prawdziwy" (True) w instrukcji if? Domyślnie każdy obiekt jest True, chyba że zdefiniujemy __bool__.

class Bateria:
    def __init__(self, poziom):
        self.poziom = poziom

    def __bool__(self):
        return self.poziom > 0

b = Bateria(0)
if not b:
    print("Urządzenie wyłączone!")
17/36
Wywoływanie obiektów: __call__

Obiekt jako funkcja

Metoda __call__ sprawia, że instancja klasy zachowuje się jak zwykła funkcja (można ją wywołać nawiasami).

class Mnoznik:
    def __init__(self, czynnik):
        self.czynnik = czynnik

    def __call__(self, x):
        return x * self.czynnik

podwoj = Mnoznik(2)
print(podwoj(5)) # 10

Przydatne przy tworzeniu dekoratorów klasowych lub obiektów przechowujących stan między wywołaniami.

18/36
Atrybuty: __getattr__ i __setattr__

Dynamiczne pola

Python pozwala przechwycić próbę dostępu do pola, które nie istnieje.

class Dynamiczny:
    def __getattr__(self, nazwa):
        return f"Brak pola {nazwa}, ale zwracam to!"

obj = Dynamiczny()
print(obj.jakies_pole)
Ostrzeżenie: Metody te są bardzo potężne, ale mogą utrudnić debugowanie i pogorszyć wydajność. Używaj ich z rozwagą!
19/36
Case Study: Klasa LiczbaRzymska

Zastosowanie praktyczne

Zbudujemy klasę, która przechowuje wartość liczbową, ale wyświetla ją i operuje na niej w formacie rzymskim (I, V, X, L...).

Nasze cele:

  1. Czytelna reprezentacja rzymska (__str__).
  2. Możliwość dodawania liczb rzymskich (__add__).
  3. Łatwe porównywanie wartości (__eq__, __lt__).
  4. Konwersja na standardowe int.
20/36
LiczbaRzymska: Inicjalizacja

Krok 1: Przechowywanie danych

Wewnątrz klasy będziemy trzymać zwykłą liczbę całkowitą. To ułatwi nam obliczenia.

class LiczbaRzymska:
    def __init__(self, wartosc):
        if not isinstance(wartosc, int) or wartosc <= 0:
            raise ValueError("Tylko liczby dodatnie!")
        self._val = wartosc
21/36
LiczbaRzymska: Algorytm konwersji

Krok 2: Zamiana na tekst

Pomocnicza metoda do zamiany 123 na "CXXIII".

    def _to_roman(self):
        mapowanie = [
            (1000, 'M'), (900, 'CM'), (500, 'D'), 
            (400, 'CD'), (100, 'C'), (90, 'XC'),
            (50, 'L'), (40, 'XL'), (10, 'X'), 
            (9, 'IX'), (5, 'V'), (4, 'IV'), (1, 'I')
        ]
        wynik = ""
        temp = self._val
        for n, s in mapowanie:
            wynik += (temp // n) * s
            temp %= n
        return wynik
22/36
LiczbaRzymska: Słowo do świata

Krok 3: Implementacja __str__ i __repr__

    def __str__(self):
        return self._to_roman()

    def __repr__(self):
        return f"LiczbaRzymska({self._val})"

liczba = LiczbaRzymska(44)
print(liczba) # XLIV
23/36
LiczbaRzymska: Dodawanie

Krok 4: Implementacja __add__

Zwracamy nowy obiekt LiczbaRzymska.

    def __add__(self, inny):
        if not isinstance(inny, LiczbaRzymska):
            return NotImplemented
        return LiczbaRzymska(self._val + inny._val)

Użycie NotImplemented pozwala Pythonowi spróbować metody __radd__ u drugiego obiektu.

24/36
LiczbaRzymska: Porównania

Krok 5: Równość i Kolejność

    def __eq__(self, inny):
        if not isinstance(inny, LiczbaRzymska):
            return False
        return self._val == inny._val

    def __lt__(self, inny):
        if not isinstance(inny, LiczbaRzymska):
            raise TypeError("Nieporównywalne typy")
        return self._val < inny._val
25/36
LiczbaRzymska: Wykorzystanie

Magia w akcji

x = LiczbaRzymska(10) # X
y = LiczbaRzymska(5)  # V

wynik = x + y
print(f"{x} + {y} = {wynik}") # X + V = XV
print(x > y) # True
print(int(wynik)) # 15

Nasz obiekt wygląda i zachowuje się jak wbudowany typ Pythona.

26/36
Iteratory: __iter__ i __next__

Wstęp do pętli for

Aby obiekt mógł być użyty w pętli for, musi implementować protokół iteratora. (Więcej na ten temat w osobnym wykładzie, ale warto znać fundament).

  • __iter__: Zwraca obiekt iteratora (najczęściej sam self).
  • __next__: Zwraca kolejny element lub zgłasza błąd StopIteration.
27/36
Zarządzanie zasobami: __enter__ i __exit__

Słowo kluczowe 'with'

Pozwalają na stworzenie tzw. menedżera kontekstu (Context Manager). Gwarantują posprzątanie zasobów (np. zamknięcie pliku lub bazy danych).

class Plik:
    def __enter__(self):
        print("Otwieram zasób...")
        return self

    def __exit__(self, typ, wart, slady):
        print("Zamykam zasób!")

with Plik() as p:
    print("Pracuję...")
28/36
Usuwanie obiektu: __del__

Destruktor

Metoda wywoływana, gdy licznik referencji do obiektu spadnie do zera i Garbage Collector decyduje się go usunąć z pamięci.

Uwaga: W Pythonie rzadko polegamy na __del__, ponieważ nie mamy pewności, kiedy dokładnie zostanie wywołany. Lepiej używać with i __exit__.
29/36
Haszowanie: __hash__

Obiekt jako klucz słownika

Jeśli chcemy używać naszych obiektów jako kluczy w słownikach (dict) lub elementach zbiorów (set), muszą być one "haszowalne".

  • Wymaga implementacji __hash__.
  • Wymaga implementacji __eq__.
  • Obiekt powinien być niezmienny (immutable).
30/36
Częste błędy: Rekurencja

Nieskończona pętla

Bardzo łatwo wywołać metodę magiczną wewnątrz niej samej, co prowadzi do błędu RecursionError.

class Błąd:
    def __str__(self):
        # BŁĄD! str(self) wywoła __str__, który znów wywoła str(self)...
        return object.__repr__(self) 

Zawsze odwołuj się do atrybutów lub innych metod, unikając bezpośredniego rzutowania na ten sam typ wewnątrz metody konwersji.

31/36
Dlaczego nie używać własnych nazw?

Standard vs Twórczość

Mógłbyś nazwać metodę dodawania dolicz(), ale wtedy nie zadziała operator +.

  • Metody magiczne to standard komunikacji z Pythonem.
  • Dzięki nim Twoje klasy są przewidywalne dla innych programistów.
  • Umożliwiają korzystanie z tysięcy gotowych bibliotek, które "spodziewają się" standardowych zachowań.
32/36
Kopiowanie: __copy__

Klonowanie obiektów

Gdy używamy modułu copy, możemy zdefiniować własną logikę kopiowania obiektu (płytką lub głęboką).

import copy

class Dokument: def __deepcopy__(self, memo): # Własna logika głębokiego kopiowania return Dokument(self.tresc[:])
33/36
Słowniczek terminów

Najważniejsze pojęcia

Termin Opis
Dunder Double Underscore - nazwa metod specjalnych.
Operator Overloading Nadanie operatorom (+, -, *) nowego znaczenia dla własnych klas.
Protocol Zbiór metod magicznych definiujących zachowanie (np. Container Protocol).
Mutable / Immutable Zmienność lub niezmienność obiektu w czasie.
34/36
Podsumowanie: Kiedy stosować?

Złoty środek

Metody magiczne są potężne, ale nie nadużywaj ich.

  • Stosuj je tam, gdzie naturalnie pasują (np. Wektor1 + Wektor2 ma sens, ale Użytkownik1 + Użytkownik2 już niekoniecznie).
  • Zawsze implementuj __str__ lub __repr__ – ułatwia to pracę Tobie i Twojemu zespołowi.
  • Pamiętaj o obsłudze typów (używaj isinstance w metodach porównania).
35/36
Zadanie dla studentów

Pora na praktykę

Stwórz klasę Waluta, która:

  1. Przechowuje kod waluty (np. 'PLN') i wartość.
  2. Pozwala na dodawanie obiektów tej samej waluty.
  3. Przy próbie dodania różnych walut zgłasza błąd ValueError.
  4. Ładnie wypisuje się na ekranie (np. "100.00 PLN").
  5. Można ją pomnożyć przez liczbę (np. waluta * 2).
36/36
Koniec Wykładu 5

Dziękuję za uwagę!

W następnej części: Dataclass i automatyzacja tworzenia klas.

Pytania?