W poprzedniej lekcji, zapoznaliśmy się z podstawami programowania obiektowego w Python. W tej, dowiemy się co to jest dziedziczenie i jak je zastosować w praktyce. Zaczynajmy!

Powiedzmy, chcemy zaimplementować obiekty odpowiadające ptakom: sowa, orzeł, pingwin. Każdy z tych ptaków jest inny, ale wszystkie mają wiele cech i metod wspólnych:

Wspólne cechy: skrzydła, max prędkość, kolor itd

Wspólne operacje: latanie, wydawanie odgłosów itd

Mogli byśmy zaimplementować 3 klasy, odpowiadające ptakom – sowa, orzeł, pingwin, i od początku do końca w nich zdefiniować nasze konstruktory oraz metody. Programowanie jednak nie lubi powtarzalności. Skoro, wszystkie te 3 obiekty, posiadają cechy i metody wspólne, czy można to zapisać, tak aby aby to co wspólne, napisać raz, a potem wykorzystać?

I tutaj do gry wchodzi dziedziczenie.

Co to jest dziedziczenie?

Dziedziczenie, tak jak nazwa wskazuje, jest mechanizmem, który pozwala przekazać do klas sowa, orzeł, pingwin, informację, że cześć cech oraz metod jest zdefiniowana w innej klasie, a one same, są potomkami tej klasy:

klasa ptak, ma kolor, szybkość, wielkość, lata, wydaje odgłosy

klasa sowa, potomek klasy ptak, dodatkowo czuwa w nocy

klasa orzeł, potomek klasy ptak, dodatkowo poluje

kasa pingwin, potomek klasy ptak, jednak nie lata, ale umie się ślizgać

co jak widzimy znacząco może nam uprościć definicje klas sowa, orzeł, pingwin i pozwolić na skupieniu się, tylko na tych aspektach, które faktycznie te klasy charakteryzują.

Dziedziczenie w Python

jak mogło by to wyglądać w języku Python:

class ptak:
    
    def __init__ (self, gatunek, szybkość):
        self.gatunek = gatunek
        self.szybkość = szybkość
        
    def leć(self):
        print ("Tu", self.gatunek, ". Startuje, i zaraz osiągne prędkość", self.szybkość)
        
    def wydajOdgłos(self):
        pass
    
class orzeł (ptak):
    
    def poluj(self):
            print ("Tu", self.gatunek, ". Rozpoczołem polowanie")
        
class pingwin (ptak):
        
    def ślizgaj(self):
        print ("Tu", self.gatunek, ". Rozpoczołem ślizg")
        
    def leć(self):
        print ("Tu", self.gatunek, ". Przykro mi, ale nie latam")

Co się dzieje?

  1. Zdefiniowaliśmy klasę ‚ptak’, tak jak to robiliśmy w przypadku każdej innej klasy
  2. Zdefiniowaliśmy klasę orzeł oraz pingwin, jednak po nazwie, w nawiasach, umieściliśmy informację, że klasy te dziedziczą od klasy ‚ptak’. Tym samym konstruktor, jak i metoda ‚leć()’, są teraz obecne w obydwu klasach potomnych. Również, obydwie klasy odwołuja się swobodnie do parametrów, takich jak ‚gatunek’, za pomocą ‚self’.
  3. Pingwin, z faktu że nie lata, musiał nadpisać metodę ‚leć’, klasy ptak. To nastąpiło poprzez ponową jej definicję w klasie potomnej pingwin.

A tak może wyglądać, użycie tych klas w praktyce:

Dziedziczenie w Python. Kurs, szkolenie z Python.

Wszystko, zadziałało z naszymi oczekiwaniami.

Dwie rzeczy, które wydają się bez sensu:

  1. W momencie tworzenia klas ‚orzeł’ oraz ‚pingwin’, jako pierwszy parametr, podajemy nazwę ptaka. Jest to bez sensu. Skoro tworzymy klasę orzeł, to powinna widzieć, że jest orłem, prawda? Dzieje się tak, ponieważ korzystamy z konstruktora klasy, którą dziedziczymy, a ta nie wie kto ją będzie dziedziczyć. Za chwilę to naprawimy.
  2. Klasa ptak, zawiera pustą metodę ‚wydajOdgłos()’, która została zlekceważona przez nasze klasy potomne. Tym samym, jeżeli wywołamy tą metodę, nie stanie się nic. Dobrze by było wprowadzić obowiązek jej nadpisywania, przez wszystkie klasy potomne, tak aby zawsze uzyskać jakiś odgłos. I to też zaraz naprawimy.

Dodatkowe parametry konstruktora potomka

W naszym przykładnie, potomek ‚orzeł’ oraz ‚pingwin’, korzystali, tylko i wyłącznie z konstruktora klasy ‚ptak’. A tym samym musieliśmy im podawać ich nazwę.

Możemy, wyeliminować ten problem, poprzez utworzenie dodatkowego konstruktora, już w samym potomku, który dla orła będzie przypisywać ‚gatunek = orzeł’, tak aby nie trzeba było tego robić przy tworzeniu obiektu, i analogicznie zrobić to, dla klasy pingwin.

