3. díl - Hrací kostka v Pythonu - Konstruktory a náhodná čísla

Python Objektově orientované programování Hrací kostka v Pythonu - Konstruktory a náhodná čísla

V minulém tutoriálu z našeho seriálu o jazyce Python jsme si naprogramovali první objektovou aplikaci. Již umíme tvořit nové třídy a vkládat do nich atributy a metody s parametry a návratovou hodnotou. Dnes začneme pracovat na slíbené aréně, ve které budou proti sobě bojovat dva bojovníci. Boj bude tahový (na přeskáčku) a bojovník vždy druhému ubere život na základě síly jeho útoku a obrany druhého bojovníka. Simulujeme v podstatě stolní hru, budeme tedy simulovat i hrací kostku, která dodá hře prvek náhodnosti. Začněme zvolna a vytvořme si dnes právě tuto hrací kostku. Zároveň se naučíme jak definovat vlastní konstruktor.

Založíme si nový soubor a pojmenujeme ho Arena. Vytvoříme novou třídu s názvem Kostka. Zamysleme se nad atributy, které kostce dáme. Jistě by se hodilo, kdybychom si mohli zvolit počet stěn kostky (klasicky 6 nebo 10 stěn, jak je zvykem u tohoto typu her). Dále bude kostka potřebovat tzv. generátor náhodných čísel. Ten nám samozřejmě poskytne modul random, který k těmto účelům obsahuje třídu Random. Naše třída bude mít nyní atribut: pocet_sten. Minule jsme kvůli jednoduchosti nastavovali všechny atributy naší třídy jako veřejně přístupné. Většinou se však spíše nechce, aby se daly zvenčí modifikovat a proto se nastavují jako soukromé. Soukromé atributy začínají dvěma podtržítky. K atributu poté nelze normálně přistupovat. Při návrhu třídy tedy použijeme pro atributy podtržítka a v případě, že něco bude opravdu potřeba vystavit, je nepoužijeme. Naše třída zatím vypadá asi takto, atributy přidáme až později:

class Kostka:
    """
    Třída reprezentuje hrací kostku.
    """

Konstruktory

Až doposud jsme neuměli zvenčí nastavit jiné atributy než veřejné, protože soukromé atributy nejsou zvenčí viditelné. Tento problém se dá obejít pomocí komolení jmen (tzv. name mangling), ale vzhledem k zapouzdření to není dobrá praktika. Již jsme si říkali něco málo o konstruktoru objektu. Je to metoda, která se zavolá ve chvíli vytvoření instance objektu. Slouží samozřejmě k nastavení vnitřního stavu objektu a k provedení případné inicializace. Kostku nyní vytvoříme takto:

kostka = Kostka()

Právě Kostka() je konstruktor. Protože v naší třídě žádný není, Python si dogeneruje prázdnou metodu. My si však nyní konstruktor do třídy přidáme. Deklaruje se jako metoda. V Pythonu můžeme použít metody hned dvě. Metodu __new__() a metodu __init__(). Ta první se volá při vytváření objektu, ale většinou si vystačíme se druhou metodu, která se volá při inicializaci objektu. Metodu konstruktoru budeme mí prázdnou. Samozřejmě jako první parametr píšeme self. Do metody vložíme další klíčové slovo pass, které Pythonu říká, aby nic nedělal. Pokud by tam to slovo nebylo, Python by nám vyhodil chybu, že očekává blok příkazů.

def __init__(self):
    pass

Pokud v metodě __init__ jen tak vytvoříme nějakou proměnnou, tak ta po ukončení metody zaniká. Ale my potřebujeme vytvořit atribut pocet_sten. Atributy objektů se vytvářejí všemocným slůvkem self. Za self následuje tečka a název atributu. Vytvoříme tedy veřejný atribut pocet_sten.

def __init__(self):
    self.pocet_sten = 6

Pokud kostku nyní vytvoříme, bude mít atribut pocet_sten nastaven na 6. Vypišme si počet stěn do konzole, ať vidíme, že tam hodnota opravdu je. Celý kód:

class Kostka:
    """
    Třída reprezentuje hrací kostku.
    """

    def __init__(self):
        self.pocet_sten = 6


kostka = Kostka()
print(kostka.pocet_sten)
input()

