IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

22. diel - Abstraktné triedy v Pythone

V predchádzajúcom cvičení, Riešené úlohy k 18.-21. lekciu OOP v Pythone, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.

V nasledujúcom tutoriále objektového programovania v Pythone sa budeme zaoberať abstraktnými triedami. Vysvetlíme si ich účel aj praktické využitie.

Abstraktné triedy v Pythone

Povedzme, že robíme napríklad aplikáciu ZOO, kde máme niekoľko rôznych zvierat. Každé zviera má nejaké meno, váhu a má metódu na pohyb. Keďže budú tieto atribúty aj metóda na všetkých zvieratách, ponúka sa pripraviť spoločnú triedu predka.

Kód by potom vyzeral napríklad takto:

class Zvire:

    def __init__(self, jmeno, vaha):
        self.jmeno = jmeno
        self.vaha = vaha

    def pohybuj_se(self):
        pass


class Delfin(Zvire):
    def pohybuj_se(self):
        print("Plavu...")


class Ptak(Zvire):
    def pohybuj_se(self):
        print("Letím...")

Dáva v tejto chvíli zmysel vytvoriť inštanciu triedy Zvire ? Ani veľmi nie, nie je vôbec jasné, čo by sa malo stať, keď zavoláme metódu na pohyb. Ešte horšie by to bolo s metódou, ktorá má niečo vracať.

Takej triede, ako je naša Zvire, sa hovorí abstraktná trieda. Je to trieda, pri ktorej nemá zmysel vytvoriť inštanciu, pretože sama nemá definované nejaké správanie. To má definovať až jej potomok. Je to z toho dôvodu, že je všeobecná. Zviera bude však vždy konkrétne (teda nejaký potomok, napr. Delfin). Nikdy nebudeme chcieť, ani potrebovať, vytvoriť inštanciu triedy Zvire. Chceme tiež donútiť potomkov tejto triedy, aby si metódu pre pohyb implementovali po svojom – napríklad hrochy, až na vzácne výnimky dané skôr okolnosťami, nedokážu lietať:-D

Abstraktná trieda je trieda, ktorá obsahuje aspoň jednu abstraktnú metódu. Abstraktná metóda je metóda, ktorá má deklaráciu, ale neobsahuje implementáciu.

Tvorba abstraktnej triedy

Abstraktnú triedu vytvoríme tak, že zdedíme triedu ABC (skratka z Abstract Base Class). Trieda ABC sa nachádza v module abc. Na označenie metódy ako "abstraktné" v Pythone slúži dekorátor @abstractmethod. Použitím dekorátora zaistíme, že táto metóda musí byť predefinovaná (alebo, ako sa často hovorí, "prekrytá") v akejkoľvek konkrétnej (neabstraktnej) podtriede, ktorá dedí z abstraktnej triedy.

Nasledujúci kód spadne, pretože sa v ňom pokúšame vytvoriť inštanciu všeobecného zvieraťa:

from abc import ABC, abstractmethod

class Zvire(ABC):

    def __init__(self, jmeno, vaha):
        self.jmeno = jmeno
        self.vaha = vaha

    @abstractmethod
    def pohybuj_se(self):
        pass


zvire = Zvire("Alžběta", 42)

Vo výstupe uvidíme chybovú hlášku:

Chyba abstraktní třídy:
Can't instantiate abstract class Zvire with abstract method pohybuj_se

Abstraktná metóda

Ako sme už povedali, abstraktná metóda síce má deklaráciu, ale neobsahuje implementáciu. Každú metódu, ktorú v abstraktnej triede označíme dekorátorom @abstractmethod, musíme predefinovať v akejkoľvek triede, ktorá z tejto abstraktnej triedy dedí. Pokiaľ túto metódu nepredefinujeme v triede potomka, Python vyhodí TypeError pri pokuse vytvoriť takú inštanciu:

class Zvire(ABC):
    def __init__(self, jmeno, vaha):
        self.jmeno = jmeno
        self.vaha = vaha

    @abstractmethod
    def pohybuj_se(self):
        pass

class Pes(Zvire):
    # pohybuj_se() - metoda není předefinována
    pass

try:
    p = Pes("Rex", 30)  # Toto vyhodí chybu, protože Pes nepředefinuje abstraktní metodu pohybuj_se()
except TypeError as e:
    print(e)

Vo výstupe uvidíme chybovú hlášku:

Chyba abstraktní metody:
Can't instantiate abstract class Pes with abstract method pohybuj_se

Toto je jeden z hlavných účelov abstraktných tried - nútenie konkrétnych tried na implementáciu určitých metód. To zaručuje, že všetky triedy dediace z danej abstraktnej triedy budú mať rovnakú sadu metód, hoci ich implementácia sa môže líšiť.

Abstraktné triedy a metódy sú kľúčovým konceptom v objektovo orientovanom programovaní a umožňujú nám vytvárať robustnejší a lepšie organizovaný kód.

Vytvorme teraz inštanciu triedy Delfin, ktorá metódy preťažuje:

from abc import ABC, abstractmethod

class Zvire(ABC):

    def __init__(self, jmeno, vaha):
        self.jmeno = jmeno
        self.vaha = vaha

    @abstractmethod
    def pohybuj_se(self):
        pass

class Delfin(Zvire):
    # Zde "přetěžujeme" metodu "pohybuj_se()" z abstraktní třídy "Zvire".
    # Přetěžování znamená, že v třídě potomka definujeme metodu se stejným jménem,
    # která přebírá funkcionalitu původní metody a může ji rozšířit nebo úplně nahradit.
    def pohybuj_se(self):
        print("Plavu...")

