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áli 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
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.
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 Admin
tak, aby z triedy User
dedila. Atribúty a metódy užívateľa teda už nemusíme
znovu definovať, Python ich sám do triedy dodá:
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.
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 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 jediný atribút v materskej triede, táto zmena se 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
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 obvykle 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 ď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
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