Není dobré atribut nastavit jako veřejný, protože nebudeme chtít, aby nám někdo mohl již u vytvořené kostky měnit počet stěn. Přidáme do třídy tedy metodu vrat_pocet_sten(), která nám vrátí hodnotu atributu pocet_sten a tento atribut upravíme na neveřejný. Docílíme tím v podstatě toho, že je atribut read-only (atribut není viditelný a lze ho pouze číst metodou, změnit ho nelze). Python má k tomuto účelu ještě další konstrukce, ale tím se zatím nebudeme zabývat. Upravená verze třídy i metodou:

class Kostka:
    """
    Třída reprezentuje hrací kostku.
    """

    def __init__(self):
        self.__pocet_sten = 6

    def vrat_pocet_sten(self):
        """
        Vrátí počet stěn kostky.
        """
        return self.__pocet_sten


kostka = Kostka()
print(kostka.vrat_pocet_sten())
input()

Atribut se stal soukromým přidáním dvou podtržítek. Navíc jsme změnili vypisování, jelikož hodnotu atributu zjistíme pouze zavoláním metody.

Výstup:

6

Vidíme, že se konstruktor opravdu zavolal. My bychom ale chtěli, abychom mohli u každé kostky při vytvoření specifikovat, kolik stěn budeme potřebovat. Dáme tedy konstruktoru parametr:

def __init__(self, pocet_sten):
    self.__pocet_sten = pocet_sten

Všimněte si, že názvy atributu a argumentu jsou skoro stejné. Pokud bychom měli počet stěn jako veřejný atribut, tak stejný název nevadí. Pomocí self specifikujeme, že levá proměnná pocet_sten náleží instanci, pravou Python chápe jako z parametru (argumentu). Příklad:

def __init__(self, pocet_sten):
    self.pocet_sten = pocet_sten

Ale vraťme se k původnímu kódu a zkusme si zadat parametr do konstruktoru.

kostka = Kostka(10) # v tuto chvíli se zavolá konstruktor s par. 10
print(kostka.vrat_pocet_sten())
input()

Výstup:

10

Vše funguje, jak jsme očekávali. Python nám již v tuto chvíli nevygeneruje prázdný (tzv. bezparametrický konstruktor), takže kostku bez parametru již vytvořit nelze. My to však můžeme umožnit pomocí klíčového argumentu. V něm nastavíme počet stěn na 6, protože takovou hodnotu asi uživatel naší třídy u kostky očekává jako výchozí:

def __init__(self, pocet_sten=6):
    self.__pocet_sten = pocet_sten

Zkusme si nyní vytvořit 2 instance kostky, každou jiným konstruktorem:

sestistenna = Kostka()
desetistenna = Kostka(10) #nebo můžeme mít Kostka(pocet_sten=10)
print(sestistenna.vrat_pocet_sten())
print(desetistenna.vrat_pocet_sten())
input()

Výstup:

6
10

Díky klíčovému argumentu nemusíme počet stěn zadávat. Toho můžeme využívat i u všech dalších metod, nejen u konstruktorů. Mnoho funkcí a metod v Pythonu má klíčové argumenty, např. vestavěná funkce print(). Je dobré si u metod projít jejich klíčové argumenty, abyste neprogramovali něco, co již někdo udělal před vámi. Máme tedy konstruktor, který nám umožňuje tvořit různé hrací kostky. Přejděme dál.

Náhodná čísla

Definujme na kostce metodu hod(), která nám vrátí náhodné číslo od 1 do počtu stěn. Metoda nebude mít žádný parametr. Náhodné číslo získáme tak, za pomoci modulu random. Modul si naimportujeme vnitřně (použijeme jedno podtržítko). A použijeme metodu randint().

def hod(self):
    """
    Vykoná hod kostkou a vrátí číslo od 1 do
    počtu stěn.
    """
    import random as _random
    return _random.randint(1, self.__pocet_sten)

Při importování modulů se Python podívá, jestli byl již modul importován, takže pokud modul importoval dříve, tak ho Python znovu neimportuje. Tudíž se první řádek v metodě provede jen jednou.

Překrývání metody __str__

