Kurs Python · Moduł 3: Programowanie obiektowe

Metody magiczne - ukryte zachowania zwierząt

13 min czytania
W tej lekcji11

Witaj ponownie! Tu Darwin, dziś fascynująca lekcja o ukrytych mocach Pythona.

W naturze zwierzęta mają ukryte zachowania - lew ryczy, słoń trąbi, kameleon się maskuje. W Pythonie klasy też mogą mieć ukryte zachowania przez metody magiczne (magic methods) - specjalne metody z podwójnym podkreśleniem!

1lion = Animal("Lew")
2print(lion)  # Jak Python wie, co wyświetlić? __str__!
3len(lion)    # Jak Python wie, co zmierzyć? __len__!
4lion + elephant  # Jak Python wie, co dodać? __add__!

Czym są metody magiczne?

Metody magiczne (zwane też dunder methods od "double underscore") to specjalne metody z nazwami w formacie __method__, które Python automatycznie wywołuje w określonych sytuacjach.

Analogia Safari: To jak naturalne instynkty zwierząt - nie musisz ich uczyć, są wbudowane w ich naturę!

1class Animal:
2    def __init__(self, name):
3        """Wywoływane przy TWORZENIU obiektu"""
4        self.name = name
5
6    def __str__(self):
7        """Wywoływane przez print() i str()"""
8        return f"Animal: {self.name}"
9
10    def __len__(self):
11        """Wywoływane przez len()"""
12        return len(self.name)
13
14lion = Animal("Lew")  # Wywołuje __init__
15print(lion)           # Wywołuje __str__
16print(len(lion))      # Wywołuje __len__

Dlaczego "magiczne"?

  • Python wywołuje je automatycznie
  • Sprawiają, że Twoje klasy zachowują się jak wbudowane typy
  • Pozwalają używać standardowej składni Pythona

Podstawowe metody magiczne

1. __init__ - Konstruktor

Już znasz! Wywoływana przy tworzeniu obiektu.

1class Species:
2    def __init__(self, name, population):
3        """Inicjalizacja - już to znasz!"""
4        self.name = name
5        self.population = population
6
7lion = Species("Lew", 500)  # Wywołuje __init__

2. __str__ - Reprezentacja czytelna dla ludzi

Wywoływana przez print() i str() - zwraca string przyjazny użytkownikowi. Gdy piszesz print(lion), print() najpierw zamienia argument na tekst przez str(lion), a str() wywołuje metodę lion.__str__().

1class Animal:
2    def __init__(self, name, age):
3        self.name = name
4        self.age = age
5
6    def __str__(self):
7        """Dla ludzi - czytelne"""
8        return f"{self.name} (wiek: {self.age} lat)"
9
10lion = Animal("Simba", 5)
11print(lion)  # "Simba (wiek: 5 lat)" - wywołuje __str__
12print(str(lion))  # To samo

3. __repr__ - Reprezentacja dla programistów

Wywoływana przez repr() i w konsoli - zwraca jednoznaczną reprezentację obiektu.

1class Animal:
2    def __init__(self, name, age):
3        self.name = name
4        self.age = age
5
6    def __repr__(self):
7        """Dla programistów - precyzyjne"""
8        return f"Animal(name='{self.name}', age={self.age})"
9
10    def __str__(self):
11        """Dla użytkowników - czytelne"""
12        return f"{self.name} ({self.age} lat)"
13
14lion = Animal("Simba", 5)
15
16# W konsoli Python
17>>> lion
18Animal(name='Simba', age=5)  # Wywołuje __repr__
19
20# Przez print()
21print(lion)  # "Simba (5 lat)" - wywołuje __str__
22
23# repr() wywołane jawnie
24print(repr(lion))  # "Animal(name='Simba', age=5)"

Zasada: __repr__ powinien zwracać kod, którym można odtworzyć obiekt!

1class Point:
2    def __init__(self, x, y):
3        self.x = x
4        self.y = y
5
6    def __repr__(self):
7        return f"Point({self.x}, {self.y})"
8
9p = Point(3, 4)
10print(repr(p))  # "Point(3, 4)" - możesz skopiować i wkleić!

Metody porównania

