Lekce 10 - 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