Kostka je téměř hotová, ukažme si ještě jednu užitečnou metodu, kterou si přidáme a kterou budeme hojně používat i ve většině našich dalších objektů. Řeč je o metodě __str__, o které jsme se zatím nezmínili a kterou obsahuje každý objekt, tedy i nyní naše kostka. Metoda je určena k tomu, aby vrátila tzv. textovou reprezentaci instance. Hodí se ve všech případech, kdy si instanci potřebujeme vypsat nebo s ní pracovat jako s textem. Tuto metodu mají např. i čísla. V Pythonu funguje implicitní konverze, jakmile tedy budeme chtít do konzole vypsat číslo nebo kterýkoli jiný objekt, Python na něm zavolá metodu __str__ a vypíše její výstup. Pokud si děláme vlastní třídu, měli bychom zvážit, zda se nám takováto metoda nehodí. Nikdy bychom si neměli dělat vlastní metodu, např. něco jako vypis(), když máme v Pythonu připravenou cestu, jak toto řešit. U kostky nemá __str__ vyšší smysl, ale u bojovníka bude jistě vracet jeho jméno. My si ji ke kostce stejně přidáme, bude vypisovat, že se jedná o kostku a vrátí i počet stěn. Nejprve si zkusme vypsat do konzole naši instanci kostky:

print(sestistenna)

Do konzole se vypíše pouze cesta k naší třídě. Ačkoli je metoda již definována, můžeme ji jednoduše definovat znovu a tím ji překryjeme.

def __str__(self):
    """
    Vrací textovou reprezentaci kostky.
    """
    return str("Kostka s {0} stěnami".format(self.__pocet_sten))

Nyní opět zkusíme do konzole vypsat přímo instanci kostky.

Výstup:

Kostka s 6 stěnami

Ještě si naše kostky vyzkoušíme. Zkusíme si v programu s našima dvěma kostkami v cyklech házet a podíváme se, jestli fungují tak, jak se očekává. Upravíme konec souboru:

# vytvoření kostek
sestistenna = Kostka()
desetistenna = Kostka(10)

#hod šestistěnnou
print(sestistenna)
for _ in range(10):
    print(sestistenna.hod(), end=" ")

#hod desetistěnnou
print("\n", desetistenna, sep="")
for _ in range(10):
    print(desetistenna.hod(), end=" ")

input()

Za for následuje podtržítko, jelikož dělat něco s proměnnou v cyklu nepotřebujeme.

Výstup může vypadat nějak takto:

Objektová hrací kostka v C#

Máme hotovou docela pěknou a nastavitelnou třídu, která reprezentuje hrací kostku. Bude se nám hodit v naší aréně, ale můžete ji použít i kdekoli jinde. Vidíme, jak OOP umožňuje znovu používat komponenty.


 

Stáhnout

Staženo 155x (940 B)
Aplikace je včetně zdrojových kódů v jazyce python

 

  Aktivity (1)

Článek pro vás napsal gcx11
Avatar
(^_^)

Jak se ti líbí článek?
Celkem (4 hlasů) :
55555


 



 

 

Komentáře
Zobrazit starší komentáře (2)

Avatar
Benjibs
Člen
Avatar
Benjibs:

Ahoj,


Use __new__ when you need to control the creation of a new instance. Use __init__ when you need to control initialization of a new instance.**
Presne tak, ja "kontrolujem" vytváranie novej instancie, a to tak, že v prípade nesplnenia vstupných podmienok ju vôbec nevytvorím :)

#...
def __new__(cls, arg1, arg2):
   if arg1.skaredy():
       raise UglyArgException('Pfffff.')
       #...

Musíš uznať, že je zbytočné vytvárať objekt, (čiže vstúpiť do __init__), ak takto získaný objekt bude vďaka zlým argumentom v porušenom stave.

Editováno 10.6.2014 6:23
Odpovědět  +1 10.6.2014 6:22
1 + 1 = 2
Avatar
hanpari
Redaktor
Avatar
Odpovídá na Benjibs
hanpari:

Díky, chápu, špatně jsem si přečetl ten popis. Pochopil jsem to přesně obráceně. Takhle to dává smysl :)

 
Odpovědět 10.6.2014 7:17
Avatar
hanpari
Redaktor
Avatar
Odpovídá na Benjibs
hanpari:

