Kurs Python · Moduł 3: Programowanie obiektowe
Metody magiczne - ukryte zachowania zwierząt
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 samo3. __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 wagaTip: 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)- odczytobj[key]__setitem__(self, key, value)- zapisobj[key] = value__delitem__(self, key)- usunięciedel obj[key]__contains__(self, item)- członkostwoitem in obj__iter__(self)- iteratorfor x in obj__next__(self)- następny element
Callable i Context Manager
__call__(self, ...)- wywołanieobj()__enter__(self)- wejście dowith__exit__(self, ...)- wyjście zwith
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. Jaka jest różnica między __str__ a __repr__?
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):