11. diel - Aréna s mágom (dedičnosť a polymorfizmus)
V minulej lekcii, Dedičnosť a polymorfizmus v Pythone, sme si vysvetlili dedičnosť a polymorfizmus.
V dnešnom tutoriále objektovo orientovaného programovania v Pythone si vyskúšame dedičnosť a polymorfizmus v praxi. Bude to opäť na našej hre s bojovníkmi, kde z bojovníka zdedíme mága. Tento tutoriál už patrí k tým náročnejším a bude to tak aj pri ďalších. Preto si priebežne precvičujte prácu s objektmi, skúšajte si naše cvičenia a taktiež vymýšľajte svoje vlastné aplikácie, aby ste si zažili základné veci. To, že je tu prítomný celý seriál neznamená, že ho celý zrazu prečítate a pochopíte Snažte sa programovať priebežne.
Než začneme niečo písať, zhodnime sa na tom, čo by mal mág vedieť. Mág bude fungovať rovnako ako bojovník. Okrem života bude mať však aj manu. Spočiatku bude mana plná. V prípade plnej many môže mág vykonať magický útok, ktorý bude mať pravdepodobne vyššie poškodenie, než útok normálny (ale samozrejme záleží na tom, ako si ho nastavíme). Tento útok manu vybije na 0. Každé kolo sa bude mana zvyšovať o 10 a mág bude podnikať len bežný útok. Akonáhle sa mana úplne doplní, opäť bude môcť magický útok použiť. Mana bude zobrazená grafickým ukazovateľom, rovnako ako život.
Do pôvodného projektu ArenaFight
vytvoríme teda triedu
Mage
, ktorú zdedíme z triedy Warrior
. Opäť si pre
ňu založíme samostatný súbor a nazveme ho mage.py
Obsah
súboru bude zatiaľ vyzerať takto (triedu si opäť okomentujte):
from warrior import Warrior class Mage(Warrior):
Konštruktor potomka
Pôvodný konštruktor bojovníka použiť nemôžeme, pretože chceme mať u mága dva parametre navyše (manu a magický útok).
Definujeme si teda konštruktor v potomkovi, ktorý berie parametre potrebné na vytvorenie bojovníka a niekoľko parametrov navyše pre mága.
U potomkov nie je nutné vždy volať konštruktor predka. Náš konštruktor musí mať samozrejme všetky parametre potrebné pre predka plus tie nové, čo má navyše potomok, takže ho volať budeme. Niektoré potom odovzdáme predkovi a niektoré si spracujeme sami. Konštruktor predka sa vykoná pred naším konštruktorom.
V Pythone existuje metóda super()
, ktorá zavolá verziu
metódy na predkovi. My teda môžeme zavolať konštruktor predka s danými
parametrami a potom vykonať naviac inicializáciu pre mága. V Pythone volaní
konštruktora predka (metódy super()
) píšeme do tela nášho
konštruktora.
Konštruktor mága bude teda vyzerať takto:
def __init__(self, name, health, damage, defense, die, mana, magic_damage): super().__init__(name, health, damage, defense, die) self._mana = mana self._max_mana = mana self._magic_damage = magic_damage
Presuňme sa do hlavného programu v main.py
a druhého
bojovníka (Shadow) zmeňme na mága:
gandalf = Mage("Gandalf", 60, 15, 12, die, 30, 45)
Samozrejme musíme doplniť import triedy Mage
a zámenu urobiť
aj v riadku, kde bojovníka do arény vkladáme.
Polymorfizmus a prepisovanie metód
Bolo by výhodné, keby inštancia triedy Arena
mohla s mágom
pracovať rovnakým spôsobom ako s bojovníkom. My už vieme, že takémuto
mechanizmu hovoríme polymorfizmus. Aréna zavolá na
bojovníkovi metódu attack()
so súperom v parametri. Nestará sa
o to, či bude útok vykonávať bojovník alebo mág, bude s nimi pracovať
rovnako. U mága si teda prepíšeme metódu
attack()
z predka. Zdedenú metódu prepíšeme tak, aby útok
pracoval s manou. Hlavička metódy ale zostane rovnaká.
V potomkovi môžeme jednoducho a bez okolkov prepísať ľubovoľnú metódu. V Pythone sú všetky metódy - povedané terminológiou jazykov C++, C# - virtuálne.
Teraz sa vráťme k metóde attack()
. V súbore s mágom
(mage.py
) ju prepíšeme (prekryjeme). Jej správanie nebude nijako
zložité. Podľa hodnoty many buď vykonáme bežný útok alebo útok
magický. Hodnotu many potom buď zvýšime o 10
alebo naopak
znížime na 0
v prípade magického útoku:
def attack(self, enemy): # Mana isn't full if self._mana < self._max_mana: self._mana = self._mana + 10 if self._mana > self._max_mana: self._mana = self._max_mana hit = self._damage + self._die.roll() message = f"{self._name} attacks with a hit worth {hit} hp." self._set_message(message) # Magic damage else: hit = self._magic_damage + self._die.roll() message = f"{self._name} used magic for {hit} hp." self._set_message(message) self._mana = 0 enemy.defend(hit)
Kód je zrozumiteľný. Všimnime si ale obmedzenie many na
max_mana
. Môže sa nám totiž stať, že túto hodnotu
presiahneme. Teraz sa na chvíľu zastavme a zamyslime sa nad kódom. Nemagický
útok vyššie v podstate vykonáva pôvodná metóda attack()
.
Iste by teda bolo prínosné zavolať podobu metódy na predkovi namiesto toho,
aby sme správanie opisovali. Na to opäť použijeme metódu
super()
, ktorá zavolá metódu attack()
tak, ako je
definovaná v rodičovskej triede Warrior
:
{PYTHON} from warrior import Warrior class Mage(Warrior): def __init__(self, name, health, damage, defense, die, mana, magic_damage): super().__init__(name, health, damage, defense, die) self._mana = mana self._max_mana = mana self._magic_damage = magic_damage def attack(self, enemy): # Mana isn't full if self._mana < self._max_mana: self._mana = self._mana + 10 if self._mana > self._max_mana: self._mana = self._max_mana super().attack(enemy) # Magic damage else: hit = self._magic_damage + self._die.roll() message = f"{self._name} used magic for {hit} hp." self._set_message(message) self._mana = 0 enemy.defend(hit) {/PYTHON}
{PYTHON} class Warrior: """ The class represents an arena warrior. """ def __init__(self, name, health, damage, defense, die): """ name - the warrior's name health - maximum health damage - damage defense - defense die - a rolling die instance """ self._name = name self._health = health self._max_health = health self._damage = damage self._defense = defense self._die = die self._message = "" def __str__(self): return str(self._name) def is_alive(self): return self._health > 0 def health_bar(self): total = 20 count = int(self._health / self._max_health * total) if count == 0 and self.is_alive(): count = 1 return f"[{'#' * count}{' ' * (total - count)}]" def defend(self, hit): injury = hit - (self._defense + self._die.roll()) if injury > 0: message = f"{self._name} defended against the attack but still lost {injury} hp." self._health = self._health - injury if self._health <= 0: self._health = 0 message = f"{message[:-1]} and died." else: message = f"{self._name} blocked the hit." self._set_message(message) def attack(self, enemy): hit = self._damage + self._die.roll() message = f"{self._name} attacks with a hit worth {hit} hp." self._set_message(message) enemy.defend(hit) def _set_message(self, message): self._message = message def get_last_message(self): return self._message {/PYTHON}
{PYTHON} from rolling_die import RollingDie from warrior import Warrior from arena import Arena from mage import Mage # creating objects die = RollingDie(10) zalgoren = Warrior("Zalgoren", 100, 20, 10, die) gandalf = Mage("Gandalf", 60, 15, 12, die, 30, 45) arena = Arena(zalgoren, gandalf, die) # fight arena.fight() {/PYTHON}
{PYTHON} class RollingDie: def __init__(self, sides_count=6): self._sides_count = sides_count def __str__(self): return str(f"Rolling die with {self._sides_count} sides.") def get_sides_count(self): return self._sides_count def roll(self): import random as _random return _random.randint(1, self._sides_count) def __repr__(self): return f"RollingDie({self._sides_count})" {/PYTHON}
{PYTHON} from mage import Mage class Arena: def __init__(self, warrior_1, warrior_2, die): self._warrior_1 = warrior_1 self._warrior_2 = warrior_2 self._die = die def _render(self): # commented out due to the compiler # self._clear_screen() print("-------------- Arena -------------- \n") print("Warriors health: \n") print(f"{self._warrior_1} {self._warrior_1.health_bar()}") print(f"{self._warrior_2} {self._warrior_2.health_bar()}") """ def _clear_screen(self): import os as _os _os.system('cls' if _os.name == 'nt' else 'clear') """ def _print_message(self, message): import time as _time print(message) # commented out due to the compiler _time.sleep(0.75) def fight(self): import random as _random print("Welcome to the Arena!") print(f"Today {self._warrior_1} will battle against {self._warrior_2}!") print("Let the battle begin...", end=" ") input() # swapping the warriors if _random.randint(0, 1): (self._warrior_1, self._warrior_2) = (self._warrior_2, self._warrior_1) # fight loop while self._warrior_1.is_alive() and self._warrior_2.is_alive(): self._warrior_1.attack(self._warrior_2) self._render() self._print_message(self._warrior_1.get_last_message()) self._print_message(self._warrior_2.get_last_message()) if self._warrior_2.is_alive(): self._warrior_2.attack(self._warrior_1) self._render() self._print_message(self._warrior_2.get_last_message()) self._print_message(self._warrior_1.get_last_message()) {/PYTHON}
Opäť vidíme, ako môžeme znovupoužívať kód. S dedičnosťou je spojených naozaj mnoho techník, ako si ušetriť prácu. V našom prípade to ušetrí niekoľko riadkov, ale pri väčšom projekte by to mohlo mať obrovský význam. Ostatné nesúkromné metódy sa automaticky zdedia.
Aplikácia teraz funguje tak, ako má:
The game works correctly:
-------------- Arena --------------
Warriors health:
Gandalf [####################] ]
Zalgoren [############# ]
Gandalf used magic for 51 hp.
Zalgoren defended against the attack but still lost 33 hp.
Aréna nás však neinformuje o mane mága. Poďme to napraviť. Pridáme
mágovi verejnú metódu mana_bar()
, ktorá bude obdobne ako pri
živote vracať reťazec s grafickým ukazovateľom many.
Aby sme nemuseli logiku so zložením ukazovateľa písať dvakrát,
upravíme metódu health_bar()
v triede Warrior
.
Pripomeňme si, ako vyzerá:
def health_bar(self): total = 20 count = int(self._health / self._max_health * total) if (count == 0 and self.is_alive()): count = 1 return f"[{'#' * count}{' ' * (total - count)}]"
Vidíme, že nie je okrem premenných health
a
max_health
na živote nijako závislá. Metódu premenujeme na
graphical_bar()
a dáme jej dva parametre: aktuálnu hodnotu a
maximálnu hodnotu. Premenné health
a max_health
v
tele metódy potom nahradíme za current
a
maximum
:
def graphical_bar(self, current, maximum): total = 20 count = int(current / maximum * total) if (count == 0 and self.is_alive()): count = 1 return f"[{'#' * count}{' ' * (total - count)}]"
Metódu health_bar()
v triede Warrior
naimplementujeme znova, bude nám v nej stačiť jediný riadok a to zavolanie
metódy graphical_bar()
s príslušnými parametrami:
def health_bar(self): return self.graphical_bar(self._health, self._max_health)
Určite sme mohli v tutoriáli s bojovníkom urobiť metódu
graphical_bar()
rovno. Chceli sme však ukázať, ako sa riešia
prípady, keď potrebujeme vykonať podobnú funkčnosť viackrát. S takouto
parametrizáciou sa v prax stretneme často, pretože nikdy presne nevieme, čo
budeme v budúcnosti od nášho programu požadovať.
Teraz môžeme vykresľovať ukazovateľ tak, ako sa nám to hodí. Presuňme
sa do triedy Mage
a naimplementujme metódu
mana_bar()
:
def mana_bar(self): return self.graphical_bar(self._mana, self._max_mana)
Jednoduché, že? Teraz je mág hotový, zostáva len naučiť arénu
zobrazovať manu v prípade, že je bojovník mág. Presuňme sa teda do triedy
Arena
.
Rozpoznanie typu objektu
Keďže sa nám teraz vykreslenie bojovníka skomplikovalo, urobíme si naň
v aréne samostatnú metódu print_warrior()
. Jej parametrom bude
daná inštancia bojovníka:
def _print_warrior(self, warrior): print(warrior) print(f"Health: {warrior.health_bar()}")
Teraz poďme reagovať na to, či je bojovník mág. Už sme si povedali, že
na to slúži funkcia isinstance()
:
def _print_warrior(self, warrior): print(warrior) print(f"Health: {warrior.health_bar()}") if isinstance(warrior, Mage): print(f"Mana: {warrior.mana_bar()}")
To by sme mali, print_warrior()
budeme volať v metóde
render()
, ktorá bude vyzerať takto:
{PYTHON} from mage import Mage class Arena: def __init__(self, warrior_1, warrior_2, die): self._warrior_1 = warrior_1 self._warrior_2 = warrior_2 self._die = die def _render(self): # commented out due to the compiler # self._clear_screen() print("-------------- Arena -------------- \n") print("Warriors: \n") self._print_warrior(self._warrior_1) self._print_warrior(self._warrior_2) """ def _clear_screen(self): import os as _os _os.system('cls' if _os.name == 'nt' else 'clear') """ def _print_message(self, message): import time as _time print(message) _time.sleep(.75) def fight(self): import random as _random print("Welcome to the Arena!") print(f"Today {self._warrior_1} will battle against {self._warrior_2}!") print("Zápas může začít...", end=" ") input() # swapping the warriors if _random.randint(0, 1): (self._warrior_1, self._warrior_2) = (self._warrior_2, self._warrior_1) # fight loop while self._warrior_1.is_alive() and self._warrior_2.is_alive(): self._warrior_1.attack(self._warrior_2) self._render() self._print_message(self._warrior_1.get_last_message()) self._print_message(self._warrior_2.get_last_message()) if self._warrior_2.is_alive(): self._warrior_2.attack(self._warrior_1) self._render() self._print_message(self._warrior_2.get_last_message()) self._print_message(self._warrior_1.get_last_message()) def _print_warrior(self, warrior): print(warrior) print(f"Health: {warrior.health_bar()}") if isinstance(warrior, Mage): print(f"Mana: {warrior.mana_bar()}") {/PYTHON}
{PYTHON} from rolling_die import RollingDie from warrior import Warrior from arena import Arena from mage import Mage # creating objects die = RollingDie(10) zalgoren = Warrior("Zalgoren", 100, 20, 10, die) gandalf = Mage("Gandalf", 60, 15, 12, die, 30, 45) arena = Arena(zalgoren, gandalf, die) # fight arena.fight() {/PYTHON}
{PYTHON} class RollingDie: def __init__(self, sides_count=6): self._sides_count = sides_count def __str__(self): return str(f"Rolling die with {self._sides_count} sides.") def get_sides_count(self): return self._sides_count def roll(self): import random as _random return _random.randint(1, self._sides_count) def __repr__(self): return f"RollingDie({self._sides_count})" {/PYTHON}
{PYTHON} class Warrior: """ The class represents an arena warrior. """ def __init__(self, name, health, damage, defense, die): """ name - the warrior's name health - maximum health damage - damage defense - defense die - a rolling die instance """ self._name = name self._health = health self._max_health = health self._damage = damage self._defense = defense self._die = die self._message = "" def __str__(self): return str(self._name) def is_alive(self): return self._health > 0 def health_bar(self): return self.graphical_bar(self._health, self._max_health) def graphical_bar(self, current, maximum): total = 20 count = int(current / maximum * total) if (count == 0 and self.is_alive()): count = 1 return f"[{'#' * count}{' ' * (total - count)}]" def defend(self, hit): injury = hit - (self._defense + self._die.roll()) if injury > 0: message = f"{self._name} defended against the attack but still lost {injury} hp." self._health = self._health - injury if self._health <= 0: self._health = 0 message = f"{message[:-1]} and died." else: message = f"{self._name} blocked the hit." self._set_message(message) def attack(self, enemy): hit = self._damage + self._die.roll() message = f"{self._name} attacks with a hit worth {hit} hp." self._set_message(message) enemy.defend(hit) def _set_message(self, message): self._message = message def get_last_message(self): return self._message {/PYTHON}
{PYTHON} from warrior import Warrior class Mage(Warrior): def __init__(self, name, health, damage, defense, die, mana, magic_damage): super().__init__(name, health, damage, defense, die) self._mana = mana self._max_mana = mana self._magic_damage = magic_damage def attack(self, enemy): # Mana isn't full if self._mana < self._max_mana: self._mana = self._mana + 10 if self._mana > self._max_mana: self._mana = self._max_mana super().attack(enemy) # Magic damage else: hit = self._magic_damage + self._die.roll() message = f"{self._name} used magic for {hit} hp." self._set_message(message) self._mana = 0 enemy.defend(hit) def mana_bar(self): return self.graphical_bar(self._mana, self._max_mana) {/PYTHON}
A máme hotovo
Aplikáciu ešte môžeme dodať krajší vzhľad. Vložíme napríklad
ASCIIart nadpis Arena, ktorý vytvoríme ASCII generátorom. Metódu na
vykreslenie ukazovateľa upravíme tak, aby vykresľovala plný obdĺžnik
miesto #
(ten napíšeme pomocou klávesov Alt +
2 1 9). Výsledok bude vyzerať takto:
Modified version of the application:
__ ____ ____ _ _ __
/__\ ( _ \( ___)( \( ) /__\
/(__)\ ) / )__) ) ( /(__)\
(__)(__)(_)\_)(____)(_)\_)(__)(__)
Warriors:
Zalgoren
Health: ████
Gandalf
Health: ███████
Mana: █
Gandalf used magic for 48 hp
Zalgoren defended against the attack but still lost 33 hp
Kód je k dispozícii v prílohe. Pokiaľ ste niečomu nerozumeli, skúste si článok prečítať viackrát alebo pomalšie, sú to dôležité praktiky.
V nasledujúcom kvíze, Kvíz - Dedičnosť a polymorfizmus v Pythone, si vyskúšame nadobudnuté skúsenosti z predchádzajúcich lekcií.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 0x (12.65 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Python