10. diel - Dedičnosť a polymorfizmus v Pythone
V minulej lekcii, Aréna s bojovníkmi v Pythone , sme dokončili našu arénu simulujúcu zápas dvoch bojovníkov.
V nasledujúcom tutoriále si opäť rozšírime znalosti o objektovo orientovanom programovaní v Pythone. V úvodnej lekcii do OOP sme si hovorili, že OOP stojí na troch základných pilieroch: zapuzdrenie, dedičnosti a polymorfizmu. Zapúzdrenie a používanie podčiarkovníkov už dobre poznáme. Dnes sa pozrieme na zvyšné dva piliere.
Dedičnosť
Dedičnosť je jedna zo základných vlastností OOP a slúži na tvorenie nových dátových štruktúr na základe starých. Vysvetlime si to na jednoduchom príklade:
Budeme programovať informačný systém. To je celkom reálny príklad. Aby
sme si však učenie spríjemnili, bude to informačný systém pre správu
zvierat v ZOO:-) Náš systém budú používať dva typy užívateľov:
používateľ a administrátor. Užívateľ je bežný ošetrovateľ zvierat,
ktorý bude môcť upravovať informácie o zvieratách, napr. ich váhu alebo
rozpätie krídiel. Administrátor bude môcť tiež upravovať údaje o
zvieratách a navyše zvieratá pridávať a mazať z databázy. Z atribútov
bude mať navyše telefónne číslo, aby ho bolo možné kontaktovať v
prípade výpadku systému. Bolo by určite zbytočné a neprehľadné, keby sme
si museli definovať obe triedy úplne celé, pretože mnoho vlastností týchto
dvoch objektov je spoločných. Užívateľ aj administrátor budú mať určite
meno, vek a budú sa môcť prihlásiť a odhlásiť. Nadefinujeme si teda iba
triedu Uzivatel
(nepôjde o funkčnú ukážku, dnes to bude len
teória, programovať budeme nabudúce):
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): ... ...
Triedu sme si len naznačili, ale určite si ju dokážeme dobre predstaviť.
Bez znalosti dedičnosti by sme triedu 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 v triede množstvo redundantného (duplikovaného) kódu.
Akékoľvek zmeny musíme teraz vykonávať v oboch triedach, kód sa nám
veľmi komplikuje. Riešením tohto problému je dedičnosť.
Definujeme triedu Administrator
tak, aby z triedy
Uzivatel
dedila. Atribúty a metódy užívateľa teda už
nemusíme znovu definovať, Python ich sám do triedy
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 na zdedenie používame zátvorky. Medzi zátvorkami píšeme
triedy, od ktorých naša trieda dedí. Syntax je teda
class TridaPotomka(TridaRodice):
. V anglickej literatúre sa
dedičnosť označuje slovom inheritance.
Toho "podivného" super()
si zatiaľ nebudeme všímať - bude
vysvetlené neskôr (ale je nutné, ak chceme použiť metódu rodiča).
Vráťme sa späť k príkladu.
V potomkovi nebudú prístupné privátne atribúty rodiča (označené dvojitým podčiarkovníkom). Prístupné budú iba verejné atribúty a metódy. Privátne atribúty a metódy sú chápané ako špeciálna logika konkrétnej triedy, ktorá je potomkovi utajená. Aj keď ju vlastne používa, nemôže ju meniť (s výnimkou cez name mangling, ktorý sme si vysvetlili v lekcii Zapúzdrenie atribútov podrobne).
Hovorili sme si, že prístup k privátnym atribútom týmto spôsobom nie je považovaný za dobrú prax, pretože porušuje princíp zapuzdrenia a môže viesť k nepredvídaným chybám alebo komplikáciám. Neuškodí to zopakovať.
Aby sme sprístupnili vybrané atribúty rodiča aj jeho potomkovi,
použijeme ako modifikátor prístupu jedno podčiarknutie. V
Pythone sa atribúty a metódy s jedným podčiarkovníkom
nazývajú vnútorné. My už vieme, že pre ostatných
programátorov alebo objekty to znamená: "Toto je síce zvonku viditeľné,
ale, prosím, nemeňte mi to!" Začiatok triedy Uzivatel
teda bude
vyzerať takto:
class Uzivatel: def __init__(self, jmeno, heslo, vek): self._jmeno = jmeno self._heslo = heslo self._vek = vek
Keď si teraz vytvoríme inštancie užívateľa a administrátora, obaja
budú mať napr. atribút jmeno
a metódu prihlasit()
.
Python triedu Uzivatel
zdedí a automaticky nám doplní všetky
jej atribúty.
Výhody dedenia sú jasné. Nemusíme opisovať obom triedam tie isté atribúty. Stačí dopísať len to, v čom sa líšia. Zvyšok sa zdedí. Prínos je obrovský, môžeme rozširovať existujúce komponenty o nové metódy a tým ich znovu využívať. Nemusíme písať množstvo redundantného (duplikovaného) kódu. A hlavne - keď zmeníme jediný atribút v materskej triede, táto zmena všade automaticky zdedí. Nedôjde teda k tomu, že by sme to museli meniť ručne v dvadsiatich triedach a niekde na to zabudli a spôsobili chybu. Sme ľudia a chybovať budeme vždy, musíme teda používať také programátorské postupy, aby sme mali možnosť chybovať čo najmenej.
O materskej triede sa obvykle hovorí ako o predkovi (tu
Uzivatel
) ao triede, ktorá z nej dedí, ako o
potomkovi (tu Administrator
). Potomok môže
pridávať nové metódy alebo si prispôsobovať metódy z materskej triedy
(pozri ďalej).
Okrem uvedeného názvoslovia sa často stretneme aj s pojmami nadtrieda a podtrieda.
Ďalšou možnosťou, ako objektový model navrhnúť, je
zaviesť materskú triedu Uzivatel
, ktorá by slúžila iba na
dedenie. Z triedy Uzivatel
by sme potom dedili triedu
Osetrovatel
az nej triedu Administrator
. Takáto
štruktúra sa však oplatí až pri väčšom počte typov užívateľov.
Hovoríme tu o hierarchii tried. Náš príklad bol jednoduchý
a preto nám stačili iba dve triedy. Existujú tzv. návrhové
vzory, ktoré obsahujú osvedčené schémy objektových štruktúr pre
známe prípady použitia. Máme ich popísané v sekcii Návrhové vzory, je to
však už pokročilejšia problematika a tiež veľmi zaujímavá. V objektovom
modelovaní sa dedičnosť znázorňuje graficky ako prázdna šípka smerujúca
k predkovi. V našom prípade grafická notácia vyzerá takto:
Jazyky, ktoré dedičnosť podporujú, buď vedia dedičnosť jednoduchú, kde trieda dedí len z jednej triedy, alebo viacnásobnú, kde trieda dedí hneď z niekoľkých tried naraz. Python podporuje viacnásobnú dedičnosť ako napr. C++.
Všetky objekty v Pythone dedia z triedy
object
.
Testovanie typu triedy
Testovanie, či je objekt inštancií určitej triedy, je v Pythone užitočné z niekoľkých dôvodov:
- typová kontrola: Najmä v dynamicky typovaných jazykoch ako je Python je často dôležité skontrolovať, či sú premenné alebo objekt určitého typu (triedy), než s nimi vykonávame ďalšie operácie. Pomôže nám to zabrániť chybám v kóde.
- prispôsobenie správania: Testovanie typu nám umožňuje, aby náš kód reagoval inak na základe toho, akého typu je objekt. Napríklad máme funkciu, ktorá prijíma rôzne triedy objektov a každý z nich má byť spracovaný trochu inak. Tu využijeme kontrolu typu na rozhodnutie, aký kód bude vykonaný.
type()
Funkcia type()
sa v Pythone bežne používa na získanie
priameho typu objektu. Výsledok tejto funkcie je obvykle užitočný pre
jednoduché dátové 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)
Vo výstupe konzoly uvidíme:
Výstup funkce type():
x je seznam
y je řetězec
True
False
Testovanie pomocou funkcie
isinstance()
Preferovaným spôsobom overenia, či je objekt inštanciou určitej triedy
alebo niektorého z jej potomkov, je však vstavaná funkcia
isinstance()
.
Dôvodom je to, že funkcia isinstance()
berie do úvahy
dedičnosť, zatiaľ čo funkcia type()
to nerobí. Ak máme
triedu, ktorá dedí z inej triedy, type()
nám vráti iba presnú
triedu objektu, zatiaľ čo isinstance()
potvrdí, či je objekt
inštanciou niektorej triedy v hierarchii dedičnosti. Pozrime sa na
príklad:
class Rodic: pass class Potomek(Rodic): pass karel = Potomek() print(type(karel) == Rodic) print(isinstance(karel, Rodic))
Vo výstupe konzoly uvidíme:
Výstup funkce isinstance():
False
True
V tomto príklade je karel
inštanciou triedy
Potomek
, ale vďaka dedičnosti je aj inštanciou triedy
Rodic
. Funkcia isinstance()
to správne rozpozná,
zatiaľ čo type()
vráti iba konkrétnu triedu potomka, nie triedu
rodiča alebo akúkoľvek inú triedu v hierarchii dedičnosti.
Preto je pre komplexnejšie objekty a prácu s triedami
vhodnejšie použiť funkciu isinstance()
.
Polymorfizmus
Nenechajme sa vystrašiť príšerným názvom tejto techniky, pretože je v
jadre veľmi jednoduchá. Polymorfizmus umožňuje používať jednotné
rozhranie na prácu s rôznymi typmi objektov. Majme napríklad mnoho objektov,
ktoré reprezentujú nejaké geometrické útvary (kruh, štvorec,
trojuholník). Bolo by určite prínosné a prehľadné, keby sme s nimi mohli
komunikovať jednotne, hoci sa líšia. Majme napríklad triedu
GeometrickyUtvar
, ktorá obsahuje atribút barva
a
metódu vykresli()
. Všetky geometrické tvary potom budú z tejto
triedy dediť jej interface (rozhranie). Objekty kruh
a
ctverec
sa ale iste vykresľujú každý inak.
Polymorfizmus nám preto umožňuje prepísať si metódu
vykresli()
u každej podtriedy tak, aby robila, čo
chceme. Rozhranie tak zostane zachované a my nebudeme musieť
premýšľať, ako sa to pri onom objekte volá.
Polymorfizmus býva často vysvetľovaný na obrázku so zvieratami, ktoré
majú všetky v rozhraní metódu speak()
, ale každé si ju
vykonáva po svojom:
Podstatou polymorfizmu je teda metóda alebo metódy, ktoré majú všetci potomkovia definované s rovnakou hlavičkou, ale iným telom. Detailne sa touto problematikou budeme zaoberať v lekcii Abstraktnej triedy v Pythone.
V ďalšej lekcii, Aréna s mágom (dedičnosť a polymorfizmus) , si polymorfizmus spolu s dedičnosťou
vyskúšame na bojovníkoch v našej aréne. Pridáme mága, ktorý si bude
metódu utoc()
vykonávať po svojom pomocou many, ale inak zdedí
správanie a atribúty bojovníka. Zvonku teda vôbec nepoznáme, že to nie je
bojovník, pretože bude mať rovnaké rozhranie. Bude to zábava:-)