11. diel - Dedičnosť a polymorfizmus v Pythone
V predchádzajúcej 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áli si opäť rozšírime znalosti o objektovo orientovanom programovaní v Pythone. V úvodnej lekcii OOP sme si hovorili, že OOP stojí na troch základných pilieroch: zapuzdrení, dedičnosti a polymorfizme. Zapuzdrenie 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 tvorbu 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 na 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
User (nepôjde o funkčnú ukážku, dnes to bude len teória,
programovať budeme nabudúce):
class User: def __init__(self, name, password, age): self.__name = name self.__password = password self.__age = age def log_in(self, password): ... def log_out(self): ... def set_weight(self, animal): ... ...
Triedu sme si len naznačili, ale určite si ju dokážeme dobre predstaviť.
Bez znalosti dedičnosti by sme triedu Admin definovali takto:
class Admin: def __init__(self, name, password, age, phone_number): self.__name = name self.__password = password self.__age = age self.__phone_number = phone_number def log_in(self, password): ... def log_out(self): ... def set_weight(self, animal): ... def add_animal(self, animal): ... def remove_animal(self, animal): ... ...
Vidíme, že máme v triede množstvo redundantného (duplikovaného) kódu.
Všetky zmeny teraz musíme vykonávať v oboch triedach, kód sa nám veľmi
komplikuje. Riešením tohto problému je dedičnosť.
Definujeme triedu Admin tak, aby z triedy User
dedila. Atribúty a metódy užívateľa teda už nemusíme
znovu definovať, Python ich do triedy sám pridá:
class Admin(User): def __init__(self, name, password, age, phone_number): super().__init__(name, password, age) self.__phone_number = phone_number def add_animal(self, animal): ... def remove_animal(self, animal): ... ...
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 ChildClass(ParentClass):. V anglickej literatúre sa
dedičnosť označuje slovom inheritance.
Ten "podivný" 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 Zapuzdrenie 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 User teda bude
vyzerať takto:
class User: def __init__(self, name, password, age): self._name = name self._password = password self._age = age
Keď si teraz vytvoríme inštancie užívateľa a administrátora, obaja
budú mať napr. atribút name a metódu log_in().
Python triedu User 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 jeden atribút v materskej triede, táto zmena sa 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 chyby budeme robiť vždy, musíme teda používať také programátorské postupy, aby sme mali možnosť robiť ich čo najmenej.
O materskej triede sa obvykle hovorí ako o predkovi (tu
User) a o triede, ktorá z nej dedí, ako o
potomkovi (tu Admin). 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 User, ktorá by slúžila iba na dedenie.
Z triedy User by sme potom dedili triedu Attendant a z
nej triedu Admin. 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ý.
Testovanie pomocou funkcie
type()
Funkcia type() sa v Pythone bežne používa na získanie
priameho typu objektu. Výsledok tejto funkcie je zvyčajne užitočný pre
jednoduché dátové typy:
if type(x) == list: print("x is a list") if type(y) == str: print("y is a string") a = 10 print(type(a) == int) # Result: True y = "Hello" print(type(y) == int)
Vo výstupe konzoly uvidíme:
Output of the type() function:
x is a list
y is a string
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 Parent: pass class Child(Parent): pass carl = Child() print(type(carl) == Parent) print(isinstance(carl, Parent))
Vo výstupe konzoly uvidíme:
Output of the isinstance() function:
False
True
V tomto príklade je carl inštanciou triedy Child,
ale vďaka dedičnosti je aj inštanciou triedy Parent. 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
GeometricShape, ktorá obsahuje atribút color a
metódu render(). Všetky geometrické tvary potom budú z tejto
triedy dediť jej interface (rozhranie). Objekty circle a
square sa ale iste vykresľujú každý inak. Polymorfizmus
nám preto umožňuje prepísať si metódu render() 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 nasledujúcej 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 attack() 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 
