Lekce 11 - Aréna s mágem (dědičnost a polymorfismus)
V minulé lekci, Dědičnost a polymorfismus v Pythonu, jsme si vysvětlili dědičnost a polymorfismus.
V dnešním tutoriálu objektově orientovaného programování v
Pythonu si vyzkoušíme dědičnost a
polymorfismus v praxi. Bude to opět na naší hře Tahový
boj, kde z bojovníka podědíme mága. Tento tutoriál již patří k těm
náročnějším a bude tomu tak i u dalších. Proto si průběžně
procvičujte práci s objekty, zkoušejte si naše cvičení a také
vymýšlejte své vlastní aplikace, abyste si zažili základní věci. To, že
je tu přítomen celý seriál neznamená, že ho celý najednou přečtete a
pochopíte Snažte se
programovat průběžně.

Než začneme něco psát, shodněme se na tom, co by měl mág umět. Mág bude fungovat stejně jako bojovník. Kromě života bude mít však i manu. Zpočátku bude mana plná. V případě plné many může mág vykonat magický útok, který bude mít pravděpodobně vyšší poškození, než útok normální (ale samozřejmě záleží na tom, jak si ho nastavíme). Tento útok manu vybije na 0. Každé kolo se bude mana zvyšovat o 10 a mág bude podnikat jen běžný útok. Jakmile se mana zcela doplní, opět bude moci magický útok použít. Mana bude zobrazena grafickým ukazatelem, stejně jako život.
Do původního projektu TahovyBoj
vytvoříme tedy třídu
Mag
, kterou zdědíme z třídy Bojovnik
. Opět si pro
ni založíme samostatný soubor a nazveme ho mag.py
. Obsah souboru
bude zatím vypadat takto (třídu si opět okomentujte):
from bojovnik import Bojovnik class Mag(Bojovnik):
Konstruktor potomka
Původní konstruktor bojovníka použít nemůžeme, neboť chceme mít u mága dva parametry navíc (manu a magický útok).
Definujeme si tedy konstruktor v potomkovi, který bere parametry potřebné pro vytvoření bojovníka a několik parametrů navíc pro mága.
U potomků není nutné vždy volat konstruktor předka. Náš konstruktor musí mít samozřejmě všechny parametry potřebné pro předka plus ty nové, co má navíc potomek, takže ho volat budeme. Některé potom předáme předkovi a některé si zpracujeme sami. Konstruktor předka se vykoná před naším konstruktorem.
V Pythonu existuje metoda super()
, která zavolá verzi metody
na předkovi. My tedy můžeme zavolat konstruktor předka s danými parametry a
poté vykonat navíc inicializaci pro mága. V Pythonu volání konstruktoru
předka (metody super()
) píšeme do těla našeho
konstruktoru.
Konstruktor mága bude tedy vypadat takto:
def __init__(self, jmeno, zivot, utok, obrana, kostka, mana, magicky_utok): super().__init__(jmeno, zivot, utok, obrana, kostka) self._mana = mana self._max_mana = mana self._magicky_utok = magicky_utok
Přesuňme se do hlavního programu v main.py
a druhého
bojovníka (Shadow) změňme na mága:
gandalf = Mag("Gandalf", 60, 15, 12, kostka, 30, 45)
Samozřejmě musíme doplnit import třídy Mag
a záměnu
udělat i v řádku, kde bojovníka do arény vkládáme.
Polymorfismus a přepisování metod
Bylo by výhodné, kdyby instance třídy Arena
mohla s mágem
pracovat stejným způsobem jako s bojovníkem. My již víme, že takovémuto
mechanismu říkáme polymorfismus. Aréna zavolá na
bojovníkovi metodu utoc()
se soupeřem v parametru. Nestará se o
to, jestli bude útok vykonávat bojovník nebo mág, bude s nimi pracovat
stejně. U mága si tedy přepíšeme metodu
utoc()
z předka. Zděděnou metodu přepíšeme tak, aby útok
pracoval s manou. Hlavička metody ale zůstane stejná.
V potomkovi můžeme jednoduše a bez okolků přepsat libovolnou metodu. V Pythonu jsou všechny metody - řečeno terminologií jazyků C++, C# - virtuální.
Nyní se vraťme k metodě utoc()
. V souboru s mágem
(mag.py
) ji přepíšeme (překryjeme). Její chování nebude
nijak složité. Podle hodnoty many buď provedeme běžný útok nebo útok
magický. Hodnotu many potom buď zvýšíme o 10
nebo naopak
snížíme na 0
v případě magického útoku:
def utoc(self, souper): # mana není naplněna if self._mana < self._max_mana: self._mana = self._mana + 10 if self._mana > self._max_mana: self._mana = self._max_mana uder = self._utok + self._kostka.hod() zprava = f"{self._jmeno} útočí s úderem za {uder} hp." self._nastav_zpravu(zprava) #magický útok else: uder = self._magicky_utok + self._kostka.hod() zprava = f"{self._jmeno} použil magii za {uder} hp." self._nastav_zpravu(zprava) self._mana = 0 souper.bran_se(uder)
Kód je srozumitelný. Všimněme si ale omezení many na
max_mana
. Může se nám totiž stát, že tuto hodnotu
přesáhneme. Nyní se na chvíli zastavme a zamysleme se nad kódem. Nemagický
útok výše v podstatě vykonává původní metoda utoc()
. Jistě
by tedy bylo přínosné zavolat podobu metody na předkovi místo toho, abychom
chování opisovali. K tomu opět použijeme metodu super()
, která
zavolá metodu utoc()
tak, jak je definovaná v rodičovské
třídě Bojovnik
:
{PYTHON} from bojovnik import Bojovnik class Mag(Bojovnik): def __init__(self, jmeno, zivot, utok, obrana, kostka, mana, magicky_utok): super().__init__(jmeno, zivot, utok, obrana, kostka) self._mana = mana self._max_mana = mana self._magicky_utok = magicky_utok def utoc(self, souper): # mana není naplněna if self._mana < self._max_mana: self._mana = self._mana + 10 if self._mana > self._max_mana: self._mana = self._max_mana super().utoc(souper) #magický útok else: uder = self._magicky_utok + self._kostka.hod() zprava = f"{self._jmeno} použil magii za {uder} hp." self._nastav_zpravu(zprava) self._mana = 0 souper.bran_se(uder) {/PYTHON}
{PYTHON} class Bojovnik: """ Třída reprezentující bojovníka do arény. """ def __init__(self, jmeno, zivot, utok, obrana, kostka): """ jmeno - jméno bojovníka zivot - maximální život bojovníka utok - útok bojovníka obrana - obrana bojovníka kostka - instance kostky """ self._jmeno = jmeno self._zivot = zivot self._max_zivot = zivot self._utok = utok self._obrana = obrana self._kostka = kostka self._zprava = "" def __str__(self): return str(self._jmeno) def je_nazivu(self): return self._zivot > 0 def graficky_zivot(self): celkem = 20 pocet = int(self._zivot / self._max_zivot * celkem) if pocet == 0 and self.je_nazivu(): pocet = 1 return f"[{'#' * pocet}{' ' * (celkem - pocet)}]" def bran_se(self, uder): zraneni = uder - (self._obrana + self._kostka.hod()) if zraneni > 0: zprava = f"{self._jmeno} utrpěl poškození {zraneni} hp." self._zivot = self._zivot - zraneni if self._zivot < 0: self._zivot = 0 zprava = f"{zprava[:-1]} a zemřel." else: zprava = f"{self._jmeno} odrazil útok." self._nastav_zpravu(zprava) def utoc(self, souper): uder = self._utok + self._kostka.hod() zprava = f"{self._jmeno} útočí s úderem za {uder} hp." self._nastav_zpravu(zprava) souper.bran_se(uder) def _nastav_zpravu(self, zprava): self._zprava = zprava def vrat_posledni_zpravu(self): return self._zprava {/PYTHON}
{PYTHON} from kostka import Kostka from bojovnik import Bojovnik from arena import Arena from mag import Mag # vytvoření objektů kostka = Kostka(10) zalgoren = Bojovnik("Zalgoren", 100, 20, 10, kostka) gandalf = Mag("Gandalf", 60, 15, 12, kostka, 30, 45) arena = Arena(zalgoren, gandalf, kostka) # zápas arena.zapas() {/PYTHON}
{PYTHON} class Kostka: def __init__(self, pocet_sten=6): self._pocet_sten = pocet_sten def __str__(self): return str(f"Kostka s {self._pocet_sten} stěnami.") def vrat_pocet_sten(self): return self._pocet_sten def hod(self): import random as _random return _random.randint(1, self._pocet_sten) def __repr__(self): return f"Kostka({self._pocet_sten})" {/PYTHON}
{PYTHON} from mag import Mag class Arena: def __init__(self, bojovnik_1, bojovnik_2, kostka): self._bojovnik_1 = bojovnik_1 self._bojovnik_2 = bojovnik_2 self._kostka = kostka def _vykresli(self): # zakomentováno kvůli kompileru # self._vycisti_obrazovku() print("-------------- Aréna -------------- \n") print("Zdraví bojovníků: \n") print(f"{self._bojovnik_1} {self._bojovnik_1.graficky_zivot()}") print(f"{self._bojovnik_2} {self._bojovnik_2.graficky_zivot()}") """ def _vycisti_obrazovku(self): import os as _os _os.system('cls' if _os.name == 'nt' else 'clear') """ def _vypis_zpravu(self, zprava): import time as _time print(zprava) # zakomentováno kvůli kompileru _time.sleep(0.75) def zapas(self): import random as _random print("Vítejte v aréně!") print(f"Dnes se utkají {self._bojovnik_1} a {self._bojovnik_2}!") print("Zápas může začít...", end=" ") input() # prohození bojovníků if _random.randint(0, 1): (self._bojovnik_1, self._bojovnik_2) = (self._bojovnik_2, self._bojovnik_1) # cyklus s bojem while self._bojovnik_1.je_nazivu() and self._bojovnik_2.je_nazivu(): self._bojovnik_1.utoc(self._bojovnik_2) self._vykresli() self._vypis_zpravu(self._bojovnik_1.vrat_posledni_zpravu()) self._vypis_zpravu(self._bojovnik_2.vrat_posledni_zpravu()) if self._bojovnik_2.je_nazivu(): self._bojovnik_2.utoc(self._bojovnik_1) self._vykresli() self._vypis_zpravu(self._bojovnik_2.vrat_posledni_zpravu()) self._vypis_zpravu(self._bojovnik_1.vrat_posledni_zpravu()) {/PYTHON}
Opět vidíme, jak můžeme znovupoužívat kód. S dědičností je spojeno opravdu mnoho technik, jak si ušetřit práci. V našem případě to ušetří několik řádků, ale u většího projektu by to mohlo mít obrovský význam. Ostatní nesoukromé metody se automaticky podědí.
Aplikace nyní funguje tak, jak má:
Hra funguje správně:
-------------- Aréna --------------
Zdraví bojovníků:
Gandalf [####################] ]
Zalgoren [############# ]
Gandalf použil magii za 51 hp.
Zalgoren utrpěl poškození 33 hp.
Aréna nás však neinformuje o maně mága. Pojďme to napravit. Přidáme
mágovi veřejnou metodu graficka_mana()
, která bude obdobně jako
u života vracet řetězec s grafickým ukazatelem many.
Abychom nemuseli logiku se složením ukazatele psát dvakrát, upravíme
metodu graficky_zivot()
ve třídě Bojovnik
.
Připomeňme si, jak vypadá:
def graficky_zivot(self): celkem = 20 pocet = int(self._zivot / self._max_zivot * celkem) if (pocet == 0 and self.je_nazivu()): pocet = 1 return f"[{'#' * pocet}{' ' * (celkem - pocet)}]"
Vidíme, že není kromě proměnných zivot
a
max_zivot
na životě nijak závislá. Metodu přejmenujeme na
graficky_ukazatel()
a dáme jí dva parametry: aktuální hodnotu a
maximální hodnotu. Proměnné zivot
a max_zivot
v
těle metody poté nahradíme za aktualni
a
maximalni
:
def graficky_ukazatel(self, aktualni, maximalni): celkem = 20 pocet = int(aktualni / maximalni * celkem) if (pocet == 0 and self.je_nazivu()): pocet = 1 return f"[{'#' * pocet}{' ' * (celkem - pocet)}]"
Metodu graficky_zivot()
ve třídě Bojovnik
naimplementujeme znovu, bude nám v ní stačit jediný řádek a to zavolání
metody graficky_ukazatel()
s příslušnými parametry:
def graficky_zivot(self): return self.graficky_ukazatel(self._zivot, self._max_zivot)
Určitě jsme mohli v tutoriálu s bojovníkem udělat metodu
graficky_ukazatel()
rovnou. Chtěli jsme však ukázat, jak se
řeší případy, kdy potřebujeme vykonat podobnou funkčnost vícekrát. S
takovouto parametrizací se praxi setkáme často, protože nikdy přesně
nevíme, co budeme v budoucnu od našeho programu požadovat.
Nyní můžeme vykreslovat ukazatel tak, jak se nám to hodí. Přesuňme se
do třídy Mag
a naimplementujme metodu
graficka_mana()
:
def graficka_mana(self): return self.graficky_ukazatel(self._mana, self._max_mana)
Jednoduché, že? Nyní je mág hotový, zbývá jen naučit arénu
zobrazovat manu v případě, že je bojovník mág. Přesuňme se tedy do
třídy Arena
.
Rozpoznání typu objektu
Jelikož se nám nyní vykreslení bojovníka zkomplikovalo, uděláme si na
něj samostatnou metodu vypis_bojovnika()
. Jejím parametrem bude
daná instance bojovníka:
def _vypis_bojovnika(self, bojovnik): print(bojovnik) print(f"Život: {bojovnik.graficky_zivot()}")
Nyní pojďme reagovat na to, jestli je bojovník mág. Už jsme si řekli,
že k tomu slouží funkce isinstance()
:
def _vypis_bojovnika(self, bojovnik): print(bojovnik) print(f"Život: {bojovnik.graficky_zivot()}") if isinstance(bojovnik, Mag): print(f"Mana: {bojovnik.graficka_mana()}")
To bychom měli, vypis_bojovnika()
budeme volat v metodě
vykresli()
, která bude vypadat takto:
{PYTHON} from mag import Mag class Arena: def __init__(self, bojovnik_1, bojovnik_2, kostka): self._bojovnik_1 = bojovnik_1 self._bojovnik_2 = bojovnik_2 self._kostka = kostka def _vykresli(self): # zakomentováno kvůli kompileru # self._vycisti_obrazovku() print("-------------- Aréna -------------- \n") print("Bojovníci: \n") self._vypis_bojovnika(self._bojovnik_1) self._vypis_bojovnika(self._bojovnik_2) """ def _vycisti_obrazovku(self): import os as _os _os.system('cls' if _os.name == 'nt' else 'clear') """ def _vypis_zpravu(self, zprava): import time as _time print(zprava) _time.sleep(.75) def zapas(self): import random as _random print("Vítejte v aréně!") print(f"Dnes se utkají {self._bojovnik_1} a {self._bojovnik_2}!") print("Zápas může začít...", end=" ") input() # prohození bojovníků if _random.randint(0, 1): (self._bojovnik_1, self._bojovnik_2) = (self._bojovnik_2, self._bojovnik_1) # cyklus s bojem while self._bojovnik_1.je_nazivu() and self._bojovnik_2.je_nazivu(): self._bojovnik_1.utoc(self._bojovnik_2) self._vykresli() self._vypis_zpravu(self._bojovnik_1.vrat_posledni_zpravu()) self._vypis_zpravu(self._bojovnik_2.vrat_posledni_zpravu()) if self._bojovnik_2.je_nazivu(): self._bojovnik_2.utoc(self._bojovnik_1) self._vykresli() self._vypis_zpravu(self._bojovnik_2.vrat_posledni_zpravu()) self._vypis_zpravu(self._bojovnik_1.vrat_posledni_zpravu()) def _vypis_bojovnika(self, bojovnik): print(bojovnik) print(f"Život: {bojovnik.graficky_zivot()}") if isinstance(bojovnik, Mag): print(f"Mana: {bojovnik.graficka_mana()}") {/PYTHON}
{PYTHON} from kostka import Kostka from bojovnik import Bojovnik from arena import Arena from mag import Mag # vytvoření objektů kostka = Kostka(10) zalgoren = Bojovnik("Zalgoren", 100, 20, 10, kostka) gandalf = Mag("Gandalf", 60, 15, 12, kostka, 30, 45) arena = Arena(zalgoren, gandalf, kostka) # zápas arena.zapas() {/PYTHON}
{PYTHON} class Kostka: def __init__(self, pocet_sten=6): self._pocet_sten = pocet_sten def __str__(self): return str(f"Kostka s {self._pocet_sten} stěnami.") def vrat_pocet_sten(self): return self._pocet_sten def hod(self): import random as _random return _random.randint(1, self._pocet_sten) def __repr__(self): return f"Kostka({self._pocet_sten})" {/PYTHON}
{PYTHON} class Bojovnik: """ Třída reprezentující bojovníka do arény. """ def __init__(self, jmeno, zivot, utok, obrana, kostka): """ jmeno - jméno bojovníka zivot - maximální život bojovníka utok - útok bojovníka obrana - obrana bojovníka kostka - instance kostky """ self._jmeno = jmeno self._zivot = zivot self._max_zivot = zivot self._utok = utok self._obrana = obrana self._kostka = kostka self._zprava = "" def __str__(self): return str(self._jmeno) def je_nazivu(self): return self._zivot > 0 def graficky_zivot(self): return self.graficky_ukazatel(self._zivot, self._max_zivot) def graficky_ukazatel(self, aktualni, maximalni): celkem = 20 pocet = int(aktualni / maximalni * celkem) if (pocet == 0 and self.je_nazivu()): pocet = 1 return f"[{'#' * pocet}{' ' * (celkem - pocet)}]" def bran_se(self, uder): zraneni = uder - (self._obrana + self._kostka.hod()) if zraneni > 0: zprava = f"{self._jmeno} utrpěl poškození {zraneni} hp." self._zivot = self._zivot - zraneni if self._zivot < 0: self._zivot = 0 zprava = f"{zprava[:-1]} a zemřel." else: zprava = f"{self._jmeno} odrazil útok." self._nastav_zpravu(zprava) def utoc(self, souper): uder = self._utok + self._kostka.hod() zprava = f"{self._jmeno} útočí s úderem za {uder} hp." self._nastav_zpravu(zprava) souper.bran_se(uder) def _nastav_zpravu(self, zprava): self._zprava = zprava def vrat_posledni_zpravu(self): return self._zprava {/PYTHON}
{PYTHON} from bojovnik import Bojovnik class Mag(Bojovnik): def __init__(self, jmeno, zivot, utok, obrana, kostka, mana, magicky_utok): super().__init__(jmeno, zivot, utok, obrana, kostka) self._mana = mana self._max_mana = mana self._magicky_utok = magicky_utok def utoc(self, souper): # mana není naplněna if self._mana < self._max_mana: self._mana = self._mana + 10 if self._mana > self._max_mana: self._mana = self._max_mana super().utoc(souper) # magický útok else: uder = self._magicky_utok + self._kostka.hod() zprava = f"{self._jmeno} použil magii za {uder} hp." self._nastav_zpravu(zprava) self._mana = 0 souper.bran_se(uder) def graficka_mana(self): return self.graficky_ukazatel(self._mana, self._max_mana) {/PYTHON}
A máme hotovo
Aplikaci ještě můžeme dodat hezčí vzhled. Vložíme například
ASCIIart nadpis Aréna, který vytvoříme ASCII generátorem. Metodu k
vykreslení ukazatele upravíme tak, aby vykreslovala plný obdélník místo
#
(ten napíšeme pomocí kláves Alt +
219). Výsledek bude vypadat takto:
Upravená podoba aplikace:
__ ____ ____ _ _ __
/__\ ( _ \( ___)( \( ) /__\
/(__)\ ) / )__) ) ( /(__)\
(__)(__)(_)\_)(____)(_)\_)(__)(__)
Bojovníci:
Zalgoren
Život: ████
Gandalf
Život: ███████
Mana: █
Gandalf použil magii za 48 hp
Zalgoren utrpěl poškození 33 hp
Kód je k dispozici v příloze. Pokud jste něčemu nerozuměli, zkuste si článek přečíst vícekrát nebo pomaleji, jsou to důležité praktiky.
V následujícím kvízu, Kvíz - Dědičnost a polymorfismus v Pythonu, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 1159x (4.42 kB)
Aplikace je včetně zdrojových kódů v jazyce Python