1/30
Dziedziczenie i wielodziedziczenie

Wykład 4: Budowanie hierarchii i reużywalność kodu

W dzisiejszym świecie programowania nikt nie pisze wszystkiego od zera. Dziedziczenie pozwala nam rozszerzać istniejące już rozwiązania, dodając do nich nową funkcjonalność.

  • Dziedziczenie (Inheritance): Mechanizm przejmowania cech i zachowań.
  • Relacja "is-a": Podstawowa zasada budowania hierarchii.
  • Super i Overriding: Jak modyfikować zachowania rodzica.
  • Wielodziedziczenie: Kiedy jeden rodzic to za mało.
  • MRO: Algorytm poszukiwania metod w gąszczu klas.
  • Kompozycja: Dlaczego czasem lepiej nie dziedziczyć.
2/30
Co to jest dziedziczenie?

Przekazywanie cech

Dziedziczenie pozwala zdefiniować nową klasę na podstawie klasy już istniejącej.

Dzięki temu nowa klasa (klasa pochodna) automatycznie zyskuje wszystkie atrybuty i metody klasy bazowej (klasy rodzica).

  • Pozwala uniknąć powtarzania kodu (zasada DRY - Don't Repeat Yourself).
  • Ułatwia zarządzanie kodem - poprawka w klasie bazowej trafia do wszystkich dzieci.
  • Tworzy logiczną strukturę danych w programie.
Dziedziczenie odzwierciedla relację "JEST" (ang. is-a). Na przykład: Pies JEST Zwierzęciem, Samochód JEST Pojazdem.
3/30
Klasa Bazowa i Klasa Pochodna

Rodzic i Dziecko

W programowaniu obiektowym używamy kilku zamiennych określeń:

Termin Opis
Klasa Bazowa / Superklasa / Rodzic Klasa, po której dziedziczymy (ogólniejsza).
Klasa Pochodna / Podklasa / Dziecko Klasa, która dziedziczy (bardziej szczegółowa).

W Pythonie klasa pochodna może posiadać dowolną liczbę rodziców (wielodziedziczenie), co jest unikalną cechą na tle innych języków (np. Javy).

4/30
Składnia dziedziczenia

Jak to zapisać?

W nawiasie po nazwie klasy podajemy nazwę klasy bazowej.

class Pojazd:
    def     trąb(self):
        print("BIIIP!")

# Samochód dziedziczy po Pojazd class Samochod(Pojazd): pass
auto = Samochod() auto.trąb() # Działa, mimo że Samochod nie ma tej metody!

Słowo kluczowe pass oznacza tutaj "nie dodawaj nic nowego, bądź dokładnie taki jak rodzic".

5/30
Przykład: Królestwo Zwierząt

Hierarchia w praktyce

class Zwierze:
    def __init__(self, imie):
        self.imie = imie

def oddychaj(self): print("Wdech, wydech...")
class Pies(Zwierze): def szczekaj(self): print("Hau!")
reksio = Pies("Reksio") reksio.oddychaj() # Zyskane od Zwierzecia reksio.szczekaj() # Własne zachowanie

Obiekt reksio posiada zarówno imię (atrybut), jak i umiejętność oddychania (metoda), mimo że nie zostały one zdefiniowane w klasie Pies.

6/30
Nadpisywanie metod (Overriding)

Zmiana zasad

Czasem zachowanie rodzica jest zbyt ogólne. Podklasa może zdefiniować metodę o tej samej nazwie, co rodzic, aby ją nadpisać.

class Ptak:
    def wydaj_dzwiek(self):
        print("Ćwir ćwir")

class Kruk(Ptak): def wydaj_dzwiek(self): print("KRAAA!")
k = Kruk() k.wydaj_dzwiek() # Wypisze: KRAAA!

Mechanizm ten pozwala na specyficzne zachowanie obiektów różnych klas przy wywołaniu tej samej nazwy metody.

7/30
Funkcja super() - Współpraca

Nie zapominaj o rodzicu

Często nadpisując metodę, nie chcemy całkowicie pozbyć się kodu rodzica, a jedynie go rozszerzyć. Używamy do tego funkcji super().

class Pracownik:
    def pracuj(self):
        print("Wykonuję standardowe zadania.")

class Programista(Pracownik): def pracuj(self): super().pracuj() # Wywołaj kod Pracownika print("Piszę kod w Pythonie.")

super() to dynamiczne odniesienie do klasy bazowej w hierarchii MRO.

8/30
super() w konstruktorze

Inicjalizacja fundamentów

Najczęstsze użycie super() to konstruktor __init__. Pozwala upewnić się, że atrybuty rodzica zostaną poprawnie ustawione.

class Osoba:
    def __init__(self, imie, nazwisko):
        self.imie = imie
        self.nazwisko = nazwisko

class Student(Osoba): def __init__(self, imie, nazwisko, indeks): super().__init__(imie, nazwisko) self.indeks = indeks
Zawsze pamiętaj o wywołaniu konstruktora rodzica, jeśli go nadpisujesz! Bez tego atrybuty z klasy bazowej nie zostaną utworzone.
9/30
Dziedziczenie wielopoziomowe

Wielopokoleniowość

Dziedziczenie może tworzyć długie łańcuchy (Pancernik -> Ssak -> Zwierze).

class A: pass
class B(A): pass
class C(B): pass

W tym przypadku klasa C dziedziczy wszystko po B, a pośrednio także po A. Każda klasa w Pythonie ostatecznie dziedziczy po wbudowanej klasie object.

10/30
Przykład: System Bankowy

Konta i ich rodzaje

class Konto:
    def __init__(self, balans):
        self._balans = balans
    def zaksieguj(self, kwota):
        self._balans += kwota

class KontoOszczednosciowe(Konto):
    def dolicz_odsetki(self):
        self._balans *= 1.05

Konto oszczędnościowe to zwykłe konto, które "umie coś więcej". Wszystkie operacje wpłat/wypłat są dziedziczone.

11/30
Blokowanie dziedziczenia

Finalne wersje

W Pythonie (od wersji 3.8) możemy oznaczyć klasę jako "finalną", sugerując, że nie powinna być rodzicem dla innych.

from typing import final

@final class WaznySystem: pass
class Hack(WaznySystem): # Static type checker zgłosi błąd! pass

Należy pamiętać, że Python sam w sobie nie "zatrzyma" wykonania takiego kodu - błąd zgłoszą jedynie narzędzia do analizy statycznej (np. Mypy).

12/30
Wielodziedziczenie (Multiple Inheritance)

Jeden byt, wiele natur

Python pozwala na dziedziczenie po więcej niż jednej klasie jednocześnie.

Jest to przydatne, gdy obiekt powinien posiadać cechy dwóch niezależnych od siebie systemów.

class Latawiec:
    def lec(self): print("Lecę!")

class Pływak: def plyn(self): print("Płynę!")
class Wodolot(Latawiec, Plywak): pass

Wodolot ma teraz obie metody: lec() oraz plyn().

13/30
Problem Diamentu (Diamond Problem)

Genealogiczna pułapka

Pojawia się, gdy dwie klasy bazowe dziedziczą po tej samej klasie nadrzędnej i obie nadpisują tę samą metodę.

    A (start)
   / \
  B   C
   \ /
    D (koniec)

Jeśli D wywoła metodę z A, to czy ma iść ścieżką przez B czy przez C? Programista musi wiedzieć, jaka jest kolejność poszukiwania.

Wiele języków (Java, C#) zabrania wielodziedziczenia klas właśnie z powodu tego problemu. Python rozwiązuje go za pomocą algorytmu MRO.
14/30
MRO - Method Resolution Order

Kolejność poszukiwań

MRO to lista klas, które Python sprawdza w poszukiwaniu metody lub atrybutu, zaczynając od klasy samego obiektu.

Zasady MRO (Algorytm C3):

  1. Podklasy przed nadklasami.
  2. Kolejność rodziców w nawiasie (od lewej do prawej).
  3. Brak dublowania klas w hierarchii.

Możesz to sprawdzić w konsoli używając help(Klasa) lub Klasa.mro().

15/30
Sprawdzanie MRO w kodzie

Dowód empiryczny

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(D.mro()) # Wynik: [D, B, C, A, object]

Python najpierw sprawdzi B, potem C, a na samym końcu wspólnego przodka A oraz klasę object.

16/30
Domieszki (Mixins)

Dodatkowe umiejętności

Mixin to klasa, która nie jest przeznaczona do tworzenia samodzielnych obiektów. Służy jedynie do "wstrzykiwania" konkretnych metod do innych klas przez wielodziedziczenie.

  • Mixiny zazwyczaj nie mają własnego stanu (atrybutów).
  • Są krótkie i realizują jedno zadanie (np. logowanie, serializacja).
  • Pozwalają uniknąć skomplikowanych i "głębokich" hierarchii dziedziczenia.
17/30
Przykład Mixinu: Logger

Uniwersalne logowanie

class LogMixin:
    def log(self, msg):
        print(f"[LOG - {self.__class__.__name__}]: {msg}")

class BazaDanych(LogMixin): def zapisz(self): self.log("Zapisywanie danych...")
db = BazaDanych() db.zapisz()

Możesz dodać LogMixin do dowolnej klasy w projekcie, dając jej natychmiastową możliwość logowania.

18/30
Dziedziczenie vs Kompozycja

Odwieczny dylemat

Dziedziczenie (is-a): Używaj tylko, gdy klasa pochodna jest prawdziwym, specjalistycznym wariantem rodzica.

Kompozycja (has-a): Używaj, gdy klasa "składa się" z innych obiektów lub z nich korzysta.

Złota zasada: "Favor object composition over class inheritance" (Preferuj kompozycję obiektów nad dziedziczenie klas). Nadmierne dziedziczenie tworzy sztywny kod, który trudno zmienić.
19/30
Przykład Kompozycji

Samochód ma Silnik

class Silnik:
    def start(self): print("Wrrr!")

class Samochod:
    def __init__(self):
        self.silnik = Silnik() # Kompozycja

def jedz(self): self.silnik.start() print("Jadę...")

Samochód nie jest silnikiem, on go posiada. Możemy łatwo wymienić silnik na elektryczny, nie zmieniając całej struktury klasy Samochod.

20/30
isinstance() i issubclass()

Testy tożsamości

W programowaniu obiektowym często musimy sprawdzić, czy obiekt pasuje do oczekiwanej klasy lub jej potomków.

  • isinstance(obiekt, Klasa): Czy ten konkretny obiekt jest instancją podanej klasy lub czegoś, co po niej dziedziczy?
  • issubclass(KlasaA, KlasaB): Czy definicja Klasy A wywodzi się z Klasy B?
print(isinstance(reksio, Zwierze)) # True
print(issubclass(Pies, Zwierze))    # True
21/30
Zasada Liskov (LSP)

Kontrakt o niezmienności

Zasada podstawienia Liskov mówi: "Obiekty klasy bazowej powinny być zastępowalne przez obiekty klas pochodnych bez wpływu na poprawność programu".

Jeśli funkcja oczekuje Zwierze, to po podaniu Pies wszystko musi nadal działać poprawnie. Jeśli podanie Pies psuje logikę - dziedziczenie jest źle zaprojektowane.

22/30
Wielowarstwowe init - Problem

Pułapka ręcznego wywoływania

Zamiast KlasaBazowa.__init__(self), zawsze używaj super().__init__().

# ŹLE (sztywne powiązanie):
class B(A):
    def __init__(self):
        A.__init__(self)

# DOBRZE (elastyczna hierarchia):
class B(A):
    def __init__(self):
        super().__init__()

Użycie super() gwarantuje, że w wielodziedziczeniu każda klasa zostanie zainicjalizowana dokładnie raz, zgodnie z porządkiem MRO.

23/30
Hermetyzacja a Dziedziczenie

Co widzi dziecko?

Pamiętasz podkreślniki z poprzedniego wykładu?

  • self._atrybut (chroniony): Jest widoczny i dostępny w podklasach. To najczęstszy sposób współdzielenia szczegółów implementacji klasy.
  • self.__atrybut (prywatny): Przez name mangling podklasa nie zobaczy go pod tą samą nazwą. Jest on "bardziej ukryty".

Zaleca się używanie pojedynczego podkreślnika, aby umożliwić podklasom bezpieczne rozszerzanie funkcjonalności.

24/30
Mixins - Najlepsze praktyki

Jak nie zepsuć diamentu?

Pisząc Mixiny:

  1. Nazywaj je z końcówką Mixin (np. JsonSerializerMixin).
  2. Nie definiuj w nich konstruktora __init__.
  3. W wielodziedziczeniu stawiaj Mixiny po lewej stronie (przed główną klasą bazową).
class User(JsonMixin, DbMixin, BaseModel):
    pass
25/30
Przykład: Serializacja do JSON

Potęga Mixinów

import json

class AsJsonMixin: def to_json(self): return json.dumps(self.__dict__)
class Produkt(AsJsonMixin): def __init__(self, nazwa, cena): self.nazwa = nazwa self.cena = cena
p = Produkt("Kawa", 25) print(p.to_json())
26/30
Kiedy unikać dziedziczenia?

Antywzorce

Nie dziedzicz tylko po to, żeby "ukraść" jedną metodę z innej klasy. To doprowadzi do chaosu.

Nigdy nie dziedzicz, jeśli relacja JEST (is-a) nie ma sensu logicznego. Na przykład: klasa Ptak nie powinna dziedziczyć po klasie Skrzydlo. Skrzydło to część ptaka (kompozycja), a nie jego przodek.

Jeśli Twoja klasa zaczyna mieć więcej niż 3-4 poziomy dziedziczenia, prawdopodobnie czas pomyśleć o kompozycji.
27/30
Szybkie zadanie: Zwierzęta

Spróbuj sam

Dopisz brakujące klasy tak, aby poniższy kod zadziałał:

# Twoje klasy tutaj...

kot = Kot("Mruczek") kot.wydaj_dzwiek() # Powinno wypisać: Miau! print(kot.imie) # Powinno wypisać: Mruczek

Podpowiedź: Użyj klasy bazowej Zwierze i funkcji super().

28/30
Podsumowanie wykładu

Co musisz zapamiętać?

  1. Dziedziczenie to relacja "is-a" (jest).
  2. Służy do reużywania i porządkowania kodu.
  3. super() pozwala na wywoływanie metod rodzica.
  4. Wielodziedziczenie w Pythonie jest potężne, ale wymaga znajomości MRO.
  5. Warto rozważyć Kompozycję zanim zdecydujesz się na Dziedziczenie.
29/30
Najczęstszy błąd początkujących

Zapomniany super()

Błąd, który zdarza się każdemu:

class A:
    def __init__(self):
        self.x = 10

class B(A):
    def __init__(self):
        self.y = 20 # BRAK super().__init__()!

obj = B()
print(obj.x) # AttributeError: 'B' object has no attribute 'x'

Zawsze sprawdzaj, czy zainicjalizowałeś rodzica!

30/30
Koniec Części 4

Co dalej?

W kolejnej części zajmiemy się Metodami Magicznymi (dunder methods).

Dowiesz się, jak sprawić, by Twoje obiekty można było dodawać, odejmować, a nawet wyświetlać w czytelny sposób za pomocą zwykłego print().


Dziękuję za uwagę!