delfin = Delfin("Pavel", 1500)
delfin.pohybuj_se()

Výstup v konzole:

Korektní výstup:
Plavu...

Abstraktná trieda ako rozhranie (interface)

V niektorých programovacích jazykoch, ako je napríklad Java alebo C#, sa abstraktné triedy, ktoré majú iba abstraktné metódy, nazývajú rozhranie (alebo interface). Tieto jazyky umožňujú triede dediť z jednej triedy a implementovať viac rozhrania.

V Pythone však takýto explicitný koncept rozhrania neexistuje. Namiesto toho Python podporuje viacnásobnú dedičnosť, čo znamená , že trieda môže dediť z viac ako jednej triedy. Vďaka tomu môžeme v Pythone použiť abstraktné triedy podobným spôsobom ako rozhranie v iných jazykoch:

from abc import ABC, abstractmethod

class Letajici(ABC):
    @abstractmethod
    def letej(self):
        pass

class Plavajici(ABC):
    @abstractmethod
    def plav(self):
        pass

class Ptak(Letajici):
    def letej(self):
        print("Jsem pták a letím...")

class Delfin(Plavajici):
    def plav(self):
        print("Jsem delfín a plavu...")

class LetajiciDelfin(Delfin, Letajici):
    def letej(self):
        print("Jsem delfín a letím...")

ptak = Ptak()
delfin = LetajiciDelfin()

ptak.letej()
delfin.plav()
delfin.letej()

V konzole potom uvidíme:

Abstraktní třídy jako rozhraní:
Jsem pták a letím...
Jsem delfín a plavu...
Jsem delfín a letím...

V tomto príklade fungujú triedy Letajici a Plavajici ako rozhrania, ktoré definujú metódy letej() a plav(). Trieda Ptak implementuje rozhranie Letajici a trieda Delfin implementuje rozhranie Plavajici. Trieda LetajiciDelfin potom dedí z oboch týchto rozhraní a implementuje obe metódy.

Keď vytvoríme inštanciu triedy Ptak a LetajiciDelfin, obe tieto inštancie vedia volať metódu letej(). Inštancia triedy LetajiciDelfin vie volať aj metódu plav(), pretože dedí túto funkcionalitu z triedy Delfin.

Hoci Python nemá explicitný koncept rozhrania ako takých, tento prístup nám umožňuje využiť podobnú funkcionalitu a vytvárať kód, ktorý je dobre štruktúrovaný a ľahko rozšíriteľný.

Metód resolution order

Jednou z kľúčových vecí pri práci s abstraktnými triedami a preťažovaní metód je, že keď trieda dedí z viacerých tried a preťažuje metódu, je táto metóda preddefinovaná v súlade s poslednou triedou v zozname rodičovských tried. To je dôvod, prečo v našom príklade inštancie triedy LetajiciDelfin používa metódu letej() definovanú priamo v triede LetajiciDelfin, a nie tú, ktorá je definovaná v triede Ptak.

Tento koncept sa nazýva Method Resolution Order (MRO), alebo aj "riešenie poradia metód" vo výklade do slovenčiny. MRO v Pythone určuje poradie, v akom sa prehľadávajú rodičovské triedy pri hľadaní metódy, keď je táto metóda volaná na inštanciu triedy.

Predstavme si napríklad, že máme tri triedy - A, B a C - kde B dedí od A a C dedí od B. Ak by sme chceli vytvoriť objekt z triedy C a volať metódu, ktorú definujú všetky tri triedy, MRO určí, ktorá z týchto metód bude volaná.

V Pythone je MRO určený pravidlom zvaným C3 Linearization, alebo tiež "Lelouch's rule". Toto pravidlo určuje jednoznačné poradie tried v hierarchii dedenia, ktoré umožňuje Pythonu konzistentne a predvídateľne určovať, akú metódu použiť v prípade, že existuje viac možných implementácií rovnakej metódy v rôznych rodičovských triedach.

Poradie MRO

Ak chceme zistiť poradie MRO pre konkrétnu triedu v Pythone, použijeme vstavanú metódu mro():

class Prvni:
    def test(self):
        print("První")

class Druha:
    def test(self):
        print("Druhá")

class Treti(Druha, Prvni):
    pass

print(Treti.mro()) #  Zjišťujeme MRO pro třídu Treti

V konzole potom uvidíme nasledujúci výstup:

Výstup MRO:
[<class '__main__.Treti'>, <class '__main__.Druha'>, <class '__main__.Prvni'>, <class 'object'>]

Ako vidíme, Python vyhľadá metódy v triede Treti, potom v triede Druha a potom v triede Prvni. To znamená, že ak je metóda test() volaná na inštanciu triedy Treti, bude najprv hľadaná v triede Treti, potom v triede Druha a nakoniec v triede Prvni:

treti = Treti()
treti.test()

Vo výstupe uvidíme:

Výstup MRO:
Druhá

To je pre dnešnú lekciu všetko.

V budúcej lekcii, Najčastejšie chyby Python nováčikov - Vieš pomenovať objekty? , si ukážeme najčastejšie chyby začiatočníkov v Pythone ohľadom pomenovania tried, metód a atribútov.


 

Predchádzajúci článok
Riešené úlohy k 18.-21. lekciu OOP v Pythone
Všetky články v sekcii
Objektovo orientované programovanie v Pythone
Preskočiť článok
(neodporúčame)
Najčastejšie chyby Python nováčikov - Vieš pomenovať objekty?
Článok pre vás napísal Martin Macura
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
Aktivity