Pozwalają porównywać obiekty z ==, <, >, etc.

1class Animal:
2    def __init__(self, name, weight):
3        self.name = name
4        self.weight = weight  # kg
5
6    def __eq__(self, other):
7        """Równość: =="""
8        if not isinstance(other, Animal):
9            return False
10        return self.weight == other.weight
11
12    def __lt__(self, other):
13        """Mniejsze niż: <"""
14        if not isinstance(other, Animal):
15            return NotImplemented
16        return self.weight < other.weight
17
18    def __le__(self, other):
19        """Mniejsze lub równe: <="""
20        return self.weight <= other.weight
21
22    def __gt__(self, other):
23        """Większe niż: >"""
24        return self.weight > other.weight
25
26    def __ge__(self, other):
27        """Większe lub równe: >="""
28        return self.weight >= other.weight
29
30    def __ne__(self, other):
31        """Nierówne: !="""
32        return not self.__eq__(other)
33
34lion = Animal("Lew", 190)
35elephant = Animal("Słoń", 5000)
36rhino = Animal("Nosorożec", 2300)
37
38print(lion < elephant)  # True - wywołuje __lt__
39print(elephant > rhino)  # True - wywołuje __gt__
40print(lion == Animal("Tygrys", 190))  # True - taka sama waga

Tip: Możesz użyć dekoratora @functools.total_ordering - zdefiniuj tylko __eq__ i jeden z __lt__, __le__, __gt__, __ge__, reszta zostanie automatycznie wygenerowana!

1from functools import total_ordering
2
3@total_ordering
4class Animal:
5    def __init__(self, name, weight):
6        self.name = name
7        self.weight = weight
8
9    def __eq__(self, other):
10        """Tylko to i __lt__ - reszta automatycznie!"""
11        if not isinstance(other, Animal):
12            return False
13        return self.weight == other.weight
14
15    def __lt__(self, other):
16        """Tylko to i __eq__ - reszta automatycznie!"""
17        if not isinstance(other, Animal):
18            return NotImplemented
19        return self.weight < other.weight
20
21    # __le__, __gt__, __ge__, __ne__ - automatycznie wygenerowane!

Metody arytmetyczne

Pozwalają używać operatorów matematycznych z obiektami!

1class Population:
2    """Reprezentuje populację gatunku"""
3
4    def __init__(self, species, count):
5        self.species = species
6        self.count = count
7
8    def __add__(self, other):
9        """Dodawanie: +"""
10        if isinstance(other, Population):
11            if self.species != other.species:
12                raise ValueError("Różne gatunki!")
13            return Population(self.species, self.count + other.count)
14        elif isinstance(other, int):
15            return Population(self.species, self.count + other)
16        return NotImplemented
17
18    def __sub__(self, other):
19        """Odejmowanie: -"""
20        if isinstance(other, int):
21            return Population(self.species, max(0, self.count - other))
22        return NotImplemented
23
24    def __mul__(self, other):
25        """Mnożenie: *"""
26        if isinstance(other, (int, float)):
27            return Population(self.species, int(self.count * other))
28        return NotImplemented
29
30    def __str__(self):
31        return f"{self.species}: {self.count} osobników"
32
33# Użycie operatorów!
34lions_north = Population("Lew", 120)
35lions_south = Population("Lew", 85)
36
37# Dodawanie populacji
38total_lions = lions_north + lions_south
39print(total_lions)  # "Lew: 205 osobników"
40
41# Dodawanie liczby
42more_lions = lions_north + 30
43print(more_lions)  # "Lew: 150 osobników"
44
45# Odejmowanie (zmniejszenie populacji)
46fewer_lions = lions_north - 20
47print(fewer_lions)  # "Lew: 100 osobników"
48
49# Mnożenie (szacowanie wzrostu)
50projected = lions_north * 1.5  # Wzrost o 50%
51print(projected)  # "Lew: 180 osobników"

Metody kontenerowe

Sprawiają, że Twoje obiekty zachowują się jak listy, słowniki!

__len__ - Długość

Gdy klasa ma metodę __len__, funkcja len() działa na jej obiektach tak samo jak na liście. Stado zwraca liczbę swoich członków:

