Lekce 11 - Dědičnost a polymorfismus v Pythonu
V minulé lekci, Aréna s bojovníky v Pythonu, jsme dokončili naši arénu simulující zápas dvou bojovníků.
V následujícím tutoriálu si opět rozšíříme znalosti o objektově orientovaném programování v Pythonu. V úvodní lekci do OOP jsme si říkali, že OOP stojí na třech základních pilířích: zapouzdření, dědičnosti a polymorfismu. Zapouzdření a používání podtržítek už dobře známe. Dnes se podíváme na zbylé dva pilíře.
Dědičnost
Dědičnost je jedna ze základních vlastností OOP a slouží k tvoření nových datových struktur na základě starých. Vysvětleme si to na jednoduchém příkladu:
Budeme programovat informační systém. To je docela reálný příklad.
Abychom si však učení zpříjemnili, bude to informační systém pro správu
zvířat v ZOO
Náš systém
budou používat dva typy uživatelů: uživatel a administrátor. Uživatel je
běžný ošetřovatel zvířat, který bude moci upravovat informace o
zvířatech, např. jejich váhu nebo rozpětí křídel. Administrátor bude
moci také upravovat údaje o zvířatech a navíc zvířata přidávat a mazat
z databáze. Z atributů bude mít navíc telefonní číslo, aby ho bylo
možné kontaktovat v případě výpadku systému. Bylo by jistě zbytečné a
nepřehledné, kdybychom si museli definovat obě třídy úplně celé,
protože mnoho vlastností těchto dvou objektů je společných. Uživatel i
administrátor budou mít jistě jméno, věk a budou se moci přihlásit a
odhlásit. Nadefinujeme si tedy pouze třídu Uzivatel (nepůjde o
funkční ukázku, dnes to bude jen teorie, programovat budeme příště):
class Uzivatel: def __init__(self, jmeno, heslo, vek): self.__jmeno = jmeno self.__heslo = heslo self.__vek = vek def prihlasit(self, heslo): ... def odhlasit(self): ... def nastav_vahu(self, zvire): ... ...
Třídu jsme si jen naznačili, ale jistě si ji dokážeme dobře
představit. Bez znalosti dědičnosti bychom třídu Administrator
definovali takto:
class Administrator: def __init__(self, jmeno, heslo, vek, telefonni_cislo): self.__jmeno = jmeno self.__heslo = heslo self.__vek = vek self.__telefonni_cislo = telefonni_cislo def prihlasit(self, heslo): ... def odhlasit(self): ... def nastav_vahu(self, zvire): ... def pridej_zvire(self, zvire): ... def vymaz_zvire(self, zvire): ... ...
Vidíme, že máme ve třídě spoustu redundantního (duplikovaného) kódu.
Jakékoli změny musíme nyní provádět v obou třídách, kód se nám velmi
komplikuje. Řešením tohoto problému je dědičnost.
Definujeme třídu Administrator tak, aby z třídy
Uzivatel dědila. Atributy a metody uživatele tedy již
nemusíme znovu definovat, Python je sám do třídy dodá:
class Administrator(Uzivatel): def __init__(self, jmeno, heslo, vek, telefonni_cislo): super().__init__(jmeno, heslo, vek) self.__telefonni_cislo = telefonni_cislo def pridej_zvire(self, zvire): ... def vymaz_zvire(self, zvire): ... ...
Vidíme, že ke zdědění používáme závorky. Mezi závorky píšeme
třídy, od kterých naše třída dědí. Syntaxe je tedy
class TridaPotomka(TridaRodice):. V anglické literatuře se
dědičnost označuje slovem inheritance.
Toho "podivného" super() si zatím nebudeme všímat - bude
vysvětleno později (ale je nutné, pokud chceme použít metodu rodiče).
Vraťme se zpět k příkladu.
V potomkovi nebudou přístupné privátní atributy rodiče (označené dvojitým podtržítkem). Přístupné budou pouze veřejné atributy a metody. Privátní atributy a metody jsou chápány jako speciální logika konkrétní třídy, která je potomkovi utajena. I když ji vlastně používá, nemůže ji měnit (s výjimkou přes name mangling, který jsme si vysvětlili v lekci Zapouzdření atributů podrobně).
Říkali jsme si, že přístup k privátním atributům tímto způsobem není považován za dobrou praxi, protože porušuje princip zapouzdření a může vést k nepředvídaným chybám nebo komplikacím. Neuškodí to zopakovat.
Abychom zpřístupnili vybrané atributy rodiče i jeho potomkovi, použijeme
jako modifikátor přístupu jedno podtržítko. V Pythonu se
atributy a metody s jedním podtržítkem nazývají
vnitřní. My už víme, že pro ostatní programátory nebo
objekty to znamená: "Toto je sice zvenčí viditelné, ale, prosím, neměňte
mi to!" Začátek třídy Uzivatel tedy bude vypadat takto:
class Uzivatel: def __init__(self, jmeno, heslo, vek): self._jmeno = jmeno self._heslo = heslo self._vek = vek
Když si nyní vytvoříme instance uživatele a administrátora, oba budou
mít např. atribut jmeno a metodu prihlasit(). Python
třídu Uzivatel zdědí a automaticky nám doplní všechny její
atributy.
Výhody dědění jsou jasné. Nemusíme opisovat oběma třídám ty samé atributy. Stačí dopsat jen to, v čem se liší. Zbytek se podědí. Přínos je obrovský, můžeme rozšiřovat existující komponenty o nové metody a tím je znovu využívat. Nemusíme psát spousty redundantního (duplikovaného) kódu. A hlavně - když změníme jediný atribut v mateřské třídě, tato změna všude automaticky podědí. Nedojde tedy k tomu, že bychom to museli měnit ručně u dvaceti tříd a někde na to zapomněli a způsobili chybu. Jsme lidé a chybovat budeme vždy, musíme tedy používat takové programátorské postupy, abychom měli možností chybovat co nejméně.
O mateřské třídě se obvykle hovoří jako o předkovi
(zde Uzivatel) a o třídě, která z ní dědí, jako o
potomkovi (zde Administrator). Potomek může
přidávat nové metody nebo si uzpůsobovat metody z mateřské třídy (viz
dále).
Kromě uvedeného názvosloví se často setkáme i s pojmy nadtřída a podtřída.
Další možností, jak objektový model navrhnout, je
zavést mateřskou třídu Uzivatel, která by sloužila pouze k
dědění. Z třídy Uzivatel bychom potom dědili třídu
Osetrovatel a z ní třídu Administrator. Taková
struktura se však vyplatí až při větším počtu typů uživatelů.
Hovoříme zde o hierarchii tříd. Náš příklad byl
jednoduchý a proto nám stačily pouze dvě třídy. Existují tzv.
návrhové vzory, které obsahují osvědčená schémata
objektových struktur pro známé případy užití. Máme je popsané v sekci
Návrhové vzory, je
to však již pokročilejší problematika a také velmi zajímavá. V
objektovém modelování se dědičnost znázorňuje graficky jako prázdná
šipka směřující k předkovi. V našem případě grafická notace vypadá
takto:

