Cześć! Dziś zabieram Was w fascynującą podróż po metodach magicznych w Pythonie. Jeśli kiedykolwiek zastanawialiście się, jak działa operator + na własnych klasach, jak zdefiniować zachowanie obiektu podczas używania len() czy co się dzieje, gdy używacie instrukcji with - ten wpis jest dla Was.
Metody magiczne (zwane też dunder - double underscore) to jedna z najpotężniejszych funkcji Pythona, która pozwala pisać bardziej intuicyjny i elegancki kod, a jednocześnie daje ogromną kontrolę nad zachowaniem obiektów. Czym właściwie są te tajemnicze metody?
Czym są metody magiczne?
Metody magiczne to specjalne metody w Pythonie, które mają nazwy zaczynające się i kończące się podwójnym podkreśleniem, na przykład __init__ czy __add__. Dzięki nim możemy dostosować zachowanie obiektów w różnych kontekstach i sprawić, że będą działać jak wbudowane typy Pythona.
class Punkt:
def __init__(self, x, y):
self.x = x
self.y = y
def __str__(self):
return f"({self.x}, {self.y})"
p = Punkt(10, 5)
print(p) # Wyświetli: (10, 5)W powyższym przykładzie użyliśmy dwóch metod magicznych:
__init__- inicjalizuje obiekt po jego utworzeniu__str__- określa, jak obiekt jest konwertowany na string przy użyciu funkcjistr()lubprint()
Te metody są powszechnie używane np. we frameworku Django. Metoda __str__ definiuje, co nam się wyświetli jeśli w szablonie poprosimy o obiekt bez atrybutu: {{obiekt}}
Teraz przejdźmy do omówienia najważniejszych grup metod magicznych wraz z przykładami.
Uwaga: Nadpisywanie dunder methods powinno być stosowane z rozwagą, tylko gdy faktycznie poprawia czytelność kodu. Wyjątkiem są standardowe metody często używane w frameworkach jak Django, gdzie implementacja
__str__jest dobrą praktyką dla modeli, aby zapewnić czytelne reprezentacje w adminie i podczas debugowania. Zawsze wybieraj najprostsze rozwiązanie, które nie wprowadzi zamieszania.
Tworzenie i inicjalizacja obiektów
Te metody kontrolują proces tworzenia, inicjalizacji i usuwania obiektów.
__new__ vs __init__
Choć często używamy tylko __init__, warto znać różnicę między tymi metodami:
class MojaKlasa:
def __new__(cls, *args, **kwargs):
print("Tworzenie nowej instancji (wywołano __new__)")
# Tworzy i zwraca nową instancję klasy
return super().__new__(cls)
def __init__(self, nazwa):
print("Inicjalizacja obiektu (wywołano __init__)")
# ustawiamy atrybuty obiektu
self.nazwa = nazwa
obj = MojaKlasa("Python")
# Wyświetli:
# Tworzenie nowej instancji (wywołano __new__)
# Inicjalizacja obiektu (wywołano __init__)Czym się różnią?
__new__to metoda statyczna, która tworzy nową instancję klasy. Jest wywoływana przed__init__.__init__inicjalizuje już istniejący obiekt utworzony przez__new__. Używana do przekazywania parametrów do nowego obiektu.__new__musi zwrócić nowy obiekt, a__init__nie zwraca nic (zwracaNone).
__del__
Metoda __del__ jest wywoływana, gdy obiekt jest usuwany przez Garbage Collector pamięci:
class Plik:
def __init__(self, nazwa):
self.nazwa = nazwa
self.plik = open(nazwa, 'w')
print(f"Otwarto plik {nazwa}")
def __del__(self):
self.plik.close()
print(f"Zamknięto plik {self.nazwa}")
# Użycie
p = Plik("test.txt")
# Jakiś kod...
del p # Wyświetli: Zamknięto plik test.txtUwaga: lepszym rozwiązaniem do zarządzania zasobami jest używanie context managera (z metod __enter__ i __exit__), o czym będzie dalej.
Do czego jeszcze można użyć __del__?
- do zapisania lub usunięcia obiektu z bazy danych - przydatne we własnych frameworkach
- do zamknięcia połączeń sieciowych
- i tym podobnych działań sprzątających
Uwaga! Metoda __del__ nie jest zawsze wywoływana w przewidywalny sposób, co może powodować problemy przy pracy z zasobami zewnętrznymi (ref: https://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python/)
Reprezentacja obiektów
Te metody określają, jak obiekt jest reprezentowany jako string.
__str__ vs __repr__
class Osoba:
def __init__(self, imie, nazwisko, wiek):
self.imie = imie
self.nazwisko = nazwisko
self.wiek = wiek
def __str__(self):
return f"{self.imie} {self.nazwisko}, {self.wiek} lat"
def __repr__(self):
return f"Osoba('{self.imie}', '{self.nazwisko}', {self.wiek})"
person = Osoba("Jan", "Kowalski", 30)
print(str(person)) # Jan Kowalski, 30 lat
print(repr(person)) # Osoba('Jan', 'Kowalski', 30)__str__- "czytelna" reprezentacja dla użytkownika, wywoływana przezstr()iprint(). I w szablonie Django{{person}}__repr__- "techniczna" reprezentacja dla developera, wywoływana przezrepr()
Dobrą praktyką jest, aby __repr__ zwracało kod Pythona, który po wykonaniu odtworzy ten sam obiekt.
__format__
Metoda __format__ określa, jak obiekt jest formatowany przez operator {} w metodzie format() i f-stringach:
class Pieniadze:
def __init__(self, kwota):
self.kwota = kwota
def __format__(self, format_spec):
if format_spec == 'PLN':
return f"{self.kwota:.2f} zł"
elif format_spec == 'USD':
return f"${self.kwota:.2f}"
else:
return str(self.kwota)
p = Pieniadze(123.456)
print(f"{p:PLN}") # 123.46 zł
print(f"{p:USD}") # $123.46Porównywanie obiektów
Metody te pozwalają na zdefiniowanie, jak obiekty są porównywane.
class Samochod:
def __init__(self, marka, model, pojemnosc_bagaznika, liczba_miejsc):
self.marka = marka
self.model = model
self.pojemnosc_bagaznika = pojemnosc_bagaznika # w litrach
self.liczba_miejsc = liczba_miejsc
def __eq__(self, other):
if not isinstance(other, Samochod):
return False
return (self.marka, self.model) == (other.marka, other.model)
def __lt__(self, other):
# Porównujemy po pojemności bagażnika
if not isinstance(other, Samochod):
raise TypeError("Można porównać tylko z innym samochodem")
return self.pojemnosc_bagaznika < other.pojemnosc_bagaznika
def __gt__(self, other):
if not isinstance(other, Samochod):
raise TypeError("Można porównać tylko z innym samochodem")
return self.pojemnosc_bagaznika > other.pojemnosc_bagaznika
auto1 = Samochod("Toyota", "Corolla", 450, 5)
auto2 = Samochod("Ford", "Focus", 500, 5)
auto3 = Samochod("Toyota", "Corolla", 470, 5)
print(auto1 == auto3) # True, te same marki i modele
print(auto1 < auto2) # True, Toyota ma mniejszy bagażnik
print(auto1 > auto3) # FalseW powyższym przykładzie, samochody są równe, jeśli mają tę samą markę i model, ale porównywanie wielkości odbywa się na podstawie pojemności bagażnika. Moglibyśmy zdefiniować alternatywne metody porównania, na przykład według liczby miejsc:
def porownaj_wg_miejsc(auto1, auto2):
return auto1.liczba_miejsc > auto2.liczba_miejsc
print(porownaj_wg_miejsc(auto1, auto2)) # False, mają tyle samo miejscMetody porównywania:
__eq__- równość (==)__ne__- nierówność (!=)__lt__- mniejsze niż (<)__le__- mniejsze lub równe (<=)__gt__- większe niż (>)__ge__- większe lub równe (>=)
Operacje arytmetyczne
Te metody pozwalają zdefiniować, jak obiekty reagują na operatory arytmetyczne.
class Wektor:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Wektor(self.x + other.x, self.y + other.y)
def __sub__(self, other):
return Wektor(self.x - other.x, self.y - other.y)
def __mul__(self, scalar):
return Wektor(self.x * scalar, self.y * scalar)
def __str__(self):
return f"Wektor({self.x}, {self.y})"
v1 = Wektor(2, 3)
v2 = Wektor(1, 4)
v3 = v1 + v2
v4 = v1 - v2
v5 = v1 * 2
print(v3) # Wektor(3, 7)
print(v4) # Wektor(1, -1)
print(v5) # Wektor(4, 6)Podobnie możemy zdefiniować inne operacje:
__add__- dodawanie (+)__sub__- odejmowanie (-)__mul__- mnożenie (*)__truediv__- dzielenie (/)__floordiv__- dzielenie całkowite (//)__mod__- reszta z dzielenia (%)__pow__- potęgowanie (**)
Odwrotne operacje arytmetyczne
Metody te są wywoływane, gdy operatory są użyte w odwrotnej kolejności:
class Liczba:
def __init__(self, wartosc):
self.wartosc = wartosc
def __mul__(self, other):
print("Wywołano __mul__")
return Liczba(self.wartosc * other)
def __rmul__(self, other):
print("Wywołano __rmul__")
return Liczba(self.wartosc * other)
def __str__(self):
return str(self.wartosc)
l = Liczba(5)
print(l * 2) # Wywołano __mul__, 10
print(2 * l) # Wywołano __rmul__, 10Kiedy piszemy 2 * l, Python najpierw szuka metody __mul__ w obiekcie 2, ale ponieważ jest to wbudowany typ, który nie wie jak pomnożyć się przez naszą klasę, Python następnie szuka metody __rmul__ w obiekcie l.
Metody odwrotnych operacji:
__radd__,__rsub__,__rmul__, itd.
Operacje w miejscu
Te metody są wywoływane dla operatorów, które modyfikują obiekt w miejscu:
class Lista:
def __init__(self, elementy):
self.elementy = elementy[:] # Kopia listy
def __iadd__(self, other):
# += operator
self.elementy += other
return self
def __str__(self):
return str(self.elementy)
lista = Lista([1, 2, 3])
lista += [4, 5]
print(lista) # [1, 2, 3, 4, 5]Metody operacji w miejscu:
__iadd__- +=__isub__- -=__imul__- *=- I tak dalej...
Metody kontenerów
Te metody pozwalają na zachowanie obiektów jak kolekcji (listy, słowniki, itp.):
class MojaLista:
def __init__(self, *elementy):
self.elementy = list(elementy)
def __len__(self):
return len(self.elementy)
def __getitem__(self, indeks):
return self.elementy[indeks]
def __setitem__(self, indeks, wartosc):
self.elementy[indeks] = wartosc
def __delitem__(self, indeks):
del self.elementy[indeks]
def __contains__(self, item):
return item in self.elementy
def __iter__(self):
return iter(self.elementy)
def __reversed__(self):
return reversed(self.elementy)
lista = MojaLista(1, 2, 3, 4, 5)
print(len(lista)) # 5
print(lista[2]) # 3
lista[1] = 10
print(lista[1]) # 10
print(3 in lista) # True
print(6 in lista) # False
for element in lista:
print(element, end=' ') # 1 10 3 4 5
print()
del lista[0]
for element in lista:
print(element, end=' ') # 10 3 4 5Główne metody kontenerów:
__len__- długość (len(obj))__getitem__- pobieranie elementu (obj[key])__setitem__- ustawianie elementu (obj[key] = value)__delitem__- usuwanie elementu (del obj[key])__contains__- sprawdzanie przynależności (item in obj)__iter__- iteracja (for x in obj)__reversed__- odwrócona iteracja (reversed(obj))
Zarządzanie kontekstem (with statement)
Te metody pozwalają na używanie obiektów w instrukcji with:
class Plik:
def __init__(self, nazwa, tryb='r'):
self.nazwa = nazwa
self.tryb = tryb
def __enter__(self):
print(f"Otwieranie pliku {self.nazwa}")
self.plik = open(self.nazwa, self.tryb)
return self.plik
def __exit__(self, exc_type, exc_val, exc_tb):
print(f"Zamykanie pliku {self.nazwa}")
self.plik.close()
# Zwracamy False, aby propagować wyjątki
return False
# Użycie:
with Plik("test.txt", "w") as f:
f.write("Hello, World!")
# Po wyjściu z bloku `with` plik zostanie automatycznie zamkniętyMetody zarządzania kontekstem:
__enter__- wejście do bloku with__exit__- wyjście z bloku with (także w przypadku wyjątku)
Metody dynamiczne
Te metody pozwalają na dynamiczne zachowanie obiektów:
__call__
Metoda __call__ pozwala na wywołanie obiektu jak funkcji:
class Mnoznik:
def __init__(self, wspolczynnik):
self.wspolczynnik = wspolczynnik
def __call__(self, x):
return x * self.wspolczynnik
podwoj = Mnoznik(2)
potroj = Mnoznik(3)
print(podwoj(5)) # 10
print(potroj(5)) # 15__getattr__, __setattr__, __delattr__
Te metody kontrolują dostęp do atrybutów:
class LogujacyDostep:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
def __getattr__(self, nazwa):
print(f"Próba dostępu do nieistniejącego atrybutu: {nazwa}")
return None
def __setattr__(self, nazwa, wartosc):
print(f"Ustawianie {nazwa} = {wartosc}")
self.__dict__[nazwa] = wartosc
def __delattr__(self, nazwa):
print(f"Usuwanie atrybutu {nazwa}")
if nazwa in self.__dict__:
del self.__dict__[nazwa]
else:
print(f"Atrybut {nazwa} nie istnieje")
obj = LogujacyDostep(a=1, b=2)
print(obj.a) # 1
print(obj.c) # Próba dostępu do nieistniejącego atrybutu: c, None
obj.d = 4 # Ustawianie d = 4
del obj.b # Usuwanie atrybutu b
del obj.e # Usuwanie atrybutu e, Atrybut e nie istnieje__dir__
Metoda __dir__ kontroluje, co zwraca funkcja dir() dla obiektu:
class UkrytaKlasa:
def __init__(self):
self.publiczna = "Widoczna"
self._prywatna = "Niewidoczna"
self.__ukryta = "Bardzo ukryta"
def __dir__(self):
# Zwracamy tylko publiczne atrybuty
return ['publiczna', 'metoda_publiczna']
def metoda_publiczna(self):
pass
def _metoda_prywatna(self):
pass
obj = UkrytaKlasa()
print(dir(obj)) # ['metoda_publiczna', 'publiczna']Metoda dir na obiekcie np. dir(person) jest szalenie przydatnym narzędziem do debugowania - pozwala zobaczyć jakie metody i atrybuty ma obiekt, często lepiej niż print(person). Polecam sprawdzić.
Inne metody magiczne
Istnieje jeszcze wiele innych metod magicznych, oto kilka z nich:
__bool__
Metoda __bool__ określa, czy obiekt jest prawdziwy w kontekście warunku:
class Konto:
def __init__(self, saldo=0):
self.saldo = saldo
def __bool__(self):
# Konto jest prawdziwe, jeśli ma dodatnie saldo
return self.saldo > 0
konto1 = Konto(100)
konto2 = Konto(0)
if konto1:
print("Konto 1 ma środki") # Zostanie wyświetlone
if konto2:
print("Konto 2 ma środki") # Nie zostanie wyświetlone__hash__
Metoda __hash__ pozwala na używanie obiektów jako kluczy w słownikach:
class Osoba:
def __init__(self, imie, nazwisko, pesel):
self.imie = imie
self.nazwisko = nazwisko
self.pesel = pesel
def __eq__(self, other):
if not isinstance(other, Osoba):
return False
return self.pesel == other.pesel
def __hash__(self):
# Używamy PESEL jako unikalnego identyfikatora
return hash(self.pesel)
osoby = {}
o1 = Osoba("Jan", "Kowalski", "12345678901")
o2 = Osoba("Anna", "Nowak", "10987654321")
o3 = Osoba("Jan", "Inny", "12345678901") # Ten sam PESEL co o1
osoby[o1] = "Dane Jana K."
osoby[o2] = "Dane Anny N."
osoby[o3] = "Dane Jana I." # Nadpisze dane o1, bo mają ten sam hash
print(len(osoby)) # 2, nie 3, bo o1 i o3 są uznawane za równe (ten sam PESEL)
print(o1 == o3) # True__copy__ i __deepcopy__
Te metody kontrolują zachowanie podczas kopiowania obiektów:
import copy
class Dane:
def __init__(self, nazwa, wartosci):
self.nazwa = nazwa
self.wartosci = wartosci
def __copy__(self):
print("Wywołano __copy__")
# Płytka kopia - kopiuje referencje do obiektów
return Dane(self.nazwa, self.wartosci)
def __deepcopy__(self, memo):
print("Wywołano __deepcopy__")
# Głęboka kopia - tworzy nowe kopie wszystkich obiektów
return Dane(self.nazwa, copy.deepcopy(self.wartosci, memo))
def __str__(self):
return f"Dane({self.nazwa}, {self.wartosci})"
d1 = Dane("Test", [1, 2, [3, 4]])
d2 = copy.copy(d1) # Wywołano __copy__
d3 = copy.deepcopy(d1) # Wywołano __deepcopy__
d1.wartosci[2][0] = 999
print(d1) # Dane(Test, [1, 2, [999, 4]])
print(d2) # Dane(Test, [1, 2, [999, 4]]) - zmieniło się, bo to płytka kopia
print(d3) # Dane(Test, [1, 2, [3, 4]]) - nie zmieniło się, bo to głęboka kopiaPodsumowanie
Metody magiczne są jedną z najpotężniejszych i najbardziej eleganckich funkcji Pythona. Pozwalają na tworzenie obiektów, które zachowują się jak wbudowane typy, co czyni kod bardziej intuicyjnym i naturalnym.
Dzięki metodom magicznym możemy:
- Dostosować tworzenie i inicjalizację obiektów
- Zdefiniować, jak obiekty są reprezentowane jako stringi
- Określić, jak obiekty są porównywane
- Zdefiniować operacje arytmetyczne na obiektach
- Sprawić, by obiekty zachowywały się jak kontenery
- Używać obiektów w instrukcji
with - Tworzyć obiekty, które można wywoływać jak funkcje
- I wiele więcej!
Chociaż początkowo metody magiczne mogą wydawać się skomplikowane, warto się ich nauczyć, ponieważ są one fundamentem wielu zaawansowanych technik programowania w Pythonie.
Jeśi jesteś bardziej zainteresowany tematem, napisz do mnie lub porozmawiaj z AI: chatGPT lub Claude z pewnością bardzo szczegółowo odpowiedzą Ci na pytania. Możesz wkleić kod z tego posta i poprosić AI o wyjaśnienia. W ten sposób stworzysz sobie własnego korepetytora 🙂
Spodobał Ci się post?
Podziel się nim!
Masz uwagi do posta, chcesz porozmawiać, szukasz pomocy z Pythonem i Django? Napisz do mnie!
Zapraszam do zadawania pytań przez formularz kontaktowy. Pamiętaj, że jeśli potrzebujesz wsparcia, możesz napisać do mnie - pomogę.
Jeśli jesteście zainteresowani pogłębieniem wiedzy o Django i Pythonie, zapraszam do dołączenia do społeczności PyMasters, gdzie regularnie omawiamy najnowsze technologie i najlepsze praktyki. A dla początkujących programistów przygotowałem darmowego ebooka "WARSZTAT JUNIORA: Przewodnik po kluczowych kompetencjach i narzędziach dla początkującego programisty Pythona".
Do następnego razu!