1class Pack:
2    """Stado zwierząt"""
3
4    def __init__(self, species):
5        self.species = species
6        self.members = []
7
8    def add(self, name):
9        self.members.append(name)
10
11    def __len__(self):
12        """Wywołane przez len()"""
13        return len(self.members)
14
15pack = Pack("Lwy")
16pack.add("Simba")
17pack.add("Nala")
18pack.add("Mufasa")
19
20print(len(pack))  # 3 - wywołuje __len__

__getitem__ i __setitem__ - Dostęp przez indeks

Te dwie metody pozwalają czytać i zmieniać elementy obiektu przez nawiasy kwadratowe, jak w liście. Siedlisko dostaje przy okazji metodę __contains__, która obsługuje operator in:

1class Habitat:
2    """Siedlisko ze zwierzętami"""
3
4    def __init__(self, name):
5        self.name = name
6        self.animals = []
7
8    def __getitem__(self, index):
9        """Dostęp do odczytu: habitat[0]"""
10        return self.animals[index]
11
12    def __setitem__(self, index, value):
13        """Dostęp do zapisu: habitat[0] = "Lew" """
14        self.animals[index] = value
15
16    def __len__(self):
17        return len(self.animals)
18
19    def __contains__(self, item):
20        """Operator 'in': "Lew" in habitat"""
21        return item in self.animals
22
23    def append(self, animal):
24        self.animals.append(animal)
25
26savanna = Habitat("Sawanna")
27savanna.append("Lew")
28savanna.append("Słoń")
29savanna.append("Żyrafa")
30
31# Dostęp jak do listy!
32print(savanna[0])  # "Lew" - wywołuje __getitem__
33savanna[1] = "Nosorożec"  # Wywołuje __setitem__
34
35# Operator in
36print("Lew" in savanna)  # True - wywołuje __contains__
37
38# len()
39print(len(savanna))  # 3 - wywołuje __len__
40
41# Iteracja (automatyczna przez __getitem__)
42for animal in savanna:
43    print(animal)

__iter__ i __next__ - Iteracja

Pętla for najpierw wywołuje __iter__, a potem __next__ w każdym kroku, aż dostanie wyjątek StopIteration. Tak zbudujemy kalendarz dni ekspedycji:

1class Expedition:
2    """Iterator po dniach ekspedycji"""
3
4    def __init__(self, days):
5        self.days = days
6        self.current_day = 0
7
8    def __iter__(self):
9        """Zwróć iterator (siebie)"""
10        self.current_day = 0
11        return self
12
13    def __next__(self):
14        """Zwróć następny element"""
15        if self.current_day >= self.days:
16            raise StopIteration  # Koniec iteracji
17
18        self.current_day += 1
19        return f"Dzień {self.current_day}"
20
21expedition = Expedition(5)
22
23# Iteracja przez for
24for day in expedition:
25    print(day)
26# Dzień 1
27# Dzień 2
28# Dzień 3
29# Dzień 4
30# Dzień 5
31
32# Lub manualnie
33exp2 = Expedition(3)
34print(next(exp2))  # "Dzień 1"
35print(next(exp2))  # "Dzień 2"
36print(next(exp2))  # "Dzień 3"

Czwarte wywołanie next(exp2) zgłosiłoby wyjątek StopIteration. To ten sam sygnał, po którym pętla for sama kończy pracę.

__call__ - Obiekty wywoływalne

Sprawia, że obiekt można wywołać jak funkcję!

1class AnimalSound:
2    """Callable - obiekt jak funkcja"""
3
4    def __init__(self, species, sound):
5        self.species = species
6        self.sound = sound
7
8    def __call__(self, times=1):
9        """Wywołanie obiektu jak funkcji"""
10        return " ".join([self.sound] * times)
11
12lion_roar = AnimalSound("Lew", "ROAR")
13elephant_trumpet = AnimalSound("Słoń", "TRUUU")
14
15# Wywołaj jak funkcję!
16print(lion_roar())  # "ROAR" - wywołuje __call__
17print(lion_roar(3))  # "ROAR ROAR ROAR"
18print(elephant_trumpet(2))  # "TRUUU TRUUU"

Context managers - __enter__ i __exit__

