Sestavte aplikaci Python Multiplication Table s OOP

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.