V tomto článku si ukážeme, jak vytvořit aplikaci pro procvičování násobilky s využitím objektově orientovaného programování (OOP) v Pythonu. Budeme se zabývat klíčovými principy OOP a jejich praktickým využitím při vývoji plnohodnotné aplikace.
Python je jazyk s více paradigmaty, což nám, vývojářům, umožňuje vybrat nejvhodnější přístup pro danou situaci. Objektově orientované programování představuje jedno z nejrozšířenějších paradigmat pro tvorbu škálovatelných aplikací v posledních desetiletích.
Základní principy OOP
Pojďme si stručně osvětlit nejdůležitější koncepty OOP v Pythonu, zejména třídy.
Třída je definice struktury a chování objektů. Slouží jako šablona pro vytváření instancí, které jsou konkrétními objekty vytvořenými podle této šablony.
Jednoduchou třídu pro reprezentaci knihy s atributy, jako je název a barva, bychom definovali takto:
class Kniha: def __init__(self, nazev, barva): self.nazev = nazev self.barva = barva
Pro vytvoření instancí třídy Kniha musíme tuto třídu zavolat a předat jí požadované argumenty:
# Vytvoření instancí třídy Kniha modra_kniha = Kniha("Modrý kluk", "Modrá") zelena_kniha = Kniha("Příběh o žábě", "Zelená")
Náš aktuální program bychom tedy mohli reprezentovat takto:
Je zajímavé, že když zkontrolujeme typ proměnných `modra_kniha` a `zelena_kniha`, získáme výstup „Kniha“.
# Zobrazení typu instancí knih print(type(modra_kniha)) # <class '__main__.Kniha'> print(type(zelena_kniha)) # <class '__main__.Kniha'>
S těmito základními principy můžeme začít pracovat na našem projektu 😃.
Specifikace projektu
Při práci na pozici vývojáře nebo programátora netrávíme většinu času psaním kódu. Podle studie newstack se psaní a refaktorování kódu věnujeme pouze třetinu času.
Zbylé dvě třetiny času analyzujeme cizí kód a problémy, na kterých pracujeme.
Proto pro tento projekt definujeme konkrétní problém a budeme analyzovat, jak z něj vytvořit naši aplikaci. Tímto způsobem projdeme celý proces, od návrhu řešení až po jeho implementaci pomocí kódu.
Učitel základní školy potřebuje hru, která by otestovala dovednosti žáků ve věku 8 až 10 let v oblasti násobení.
Hra by měla mít systém životů a bodů. Žák začíná se 3 životy a pro výhru musí dosáhnout určitého počtu bodů. Pokud student ztratí všechny životy, program zobrazí zprávu o prohře.
Hra by měla mít dva režimy: náhodné násobení a násobení tabulkami.
V prvním režimu se studentovi zobrazí náhodný příklad násobení (čísla 1 až 10) a za správnou odpověď získá bod. Pokud odpoví špatně, ztratí život a hra pokračuje. Student vyhraje, pokud získá 5 bodů.
V druhém režimu se zobrazí násobilka (od 1 do 10) a student musí zadat správný výsledek pro každé násobení. Pokud student 3x neuspěje, prohrává. Pokud student úspěšně dokončí dvě tabulky, vyhrává.
Vím, že požadavky jsou možná trochu rozsáhlé, ale slibuji, že je v tomto článku zvládneme 😁.
Rozděl a panuj
Klíčovou dovedností v programování je schopnost řešit problémy. Před zahájením kódování je důležité mít plán.
Doporučuji si velký problém rozdělit na menší, které lze snadněji a efektivněji řešit.
Při vytváření hry je tedy vhodné ji rozdělit na její nejdůležitější části. Tyto menší problémy bude mnohem snadnější vyřešit.
Poté si ujasníme, jak vše propojit a integrovat do kódu.
Nyní si vytvořme diagram, jak by hra mohla vypadat.
Tento diagram ilustruje vztahy mezi objekty naší aplikace. Jak vidíte, dvě hlavní části jsou náhodné násobení a násobení tabulkami. Společné mají atributy body a životy.
S těmito informacemi můžeme přejít k samotnému kódu.
Vytvoření rodičovské třídy Hra
Při práci s objektově orientovaným programováním se snažíme co nejvíce omezit opakování kódu. Využíváme koncept DRY (neopakuj se).
Důležité: Cílem není snížit počet řádků kódu, ale abstrahovat nejpoužívanější logiku.
Rodičovská třída naší aplikace definuje strukturu a chování, které budou sdílet obě odvozené třídy.
Podívejme se, jak by to vypadalo v kódu:
class ZakladniHra: # Délka zprávy pro zarovnání na střed delka_zpravy = 60 popis = "" def __init__(self, body_k_vyhre, pocet_zivotu=3): """Základní třída hry Args: body_k_vyhre (int): počet bodů potřebný k dokončení hry pocet_zivotu (int): počet životů, které má student. Výchozí hodnota je 3. """ self.body_k_vyhre = body_k_vyhre self.body = 0 self.zivoty = pocet_zivotu def ziskej_ciselny_vstup(self, zprava=""): while True: # Získání vstupu od uživatele vstup_uzivatele = input(zprava) # Pokud je vstup číselný, vrať ho # Jinak zobraz zprávu a opakuj if vstup_uzivatele.isnumeric(): return int(vstup_uzivatele) else: print("Vstup musí být číslo") continue def zobraz_uvitaci_zpravu(self): print("HRA NA NÁSOBILKU V PYTHONU".center(self.delka_zpravy)) def zobraz_zpravu_o_prohre(self): print("PROHRÁL/A JSI, VYČERPAL/A JSI VŠECHNY ŽIVOTY".center(self.delka_zpravy)) def zobraz_zpravu_o_vyhre(self): print(f"GRATULUJEME, ZÍSKAL/A JSI {self.body}".center(self.delka_zpravy)) def zobraz_aktualni_zivoty(self): print(f"Aktuálně máš {self.zivoty} životů\n") def zobraz_aktualni_skore(self): print(f"\nTvé skóre je {self.body}") def zobraz_popis(self): print("\n\n" + self.popis.center(self.delka_zpravy) + "\n") # Základní metoda pro spuštění hry def spustit(self): self.zobraz_uvitaci_zpravu() self.zobraz_popis()
Tato třída vypadá dost rozsáhle. Pojďme si ji podrobně rozebrat.
Nejprve se podíváme na atributy třídy a konstruktor.
Atributy třídy jsou proměnné definované uvnitř třídy, ale mimo konstruktor nebo jakoukoli metodu.
Atributy instance jsou proměnné, které jsou vytvořeny pouze uvnitř konstruktoru.
Zásadní rozdíl mezi nimi je v rozsahu. Atributy třídy jsou dostupné z instancí i ze samotné třídy, zatímco atributy instance jsou dostupné pouze z objektu instance.
hra = ZakladniHra(5) # Přístup k atributu třídy delka_zpravy ze třídy print(hra.delka_zpravy) # 60 # Přístup k atributu třídy delka_zpravy ze třídy print(ZakladniHra.delka_zpravy) # 60 # Přístup k atributu instance body z instance print(hra.body) # 0 # Přístup k atributu instance body ze třídy # vyvolá Attribute error # print(ZakladniHra.body)
Podrobněji se tomuto tématu můžeme věnovat v některém z příštích článků.
Funkce `ziskej_ciselny_vstup` slouží k tomu, aby se zabránilo vložení jiného vstupu než číselného. Tato metoda opakovaně vyžaduje vstup od uživatele, dokud nezíská číselný vstup. Budeme ji používat v odvozených třídách.
Tiskové metody nám umožňují vyhnout se opakování tisku stejné zprávy pokaždé, když ve hře dojde k nějaké události.
Metoda `spustit` je pouze obal, který budou třídy pro náhodné násobení a tabulkové násobení používat pro interakci s uživatelem a spuštění hry.
Vytvoření odvozených tříd
Po vytvoření rodičovské třídy, která definuje základní strukturu a funkce naší aplikace, můžeme přejít k tvorbě specifických herních režimů s využitím principu dědičnosti.
Třída NáhodnéNásobení
Tato třída bude implementovat „první režim“ naší hry. Využijeme modul `random` pro generování náhodných příkladů násobení čísel od 1 do 10. Zde je užitečný článek o modulu random (a dalších užitečných modulech) 😉.
import random # Modul pro náhodné operace
class NahodneNasobeni(ZakladniHra): popis = "V této hře musíš správně odpovědět na náhodné příklady násobení.\nVyhráváš, pokud získáš 5 bodů, prohráváš, pokud ztratíš všechny životy." def __init__(self): # Potřebuješ 5 bodů k výhře # Parametr "body_k_vyhre" se nastaví na 5 super().__init__(5) def ziskej_nahodna_cisla(self): prvni_cislo = random.randint(1, 10) druhe_cislo = random.randint(1, 10) return prvni_cislo, druhe_cislo def spustit(self): # Volá metodu spustit z rodičovské třídy pro zobrazení úvodní zprávy super().spustit() while self.zivoty > 0 and self.body_k_vyhre > self.body: # Získá dvě náhodná čísla cislo1, cislo2 = self.ziskej_nahodna_cisla() priklad = f"{cislo1} x {cislo2}: " # Vyzve uživatele k zadání odpovědi # Zabraňuje chybám odpoved_uzivatele = self.ziskej_ciselny_vstup(zprava=priklad) if odpoved_uzivatele == cislo1 * cislo2: print("\nSprávná odpověď!\n") # Přidá bod self.body += 1 else: print("\nBohužel, špatná odpověď.\n") # Odečte život self.zivoty -= 1 self.zobraz_aktualni_skore() self.zobraz_aktualni_zivoty() # Tento kód se provede, když hra skončí # a není splněna žádná z podmínek else: # Zobrazí závěrečnou zprávu if self.body >= self.body_k_vyhre: self.zobraz_zpravu_o_vyhre() else: self.zobraz_zpravu_o_prohre()
Opět se jedná o rozsáhlou třídu, ale jak jsem již uvedl, důležitý není počet řádků, ale čitelnost a efektivita. A Python umožňuje vytvářet čistý a čitelný kód, který se podobá běžné angličtině.
V této třídě se může objevit jeden koncept, který je třeba vysvětlit:
# Rodičovská třída def __init__(self, body_k_vyhre, pocet_zivotu=3): "... # Odvozená třída def __init__(self): # Potřebuješ 5 bodů k výhře # Parametr "body_k_vyhre" se nastaví na 5 super().__init__(5)
Konstruktor odvozené třídy volá funkci `super`, která odkazuje na konstruktor rodičovské třídy (`ZakladniHra`). Zjednodušeně řečeno, Pythonu tímto způsobem říkáme:
Nastav atribut `body_k_vyhre` v rodičovské třídě na hodnotu 5!
Není nutné vkládat `self` do volání `super().__init__()`, protože voláme `super` uvnitř konstruktoru, což by bylo redundantní.
Funkci `super` také používáme v metodě `spustit`. Podívejme se, co se zde děje.
# Základní metoda pro spuštění # Rodičovská metoda def spustit(self): self.zobraz_uvitaci_zpravu() self.zobraz_popis() def spustit(self): # Volá metodu spustit z rodičovské třídy pro zobrazení úvodní zprávy super().spustit() .....
Jak vidíte, metoda `spustit` v rodičovské třídě zobrazí uvítací zprávu a popis. Je však užitečné zachovat tuto funkčnost a přidat další kroky do odvozených tříd. Proto používáme `super` k provedení kódu rodičovské metody před spuštěním dalšího kódu v metodě `spustit` v odvozené třídě.
Zbytek metody `spustit` je poměrně jednoduchý. Vyžádá si od uživatele číslo na základě generovaného příkladu násobení. Výsledek se poté porovná se správným výsledkem. Pokud se shodují, přidá se bod, v opačném případě se odečte jeden život.
Používáme zde cyklus `while-else`. Podrobnější vysvětlení tohoto cyklu je nad rámec tohoto článku, ale brzy ho zveřejním.
Funkce `ziskej_nahodna_cisla` používá `random.randint`, která vrací náhodné celé číslo v daném rozsahu. Vrací n-tici dvou náhodných celých čísel.
Třída TabulkoveNasobeni
„Druhý režim“ zobrazí hru v podobě násobilky a zajistí, aby uživatel správně vyřešil alespoň dvě tabulky.
Opět využijeme `super` a upravíme atribut rodičovské třídy `body_k_vyhre` na hodnotu 2.
class TabulkoveNasobeni(ZakladniHra): popis = "V této hře musíš správně vyřešit kompletní násobilku.\nVyhráváš, pokud správně vyřešíš 2 tabulky." def __init__(self): # Potřebuje vyřešit 2 tabulky k výhře super().__init__(2) def spustit(self): # Zobrazí uvítací zprávu super().spustit() while self.zivoty > 0 and self.body_k_vyhre > self.body: # Získá náhodné číslo cislo = random.randint(1, 10) for i in range(1, 11): if self.zivoty <= 0: # Zajistí ukončení hry, # pokud uživatel vyčerpá životy self.body = 0 break priklad = f"{cislo} x {i}: " odpoved_uzivatele = self.ziskej_ciselny_vstup(zprava=priklad) if odpoved_uzivatele == cislo * i: print("Výborně! Odpověď je správná.") else: print("Bohužel, odpověď není správná.") self.zivoty -= 1 self.body += 1 # Tento kód se provede, když hra skončí # a není splněna žádná z podmínek else: # Zobrazí závěrečnou zprávu if self.body >= self.body_k_vyhre: self.zobraz_zpravu_o_vyhre() else: self.zobraz_zpravu_o_prohre()
Jak můžete vidět, upravujeme pouze metodu `spustit` v této třídě. V tom spočívá síla dědičnosti. Jednou napíšeme logiku, kterou můžeme opakovaně používat 😅.
V metodě `spustit` pomocí cyklu for získáváme čísla od 1 do 10 a sestavujeme příklad násobení, který se zobrazí uživateli.
Pokud jsou životy vyčerpány, nebo je dosaženo počtu bodů potřebných k výhře, cyklus `while` se přeruší a zobrazí se zpráva o výhře nebo prohře.
Ano, vytvořili jsme dva herní režimy, ale pokud program spustíme, nic se nestane.
Pojďme tedy dokončit program implementací volby režimu a vytvořením instancí tříd v závislosti na této volbě.
Implementace volby režimu
Uživatel si bude moci vybrat, jaký režim chce hrát. Ukážeme si, jak to implementovat.
if __name__ == "__main__": print("Vyber si herní režim") vyber = input("[1], [2]: ") if vyber == "1": hra = NahodneNasobeni() elif vyber == "2": hra = TabulkoveNasobeni() else: print("Vyber prosím platný herní režim") exit() hra.spustit()
Nejprve vyzveme uživatele, aby si vybral jeden ze dvou režimů. Pokud je vstup neplatný, skript se ukončí. Pokud si uživatel vybere první režim, program spustí herní režim NáhodnéNásobení. Pokud si uživatel vybere druhý režim, spustí se herní režim TabulkoveNasobeni.
Takto by to vypadalo:
Závěr
Gratuluji, právě jsi vytvořil python aplikaci s objektově orientovaným programováním.
Veškerý kód je dostupný v úložišti na Githubu.
V tomto článku jsi se naučil:
- Používat konstruktory v třídách Pythonu
- Vytvořit funkční aplikaci s využitím OOP
- Používat funkci super v třídách Pythonu
- Používat základy dědičnosti
- Implementovat atributy třídy a instance
Přeji příjemné kódování 👨💻
Prozkoumej také některá z nejlepších Python IDE pro zvýšení produktivity.