No history yet

Zaawansowane Programowanie Obiektowe

Magia metod specjalnych

Wiesz już, jak tworzyć klasy i obiekty w Pythonie. Ale czy zastanawiałeś się kiedyś, jak to się dzieje, że możesz dodawać dwie liczby za pomocą znaku + albo sprawdzać długość listy funkcją len()? Odpowiedzią są metody specjalne, często nazywane metodami „dunder” (od podwójnego podkreślenia, ang. double underscore).

To one pozwalają obiektom Twoich klas zachowywać się jak wbudowane typy Pythona. Definiując metody takie jak __add__ czy __len__, dajesz swoim obiektom nowe supermoce. Zamiast pisać vector1.add(vector2), możesz po prostu użyć operatora vector1 + vector2. To sprawia, że kod staje się bardziej intuicyjny i czytelny.

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

    def __add__(self, inny):
        # Ta metoda jest wywoływana, gdy używamy operatora +
        return Wektor(self.x + inny.x, self.y + inny.y)

    def __repr__(self):
        # Ta metoda kontroluje, jak obiekt jest wyświetlany
        return f"Wektor({self.x}, {self.y})"

# Użycie
w1 = Wektor(2, 3)
w2 = Wektor(5, 1)

w3 = w1 + w2  # Python w tle wywołuje w1.__add__(w2)
print(w3)     # Wyświetli: Wektor(7, 4)

Metody dunder pozwalają na przeciążanie operatorów, co jest kluczem do pisania kodu w stylu, który nazywamy „pythonicznym” – idiomatycznym i eleganckim.

Ukrywanie danych z @property

Enkapsulacja to jeden z filarów programowania obiektowego. Chodzi o to, by ukryć wewnętrzny stan obiektu i udostępnić tylko kontrolowany interfejs do interakcji z nim. W Pythonie nie mamy słów kluczowych private czy public, jak w innych językach. Zamiast tego używamy konwencji – atrybuty zaczynające się od jednego podkreślenia (np. _nazwa) są traktowane jako wewnętrzne.

Ale co, jeśli chcemy dodać logikę podczas odczytu lub zapisu atrybutu? Tutaj z pomocą przychodzi dekorator . Pozwala on zdefiniować metodę, która zachowuje się jak atrybut. Dzięki temu możesz dodać walidację lub inne operacje, nie zmieniając sposobu, w jaki użytkownicy Twojej klasy z niej korzystają.

class Pracownik:
    def __init__(self, imie, pensja):
        self.imie = imie
        self._pensja = pensja # Konwencja: atrybut wewnętrzny

    @property
    def pensja(self):
        """To jest 'getter' dla pensji."""
        print("Pobieranie pensji...")
        return self._pensja

    @pensja.setter
    def pensja(self, wartosc):
        """To jest 'setter' dla pensji z walidacją."""
        if wartosc < 0:
            raise ValueError("Pensja nie może być ujemna!")
        print("Ustawianie pensji...")
        self._pensja = wartosc

jan = Pracownik("Jan", 5000)

# Dostęp jak do zwykłego atrybutu, ale wywołuje metodę @property
print(jan.pensja) # Wyświetli: Pobieranie pensji... 5000

# Zapis jak do atrybutu, ale wywołuje metodę @pensja.setter
jan.pensja = 6000 # Wyświetli: Ustawianie pensji...

# jan.pensja = -100 # To rzuciłoby wyjątek ValueError

Dziedziczenie wielokrotne i MRO

Python, w przeciwieństwie do niektórych języków, pozwala klasie dziedziczyć po wielu klasach nadrzędnych. To potężne narzędzie, ale niesie ze sobą pewne wyzwanie: co, jeśli dwie klasy nadrzędne mają metodę o tej samej nazwie? Którą z nich powinna wywołać klasa dziedzicząca?

Problem ten rozwiązuje algorytm (MRO). Definiuje on dokładną, przewidywalną kolejność, w jakiej Python przeszukuje hierarchię klas w poszukiwaniu metody. Możesz sprawdzić MRO dla dowolnej klasy, używając atrybutu __mro__ lub metody .mro().

class A:
    def ping(self):
        print('ping w A')

class B(A):
    def pong(self):
        print('pong w B')

class C(A):
    def ping(self):
        print('ping w C')

