Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 22 - Abstraktní třídy v Pythonu

V předešlém cvičení, Řešené úlohy k 18.-21. lekci OOP v Pythonu, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

V následujícím tutoriálu objektového programování v Pythonu se budeme zabývat abstraktními třídami. Vysvětlíme si jejich účel i praktické využití.

Abstraktní třídy v Pythonu

Řekněme, že děláme například aplikaci ZOO, kde máme několik různých zvířat. Každé zvíře má nějaké jméno, váhu a má metodu na pohyb. Jelikož budou tyto atributy i metoda na všech zvířatech, nabízí se připravit společnou třídu předka.

Kód by pak vypadal napří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ává v tuto chvíli smysl vytvořit instanci třídy Zvire? Ani moc ne, není vůbec jasné, co by se mělo stát, když zavoláme metodu na pohyb. Ještě horší by to bylo s metodou, která má něco vracet.

Takové třídě, jako je naše Zvire, se říká abstraktní třída. Je to třída, u které nemá smysl vytvořit instanci, protože sama nemá definované nějaké chování. To má definovat až její potomek. Je to z toho důvodu, že je obecná. Zvíře bude ovšem vždy konkrétní (tedy nějaký potomek, např. Delfin). Nikdy nebudeme chtít, ani potřebovat, vytvořit instanci třídy Zvire. Chceme také donutit potomky této třídy, aby si metodu pro pohyb implementovali po svém – například hroši, až na vzácné výjimky dané spíše okolnostmi, nedokáží létat :-D

Abstraktní třída je třída, která obsahuje alespoň jednu abstraktní metodu. Abstraktní metoda je metoda, která má deklaraci, ale neobsahuje implementaci.

Tvorba abstraktní třídy

Abstraktní třídu vytvoříme tak, že podědíme třídu ABC (zkratka z Abstract Base Class). Třída ABC se nachází v modulu abc. K označení metody jako "abstraktní" v Pythonu slouží dekorátor @abstractmethod. Použitím dekorátoru zajistíme, že tato metoda musí být předefinována (nebo, jak se často říká, "překryta") v jakékoliv konkrétní (neabstraktní) podtřídě, která dědí z abstraktní třídy.

Následující kód spadne, protože se v něm pokoušíme vytvořit instanci obecného zvířete:

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)

Ve výstupu uvidíme chybovou hlášku:

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

Abstraktní metoda

Jak už jsme si řekli, abstraktní metoda sice má deklaraci, ale neobsahuje implementaci. Každou metodu, kterou v abstraktní třídě označíme dekorátorem @abstractmethod, musíme předefinovat v jakékoliv třídě, která z této abstraktní třídy dědí. Pokud tuto metodu nepředefinujeme ve třídě potomka, Python vyhodí TypeError při pokusu vytvořit takovou instanci:

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)

Ve výstupu uvidíme chybovou hlášku:

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

Toto je jeden z hlavních účelů abstraktních tříd - nucení konkrétních tříd k implementaci určitých metod. To zaručuje, že všechny třídy dědící z dané abstraktní třídy budou mít stejnou sadu metod, ačkoli jejich implementace se může lišit.

Abstraktní třídy a metody jsou klíčovým konceptem v objektově orientovaném programování a umožňují nám vytvářet robustnější a lépe organizovaný kód.

Vytvořme nyní instanci třídy Delfin, která metody přetěž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 konzoli:

Korektní výstup:
Plavu...

Abstraktní třída jako rozhraní (interface)

V některých programovacích jazycích, jako je například Java nebo C#, se abstraktní třídy, které mají pouze abstraktní metody, nazývají rozhraní (nebo interface). Tyto jazyky umožňují třídě dědit z jedné třídy a implementovat více rozhraní.

V Pythonu však takový explicitní koncept rozhraní neexistuje. Místo toho Python podporuje vícenásobnou dědičnost, což znamená, že třída může dědit z více než jedné třídy. Díky tomu můžeme v Pythonu použít abstraktní třídy podobným způsobem jako rozhraní v jiných jazycích:

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 konzoli pak 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 příkladu fungují třídy Letajici a Plavajici jako rozhraní, které definují metody letej() a plav(). Třída Ptak implementuje rozhraní Letajici a třída Delfin implementuje rozhraní Plavajici. Třída LetajiciDelfin pak dědí z obou těchto rozhraní a implementuje obě metody.

Když vytvoříme instanci třídy Ptak a LetajiciDelfin, obě tyto instance umí volat metodu letej(). Instance třídy LetajiciDelfin umí volat také metodu plav(), jelikož dědí tuto funkcionalitu z třídy Delfin.

Ačkoli Python nemá explicitní koncept rozhraní jako takových, tento přístup nám umožňuje využít podobnou funkcionalitu a vytvářet kód, který je dobře strukturovaný a snadno rozšiřitelný.

Method resolution order

Jednou z klíčových věcí při práci s abstraktními třídami a přetěžování metod je, že když třída dědí z více tříd a přetěžuje metodu, je tato metoda předdefinována v souladu s poslední třídou v seznamu rodičovských tříd. To je důvod, proč v našem příkladu instance třídy LetajiciDelfin používá metodu letej() definovanou přímo v třídě LetajiciDelfin, a ne tu, která je definovaná v třídě Ptak.

Tento koncept se nazývá Method Resolution Order (MRO), nebo také "řešení pořadí metod" ve výkladu do češtiny. MRO v Pythonu určuje pořadí, v jakém se prohledávají rodičovské třídy při hledání metody, když je tato metoda volána na instanci třídy.

Představme si například, že máme tři třídy - A, B a C - kde B dědí od A a C dědí od B. Pokud bychom chtěli vytvořit objekt z třídy C a volat metodu, kterou definují všechny tři třídy, MRO určí, která z těchto metod bude volaná.

V Pythonu je MRO určený pravidlem zvaným C3 Linearization, nebo také "Lelouch's rule". Toto pravidlo určuje jednoznačné pořadí tříd v hierarchii dědění, které umožňuje Pythonu konzistentně a předvídatelně určovat, jakou metodu použít v případě, že existuje více možných implementací stejné metody v různých rodičovských třídách.

Pořadí MRO

Pokud chceme zjistit pořadí MRO pro konkrétní třídu v Pythonu, použijeme vestavěnou metodu 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 konzoli pak uvidíme následující výstup:

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

Jak vidíme, Python vyhledá metody v třídě Treti, pak v třídě Druha a poté v třídě Prvni. To znamená, že pokud je metoda test() volána na instanci třídy Treti, bude nejprve hledána v třídě Treti, pak v třídě Druha a nakonec v třídě Prvni:

treti = Treti()
treti.test()

Ve výstupu uvidíme:

Výstup MRO:
Druhá

To je pro dnešní lekci vše.

V příští lekci, Nejčastější chyby Python nováčků - Umíš pojmenovat objekty?, si ukážeme nejčastější chyby začátečníků v Pythonu ohledně pojmenování tříd, metod a atributů.


 

Předchozí článek
Řešené úlohy k 18.-21. lekci OOP v Pythonu
Všechny články v sekci
Objektově orientované programování v Pythonu
Přeskočit článek
(nedoporučujeme)
Nejčastější chyby Python nováčků - Umíš pojmenovat objekty?
Článek pro vás napsal Martin Macura
Avatar
Uživatelské hodnocení:
153 hlasů
Aktivity