Pozwala używać with statement!

1class SafariCamera:
2    """Context manager do robienia zdjęć"""
3
4    def __init__(self, location):
5        self.location = location
6        self.photos = []
7
8    def __enter__(self):
9        """Wywoływane przy wejściu do bloku 'with'"""
10        print(f"Włączam kamerę w {self.location}")
11        return self  # Zwróć obiekt do użycia
12
13    def __exit__(self, exc_type, exc_val, exc_tb):
14        """Wywoływane przy wyjściu z bloku 'with'"""
15        print(f"Zapisane zdjęcia: {len(self.photos)}")
16        print(f"Wyłączam kamerę")
17        return False  # Nie tłum wyjątków
18
19    def take_photo(self, subject):
20        """Zrób zdjęcie"""
21        self.photos.append(subject)
22        print(f"  Zdjęcie: {subject}")
23
24# Użycie z 'with' - automatyczne __enter__ i __exit__!
25with SafariCamera("Serengeti") as camera:
26    camera.take_photo("Lew polujący")
27    camera.take_photo("Stado słoni")
28    camera.take_photo("Żyrafa przy drzewie")

Po uruchomieniu w konsoli zobaczysz:

1Włączam kamerę w Serengeti
2  Zdjęcie: Lew polujący
3  Zdjęcie: Stado słoni
4  Zdjęcie: Żyrafa przy drzewie
5Zapisane zdjęcia: 3
6Wyłączam kamerę

Nigdzie nie wywołaliśmy __enter__ ani __exit__ ręcznie. with uruchomił pierwszą metodę na początku bloku, a drugą automatycznie na jego końcu i zrobiłby to nawet wtedy, gdyby w środku wystąpił błąd.

Przykład z safari - kompletny system katalogowania

Na koniec klasa gatunku, która korzysta z prawie wszystkich metod magicznych z tej lekcji: jej obiekty mają czytelny opis, porównują się po populacji, obsługują + i -, zachowują się jak kontener i można je wywołać jak funkcję.