Najpierw spójrzmy na kod, a potem go wytłumaczymy:

class ptak:
    
    def __init__ (self, gatunek, szybkość):
        self.gatunek = gatunek
        self.szybkość = szybkość
        
    def lec(self):
        print ("Tu", self.gatunek, ". Startuje, i zaraz osiągne prędkość", self.szybkość)
        
    def wydajOdgłos(self):
        pass
    
class orzeł (ptak):
    
    def __init__ (self, szybkość):
        super().__init__("orzeł", szybkość)
    
    def poluj(self):
            print ("Tu", self.gatunek, ". Rozpoczołem polowanie")
        
class pingwin (ptak):
            
    def __init__(self, szybkość):
        super().__init__("pingwin", szybkość)
        
    def ślizgaj(self):
        print ("Tu", self.gatunek, ". Rozpoczołem ślizg")
        
    def lec(self):
        print ("Tu", self.gatunek, ". Przykro mi, ale nie latam")

Zwróćmy uwagę na następujące kwestie:

  1. Dodaliśmy konstruktory w klasach ‚orzeł’ oraz ‚pingwin’. W momencie tworzenia obiektu tych klas, Python, w pierwszej kolejności wywołuje ich konstruktory.
  2. Konstruktor klas potomnych, w tym przypadku, zawiera, mniej parametrów, niż konstruktor klasy ‚ptak’
  3. Wywołane konstruktory klas potomnych, inicjują konstruktor klasy od której dziedziczą, za pomocą funkcji super ()
    def __init__ (self, szybkość):
        super().__init__("orzeł", szybkość)
    

W te sposób, osiągamy dokładnie ten sam efekt, ale eliminujemy konieczność podawania nazwy ptaka, która powinna być oczywista:

Programowanie obiektowe w Python, dziedziczenie, konstruktory. Kurs i szkolenie Python.

Kolejną kwestią jest fakt, że chcieli byśmy wymusić na klasach potomnych ‚orzeł’ oraz ‚pingwin’, nadpisanie metody klasy ‚ptak’, ‚wydajOdgłos’.

Klasy i metody abstrakcyjne

Jeżeli klasa, posiada metody abstrakcyjne, oznacza to tyle że, potomstwo jest zmuszone ją nadpisać. Równocześnie klasa taka, nigdy nie powinna być sama w sobie wykorzystana do tworzenia obiektów.

W naszym przykładzie, chcieli byśmy zamienić klasę ‚ptak’, na klasę abstrakcyjną, i to samo uczyć z jej metoda ‚wydajOdgłos’. Tym samym, nie było by możliwości utworzenia obiektu klasy ‚ptak’, natomiast klasy potomne ‚orzeł’ oraz ‚pingwin’, zmuszone były by do nadpisania tej metody.

Zobaczmy jak by to mogło wyglądać:

from abc import ABC, abstractmethod

class ptak(ABC):
    
    def __init__ (self, gatunek, szybkość):
        self.gatunek = gatunek
        self.szybkość = szybkość
        
    def lec(self):
        print ("Tu", self.gatunek, ". Startuje, i zaraz osiągne prędkość", self.szybkość)
    
    @abstractmethod
    def wydajOdgłos(self):
        pass
    
class orzeł (ptak):
    
    def __init__ (self, szybkość):
        super().__init__("orzeł", szybkość)
    
    def poluj(self):
            print ("Tu", self.gatunek, ". Rozpoczołem polowanie")
            
    def wydajOdgłos(self):
        print ("wrrrr")
        
class pingwin (ptak):
            
    def __init__(self, szybkość):
        super().__init__("pingwin", szybkość)
        
    def ślizgaj(self):
        print ("Tu", self.gatunek, ". Rozpoczołem ślizg")
        
    def lec(self):
        print ("Tu", self.gatunek, ". Przykro mi, ale nie latam")

    def wydajOdgłos(self):
        print ("kwiiiii")
        

Co się dzieje?

  1. Na początku pliku importujemy ‚ABC’ oraz ‚abstractmethod’, z pakietu abc. Jest to pakiet, który umożliwia nam proces tworzenia abstrakcyjnych klas i metod
  2. Klasa ptak, dziedziczy ABC
  3. Metody, które są poprzedzone ‚@abstractmethod’, stają się metodami abstrakcyjnymi, które muszą zostać nadpisane przez potomstwo. Samo wyrażenie ‚@abstractmethod’, jest czymś co nazywamy dekoratorem, i zajmiemy się nimi w innej lekcji. Do samego tematu programowania obiektowego, nie mamy potrzeby wchodzenia w szczegóły
  4. Klasy ‚orzeł’ oraz ‚pingwin’, są zmuszone nadpisać metody klasy ‚ptak’, oznaczone jako ‚@abstractmetod’.