Tak beru zpět. Není mi to jasné o nic víc než předtím. Z toho, co jsem vyzkoušel a následně přečetl, je můj osobní dojem takový, že do __new__ se pouštět nemá smysl. Viz můj nefunkční kód:

class Test():
        def __new__(self):
                print("new")
        def __init__(self):
                print("init")
>>>new

Tohle mne trochu překvapilo, a když jsem šel po tom hlouběji, našel jsem tuhle větu:

__new__() is intended mainly to allow subclasses of immutable types (like int, str, or tuple) to customize instance creation.

Což tak trochu odporuje tomu, jak __new__ nejspíš používáš ty. Což samozřejmě nutně nemusí znamenat, že ho používáš špatně. V každém případě bych byl rád, kdybys sem mohl hodit kompletní příklad :)

 
Odpovědět 10.6.2014 16:53
Avatar
Benjibs
Člen
Avatar
Odpovídá na hanpari
Benjibs:

Tebou uvedený kód nefunguje preto,
lebo vždy na konci metódy __new__ je potrebné dať (čo si ty neurobil):

return super(NAZOV_TRIEDY, cls).__new__(cls)

Až toto, čiže zavolanie metódy __new__ u rodičovskej triedy vytvorí tú instanciu.
Takže všetko by malo fungovať správne.

class Test(object):
        def __new__(cls):
                print("new")
                return super(Test, cls).__new__(cls)

        def __init__(self):
                print("init")
 >>>new
    init
Editováno 10.6.2014 17:50
Odpovědět 10.6.2014 17:49
1 + 1 = 2
Avatar
hanpari
Redaktor
Avatar
Odpovídá na Benjibs
hanpari:

Díky, já se pak později dočetl, že to fungovat nemůže, jenom mi není jasné, co se vlastně děje, například co se schovává pod tím parametrem cls? Už jsem pochopil, že __new__ je sdílená funkce a syntaxe super je mi také jasná. Ale když na to hledím, přijde mi, že je to uzavřená smyčka, kde si předávám totéž cls pořád do kola :)
Nevím, jestli jsem se vyjádřil dost jasně, ale jde mi o úplně první volání funkce __new__. Jaký dostává parametr?

Editováno 10.6.2014 18:01
 
Odpovědět 10.6.2014 17:58
Avatar
Benjibs
Člen
Avatar
Odpovídá na hanpari
Benjibs:

"cls" = "class", tým argumentom je samotná trieda, v tomto prípade "Test".
Nekončený cyklus to nie je. Ono to len prebuble cez metódy __new__ celej hierarchie tried a zastaví sa to u triedy "object".

Odpovědět  +1 10.6.2014 18:32
1 + 1 = 2
Avatar
hanpari
Redaktor
Avatar
Odpovídá na Benjibs
hanpari:

Díky moc. Nebudu tvrdit, že zcela chápu, proč to tak je, ale aspoň už vím, jak to případně vyluštit.

 
Odpovědět 10.6.2014 21:07
Avatar
Pyvo
Člen
Avatar
Pyvo:

Ahoj,

Jsem programátor nováček. Mohl by mi někdo prosím vysvětlit jak přesně funguje sestistenna.hod()?

Metoda hod() generuje náhodná čísla od 1 do počtu stěn kostky. Pokud to dobře chápu tak metoda říká, co má instance dělat. Ale hod() neříká co má dělat, pouze generuje čísla. Takže výsledek je třeba něco jako sestistenna.4. Chápu to dobře?

 
Odpovědět 3.2.2015 9:15
Avatar
David.Landa
Člen
Avatar
David.Landa:

Python nemá privátní atributy ani metody!

class A(object):

    def __private(self):
        print("--private--")

a = A()
print(A.__dict__.keys()) #>> slovník
a._A__private()
Editováno 3.2.2015 12:20
 
Odpovědět 3.2.2015 12:17
Avatar
hanpari
Redaktor
Avatar
Odpovídá na Pyvo
hanpari:

Metoda bude dělat to, co jí řekneš, aby dělala. V tomto případě využívá znalost vnitřního stavu kostky, tj. kolik má kostka stěn.

pro 10-stěnnou kostku bude vědět, že má generovat od 1 do 10, pro šestistěnnou zase 1 do 6.

 
Odpovědět 3.2.2015 12:23
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 12. Zobrazit vše