Jazyky, které dědičnost podporují, buď umí dědičnost jednoduchou, kde třída dědí jen z jedné třídy, nebo vícenásobnou, kde třída dědí hned z několika tříd najednou. Python podporuje vícenásobnou dědičnost jako např. C++.
Všechny objekty v Pythonu dědí ze třídy
object.
Testování typu třídy
Testování, zda je objekt instancí určité třídy, je v Pythonu užitečné z několika důvodů:
- typová kontrola: Zejména v dynamicky typovaných jazycích jako je Python je často důležité zkontrolovat, zda jsou proměnná nebo objekt určitého typu (třídy), než s nimi provádíme další operace. Pomůže nám to zabránit chybám v kódu.
- přizpůsobení chování: Testování typu nám umožňuje, aby náš kód reagoval jinak na základě toho, jakého typu je objekt. Například máme funkci, která přijímá různé třídy objektů a každý z nich má být zpracován trochu jinak. Zde využijeme kontrolu typu k rozhodnutí, jaký kód bude vykonán.
Testování pomocí funkce
type()
Funkce type() se v Pythonu běžně používá k získání
přímého typu objektu. Výsledek této funkce je obvykle užitečný pro
jednoduché datové typy:
if type(x) == list: print("x je seznam") if type(y) == str: print("y je řetězec") a = 10 print(type(a) == int) # Výsledek: True y = "Hello" print(type(y) == int)
Ve výstupu konzole uvidíme:
Výstup funkce type():
x je seznam
y je řetězec
True
False
Testování pomocí funkce
isinstance()
Preferovaným způsobem ověření, zda je objekt instancí určité třídy
nebo některého z jejích potomků, je však vestavěná funkce
isinstance().
Důvodem je to, že funkce isinstance() bere v úvahu
dědičnost, zatímco funkce type() to nedělá. Pokud máme
třídu, která dědí z jiné třídy, type() nám vrátí pouze
přesnou třídu objektu, zatímco isinstance() potvrdí, zda je
objekt instancí některé třídy v hierarchii dědičnosti. Podívejme se na
příklad:
class Rodic: pass class Potomek(Rodic): pass karel = Potomek() print(type(karel) == Rodic) print(isinstance(karel, Rodic))
Ve výstupu konzole uvidíme:
Výstup funkce isinstance():
False
True
V tomto příkladu je karel instancí třídy
Potomek, ale díky dědičnosti je také instancí třídy
Rodic. Funkce isinstance() to správně rozpozná,
zatímco type() vrátí pouze konkrétní třídu potomka, nikoli
třídu rodiče nebo jakoukoli jinou třídu v hierarchii dědičnosti.
Proto je pro komplexnější objekty a práci s třídami
vhodnější použít funkci isinstance().
Polymorfismus
Nenechme se vystrašit příšerným názvem této techniky, protože je v
jádru velmi jednoduchá. Polymorfismus umožňuje používat jednotné
rozhraní pro práci s různými typy objektů. Mějme například mnoho
objektů, které reprezentují nějaké geometrické útvary (kruh, čtverec,
trojúhelník). Bylo by jistě přínosné a přehledné, kdybychom s nimi mohli
komunikovat jednotně, ačkoli se liší. Mějme například třídu
GeometrickyUtvar, která obsahuje atribut barva a
metodu vykresli(). Všechny geometrické tvary potom budou z této
třídy dědit její interface (rozhraní). Objekty kruh a
ctverec se ale jistě vykreslují každý jinak.
Polymorfismus nám proto umožňuje přepsat si metodu
vykresli() u každé podtřídy tak, aby dělala, co
chceme. Rozhraní tak zůstane zachováno a my nebudeme muset
přemýšlet, jak se to u onoho objektu volá.
Polymorfismus bývá často vysvětlován na obrázku se zvířaty, která
mají všechna v rozhraní metodu speak(), ale každé si ji
vykonává po svém:

Podstatou polymorfismu je tedy metoda nebo metody, které mají všichni potomci definované se stejnou hlavičkou, ale jiným tělem. Detailně se touto problematikou budeme zabývat v lekci Abstraktní třídy v Pythonu.
V další lekci, Aréna s mágem (dědičnost a polymorfismus), si polymorfismus spolu s dědičností vyzkoušíme
na bojovnících v naší aréně. Přidáme mága, který si bude metodu
utoc() vykonávat po svém pomocí many, ale jinak zdědí
chování a atributy bojovníka. Zvenčí tedy vůbec nepoznáme, že to není
bojovník, protože bude mít stejné rozhraní. Bude to zábava 