W praktyce, wszystkie metody, klasy która jest dziedziczona mogą być oznaczone dekoratorem ‚@abstractmethod’. W ten sposób możemy wymusić, aby gatunki ptaków miały te same metody, ale ich implementację zostawić już dla nich samych. Co w praktyce, okazuje się być bardzo praktyczne.

Podsumowując. Dowiedzieliśmy się co to jest dziedziczenie. Jak nadpisywać metody, rozszerzać konstruktor, a nawet powiedzieliśmy o klasach oraz metodach abstrakcyjnych. Bardzo spora dawka informacji. Na ten moment zrobimy przerwę, aby samodzielnie rozwiązać kilka ćwiczeń, które utrwalą i pogłębią nasze zrozumienie programowania obiektowego.

W następnej lekcji, będziemy rozmawiać o metodach specjalnych, enkapsulacji, a potem przejdziemy do całościowego projektu, który, połączy wszystkie kropki w całość. Zapraszamy!

Ćwiczenia

Ćwiczenie1: Tematem tego ćwiczenia, będą jednoślady – skutery oraz motocykle.

Jesteśmy ich dystrybutorem i potrzebujemy napisać program który będzie nimi zarządzać. Na razie jesteśmy na etapie definiowania obiektów. Skutery i motocykle, mają wiele wspólnych cech, ale również swoje specyficzne kwestie.

Wspólne są chociażby: pojemność silnika, kolor, marka, skrzynia biegów, jedne i drugie mogą zostać uruchomione oraz mogą podać swój przebieg.

Należy zdefiniować klasę jednoślad, zawierającą cechy i metody wspólne, następnie zdefiniować klasę skuter oraz motocykl, które będę tę klasę będą dziedziczyć.

Rozwiązanie 1: Potencjalne rozwiązanie, co nie oznacza optymalne, mogło by wyglądać następująco:

Definiujemy klasę jednoślad, tworzymy konstruktor, oraz między innymi abstrakcyjną metodę ‚podajDane()’

from abc import ABC, abstractmethod

class jednoślad(ABC):
    
    wlaściciel = "MotoFun sp zoo"
    przebieg = 0
    
    def __init__(self, typ, marka, kolor):
        self.typ = typ
        self.marka = marka
        self.kolor = kolor
        
    def jedz(self):
        self.przebieg += 1
    
    def podajPrzebieg(self):
        return self.przebieg
    
    @abstractmethod
    def podajDane(self):
        pass

Następnie definiujemy klasy motocykl oraz skuter. Nadpisujemy konstruktor, ale nie zapominając o konstruktorze nadrzędnych, którego też wywołujemy. Następnie nadpisujemy metodę podajDane. Gdzie każda kasa definiuje ją po swojemu.

class motocykl(jednoślad):
    
    def __init__(self, marka, kolor, rocznik, prędkośćMax):
        super().__init__("motocykl", marka, kolor)
        self.rocznik = rocznik
        self.prędkośćMax= prędkośćMax
        
    def podajDane(self):
        
        dane = "Super szybki motocykl " + str(self.prędkośćMax) + " max km/h" + "\n"
        dane += "Marki " + self.marka + "\n"
        dane += "Przebieg " + str(self.podajPrzebieg()) + "\n"
        return dane
        
class skuter(jednoślad):

    def __init__(self, marka, kolor, rocznik, styl):
        super().__init__("skuter", marka, kolor)
        self.styl = styl
        self.skrzynia = "automatyczna"
    
    def podajDane(self):
        
        dane = "Stylowy skuter " + self.styl + "\n"
        dane += "Marki " + self.marka + "\n"
        dane += "Przebieg " + str(self.podajPrzebieg()) + "\n"
        return dane  

A następnie, tworzymy zmienną typu lista, która będzie nasza bazą danych oraz tworzymy różne obiekty i je dodajemy.

Z faktu, że wszystkie dziedziczą od jednośladu, możemy być pewni, że każdy z obiektów na liście zawiera metodę ‚podajDane()’.

bazaDanych = []
bazaDanych.append(motocykl("Honda", "czerwony", 1996, 232))
bazaDanych.append(motocykl("BMW", "czarny", 2015, 190))
bazaDanych.append(skuter("Vespa", "biały", 2001, "Włoski"))
bazaDanych.append(skuter("Junak", "odcień kości słoniowej", 2005, "Skandynawski"))

for i in bazaDanych:
    print(i.podajDane())

Python, NIE tylko dla początkujących

  1. Wstęp do kursu
  2. Moduły i pakiety
  3. Programowanie obiektowe – wstęp
  4. Programowanie obiektowe – dziedziczenie <– bieżąca lekcja
  5. Programowanie obiektowe – hermetyzacja
  6. Mini projekt – Organizer
  7. Funkcje rekurencyjne w Python
  8. Pisanie skryptów w Python
  9. Mini projekt – tajny agent, generator haseł
  10. *args oraz **kwargs
  11. Dekoratory w Python
  12. Mini projekt – tajny agent – kontakty
  13. Odwzorowanie list
  14. Podsumowanie oraz dalsze kroki