1from functools import total_ordering
2from datetime import datetime
3
4@total_ordering
5class Species:
6    """
7    Klasa gatunku z pełnym zestawem metod magicznych
8
9    Zachowuje się jak wbudowany typ Pythona!
10    """
11
12    all_species = []  # Rejestr wszystkich gatunków
13
14    def __init__(self, scientific_name, common_name, population, habitat):
15        """Konstruktor"""
16        self.scientific_name = scientific_name
17        self.common_name = common_name
18        self.population = population
19        self.habitat = habitat
20        self.observations = []
21
22        Species.all_species.append(self)
23
24    # === REPREZENTACJA ===
25
26    def __str__(self):
27        """Dla print() - przyjazne dla człowieka"""
28        return f"{self.common_name} ({self.population} osobników)"
29
30    def __repr__(self):
31        """Dla repr() - jednoznaczne"""
32        return (f"Species(scientific_name='{self.scientific_name}', "
33                f"common_name='{self.common_name}', "
34                f"population={self.population}, "
35                f"habitat='{self.habitat}')")
36
37    # === PORÓWNANIA (tylko __eq__ i __lt__, reszta przez @total_ordering) ===
38
39    def __eq__(self, other):
40        """Równość: =="""
41        if not isinstance(other, Species):
42            return False
43        return self.population == other.population
44
45    def __lt__(self, other):
46        """Mniejsze niż: < (porównanie po populacji)"""
47        if not isinstance(other, Species):
48            return NotImplemented
49        return self.population < other.population
50
51    def __hash__(self):
52        """Hash z tego samego pola co __eq__ - obiekty równe muszą mieć równy hash"""
53        return hash(self.population)
54
55    # === ARYTMETYKA ===
56
57    def __add__(self, other):
58        """Dodawanie: species + 50"""
59        if isinstance(other, int):
60            return Species(
61                self.scientific_name,
62                self.common_name,
63                self.population + other,
64                self.habitat
65            )
66        return NotImplemented
67
68    def __sub__(self, other):
69        """Odejmowanie: species - 20"""
70        if isinstance(other, int):
71            return Species(
72                self.scientific_name,
73                self.common_name,
74                max(0, self.population - other),
75                self.habitat
76            )
77        return NotImplemented
78
79    # === KONTENER ===
80
81    def __len__(self):
82        """len(species) - liczba obserwacji"""
83        return len(self.observations)
84
85    def __getitem__(self, index):
86        """species[0] - dostęp do obserwacji"""
87        return self.observations[index]
88
89    def __contains__(self, location):
90        """'Serengeti' in species - sprawdź lokalizację"""
91        return any(obs["location"] == location for obs in self.observations)
92
93    # === CALLABLE ===
94
95    def __call__(self, location, count):
96        """Wywołanie jak funkcja - dodaj obserwację"""
97        observation = {
98            "date": datetime.now().strftime("%Y-%m-%d"),
99            "location": location,
100            "count": count
101        }
102        self.observations.append(observation)
103        return f"Dodano: {count}x {self.common_name} w {location}"
104
105    # === BOOL ===
106
107    def __bool__(self):
108        """bool(species) - czy gatunek ma jakąś populację?"""
109        return self.population > 0
110
111# === DEMONSTRACJA WSZYSTKICH MAGICZNYCH METOD ===
112
113print("=== TWORZENIE GATUNKÓW ===\n")
114
115lion = Species("Panthera leo", "Lew", 120, "sawanna")
116elephant = Species("Loxodonta africana", "Słoń", 450, "sawanna")
117rhino = Species("Diceros bicornis", "Nosorożec", 45, "sawanna")
118extinct = Species("Dodo", "Dodo", 0, "Mauritius")
119
120# __str__ i __repr__
121print("__str__ (print):")
122print(lion)  # "Lew (120 osobników)"
123
124print("\n__repr__ (repr):")
125print(repr(lion))
126# Species(scientific_name='Panthera leo', common_name='Lew', population=120, habitat='sawanna')
127
128# Porównania (__eq__, __lt__, etc.)
129print("\n=== PORÓWNANIA ===")
130print(f"lion == elephant? {lion == elephant}")  # False
131print(f"rhino < lion? {rhino < lion}")  # True (45 < 120)
132print(f"elephant > lion? {elephant > lion}")  # True (450 > 120)
133
134# Sortowanie (działa przez __lt__)
135species_list = [lion, rhino, elephant]
136species_list.sort()
137print(f"\nPosortowane (rosnąco): {[str(s) for s in species_list]}")
138# ['Nosorożec (45 osobników)', 'Lew (120 osobników)', 'Słoń (450 osobników)']
139
140# Arytmetyka (__add__, __sub__)
141print("\n=== ARYTMETYKA ===")
142more_lions = lion + 30  # Zwiększ populację
143print(f"lion + 30 = {more_lions}")  # "Lew (150 osobników)"
144
145fewer_rhinos = rhino - 10
146print(f"rhino - 10 = {fewer_rhinos}")  # "Nosorożec (35 osobników)"
147
148# Callable (__call__)
149print("\n=== CALLABLE - dodawanie obserwacji ===")
150print(lion("Serengeti", 12))  # Wywołaj jak funkcję!
151print(lion("Masai Mara", 8))
152print(elephant("Amboseli", 35))
153
154# Kontener (__len__, __getitem__, __contains__)
155print("\n=== KONTENER ===")
156print(f"Liczba obserwacji lwów: {len(lion)}")  # __len__
157print(f"Pierwsza obserwacja: {lion[0]}")  # __getitem__
158print(f"'Serengeti' in lion? {'Serengeti' in lion}")  # __contains__ - True
159
160# Iteracja (przez __getitem__)
161print("\nWszystkie obserwacje lwów:")
162for obs in lion:
163    print(f"  - {obs['date']} w {obs['location']}: {obs['count']} osobników")
164
165# Bool (__bool__)
166print("\n=== BOOL ===")
167print(f"bool(lion)? {bool(lion)}")  # True - ma populację
168print(f"bool(extinct)? {bool(extinct)}")  # False - populacja 0
169
170if lion:
171    print("Lew ma populację!")
172
173if not extinct:
174    print("Dodo wymarł...")
175
176# Hash (__hash__) - działa w set i dict
177print("\n=== HASH - użycie w set/dict ===")
178species_set = {lion, elephant, rhino}
179print(f"Zbiór gatunków: {len(species_set)} elementów")
180
181species_dict = {
182    lion: "Drapieżnik",
183    elephant: "Roślinożerca",
184    rhino: "Roślinożerca"
185}
186print(f"Typ lwa: {species_dict[lion]}")

