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
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:
{PYTHON}
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)
{/PYTHON}
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:
{PYTHON}
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 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)
{/PYTHON}
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:
{PYTHON}
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()
{/PYTHON}
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ů.