class D(B, C):
    def pingpong(self):
        print('pingpong w D')

d = D()
d.ping() # Wyświetli 'ping w C'

# Sprawdźmy MRO dla klasy D
# Kolejność: D -> B -> C -> A -> object
print(D.mro())

W powyższym przykładzie, mimo że B jest pierwsze na liście dziedziczenia dla D, Python wybiera metodę ping z klasy C. Dzieje się tak, ponieważ algorytm MRO dba o to, by najpierw przeszukać wszystkie klasy nadrzędne w danej gałęzi, zanim przejdzie do następnej.

Kompozycja kontra dziedziczenie

Dziedziczenie to potężny mechanizm, ale bywa nadużywany. Często prowadzi do tworzenia sztywnych i skomplikowanych hierarchii klas. Złota zasada projektowania obiektowego brzmi: (ang. favor composition over inheritance).

Na czym polega różnica? Dziedziczenie tworzy relację "jest" (np. Samochod jest Pojazdem). Kompozycja tworzy relację "ma" (np. Samochod ma Silnik). W kompozycji obiekt jednej klasy zawiera w sobie obiekt innej klasy jako swój atrybut. To podejście promuje małe, wyspecjalizowane klasy, które można łatwo łączyć w bardziej złożone struktury.

CechaDziedziczenieKompozycja
Typ relacji"Jest" (is-a)"Ma" (has-a)
PowiązanieŚcisłe (tight coupling)Luźne (loose coupling)
ElastycznośćMniejsza, statycznaWiększa, dynamiczna
Ponowne użyciePonowne użycie implementacjiPonowne użycie interfejsu
Kiedy używaćGdy podklasa jest prawdziwą specjalizacją nadklasyGdy obiekt potrzebuje określonej funkcjonalności lub danych
Lesson image

Definiowanie interfejsów z ABC

W dużych projektach często chcemy zdefiniować pewien "kontrakt" lub interfejs, który muszą spełniać różne klasy. Na przykład, możemy chcieć, aby każda klasa reprezentująca źródło danych miała metodę read(). Jak to wymusić?

Z pomocą przychodzą Klasy Abstrakcyjne Bazowe (Abstract Base Classes, ABC) z modułu abc. Klasa abstrakcyjna to taka, której nie można bezpośrednio utworzyć obiektu. Służy ona jako szablon dla innych klas. Możemy w niej zdefiniować metody abstrakcyjne – metody bez implementacji. Każda klasa, która dziedziczy po klasie abstrakcyjnej, musi zaimplementować wszystkie jej metody abstrakcyjne. W przeciwnym razie Python zgłosi błąd.

from abc import ABC, abstractmethod

class ZrodloDanych(ABC):
    @abstractmethod
    def read(self):
        """Odczytuje dane ze źródła."""
        pass

class PlikLokalny(ZrodloDanych):
    def read(self):
        print("Czytam dane z pliku lokalnego...")
        return "dane_z_pliku"

class AdresAPI(ZrodloDanych):
    def read(self):
        print("Pobieram dane z API...")
        return "dane_z_api"

class BazaDanych(ZrodloDanych):
    # Ta klasa nie implementuje metody read(), więc jest nieprawidłowa
    def connect(self):
        print("Łączę z bazą danych...")

# Użycie
plik = PlikLokalny()
api = AdresAPI()

# To zadziała
print(plik.read())
print(api.read())

# To rzuci błąd TypeError, ponieważ nie można utworzyć instancji klasy abstrakcyjnej
# db = BazaDanych()

Używanie ABC to świetny sposób na budowanie solidnych i przewidywalnych architektur oprogramowania, gdzie różne komponenty mogą na sobie polegać, wiedząc, że spełniają określony interfejs.

Czas sprawdzić, co zapamiętałeś.

Quiz Questions 1/5

Która metoda specjalna (dunder) musi zostać zaimplementowana w klasie, aby można było używać wbudowanej funkcji len() na jej obiektach?

Quiz Questions 2/5

Jaki jest główny cel stosowania dekoratora @property w klasie Pythona?

Opanowanie tych zaawansowanych koncepcji obiektowych pozwoli Ci pisać bardziej elastyczny, skalowalny i łatwiejszy w utrzymaniu kod w Pythonie.