Lista najważniejszych metod magicznych

Inicjalizacja i reprezentacja

  • __init__(self, ...) - konstruktor
  • __new__(cls, ...) - tworzy obiekt; Python wywołuje tę metodę przed __init__ (rzadko nadpisywana)
  • __str__(self) - string dla użytkownika (print)
  • __repr__(self) - string dla programisty (repr)
  • __format__(self, format_spec) - formatowanie (f"{obj:spec}")

Porównania

  • __eq__(self, other) - równość ==
  • __ne__(self, other) - nierówność !=
  • __lt__(self, other) - mniejsze <
  • __le__(self, other) - mniejsze lub równe <=
  • __gt__(self, other) - większe >
  • __ge__(self, other) - większe lub równe >=

Arytmetyka

  • __add__(self, other) - dodawanie +
  • __sub__(self, other) - odejmowanie -
  • __mul__(self, other) - mnożenie *
  • __truediv__(self, other) - dzielenie /
  • __floordiv__(self, other) - dzielenie całkowite //
  • __mod__(self, other) - modulo %
  • __pow__(self, other) - potęgowanie **

Kontener

  • __len__(self) - długość len()
  • __getitem__(self, key) - odczyt obj[key]
  • __setitem__(self, key, value) - zapis obj[key] = value
  • __delitem__(self, key) - usunięcie del obj[key]
  • __contains__(self, item) - członkostwo item in obj
  • __iter__(self) - iterator for x in obj
  • __next__(self) - następny element

Callable i Context Manager

  • __call__(self, ...) - wywołanie obj()
  • __enter__(self) - wejście do with
  • __exit__(self, ...) - wyjście z with

Inne

  • __bool__(self) - konwersja do bool
  • __hash__(self) - hash dla set/dict
  • __del__(self) - destruktor (rzadko używane)

Podsumowanie

W tej lekcji nauczyłeś/aś się:

  • Czym są metody magiczne (dunder methods)
  • Jak sprawić, by klasy zachowywały się jak wbudowane typy
  • Implementacji __str__, __repr__, __eq__, __lt__
  • Operatorów arytmetycznych (__add__, __sub__, __mul__)
  • Zachowań kontenerowych (__len__, __getitem__, __contains__)
  • Callable objects z __call__
  • Context managers z __enter__ i __exit__
  • Praktycznego przykładu z kompletnym systemem katalogowania

Checkpoint

Przed przejściem dalej:

  • Rozumiesz różnicę między __str__ a __repr__
  • Potrafisz zaimplementować porównania
  • Wiesz jak dodać operatory arytmetyczne
  • Rozumiesz __len__, __getitem__, __iter__
  • Potrafisz stworzyć callable object z __call__

Analogia Safari: Metody magiczne to instynkty zwierząt - lew nie uczy się ryczeć, po prostu to robi! Twoje klasy też mogą mieć naturalne zachowania!

W następnej lekcji Darwin nauczy Cię type hints - jak precyzyjnie opisywać typy danych, żeby kod był bardziej czytelny i bezpieczny!

Sprawdź się

Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.

  1. 1. Jaka jest różnica między __str__ a __repr__?

  2. 2. Kiedy wywoływana jest metoda __len__?

To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.

Zadania praktyczne w grze

  • Układanie w pionie

    Uporządkuj cykl życia obiektu:

  • Edytor kodu

    Dodaj metodę __str__ do klasy Animal zwracającą 'Animal: {name}'

  • Edytor kodu

    W klasie Animal(name, species) dodaj metodę __eq__, która porównuje zwierzęta po species (to samo species = równe)

  • Klikanie w kolejności

    Kliknij elementy w prawidłowej kolejności:

  • Klikanie w kolejności

    Kliknij elementy nagłówka metody, którą wywołuje print(obiekt):

Przydatne artykuły