Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Třída

Když vyvíjíme aplikaci nebo nějaký program, je občas úkolem programátora i modelovat data z reálného světa a definovat vztahy, které mezi nimi existují. V této kapitole si ukážeme, jak na to, a také zavedeme základy objektově orientovaného programování (OOP) v Pythonu.

Přenesme se do pozice vývojáře, který dostal na starost vytvořit systém pro psí útulek. Pro každého pejska si budeme chtít pamatovat jeho jméno a věk. Jak bychom tato data mohli uchovávat? Jednou možností je si data o jednom pejskovi uložit ve slovníku, a tyto slovníky následně v nějakém seznamu. Mohlo by to vypadat třeba takto:

pejsci = [
    {
        "name": "Azor",
        "age": 3
    },
    {
        "name": "Alík",
        "age": 4
    }
]

To by fungovalo, ale je to krkolomné. Pokaždé, co chceme vytvořit nového pejska, musíme vytvořit nový slovník s daty pejska. Důležitější však je, že tyto slovníky mohou v podstatě popisovat cokoli jiného – třeba kočku, ale i (poměrně zvláštně pojmenovaného) člověka. Chtěli bychom přímo v kódu popsat to, že nějaká data reprezentují právě psa.

Vytvoříme tedy třídu: tu si můžeme představit jako šablonu pro vytvářené objekty. Každý objekt psa bude obsahovat jméno a věk (a další, uvidíme zanedlouho); těm se říká atributy (anglicky attribute) nebo datové položky.

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Vytvořili jsme šablonu pro vytváření nových pejsků, kde si u každého budeme pamatovat jméno a věk. Jistě se ptáte – proč jsme vě třídě nadefinovali funkci se jménem __init__? Velkou výhodou tříd je, že můžeme definovat funkce, které můžeme volat jen a pouze na objektech, které pomocí třídy vytvoříme. Funkce __init__ je ale speciální funkce: zavolá se totiž tehdy, kdy pomocí třídy objekt vytváříme. Vytvořme tedy nového pejska a podívejme se, co funkce __init__ udělá.

pes1 = Pes("Azor", 3)
pes2 = Pes("Alík", 4)

Vytvořili jsme dva pejsky, Azora a Alíka. Po zavolání Pes("Azor", 3) Python vytvoří nový objekt (říkáme též instanci třídy) podle šablony (třídy) Pes. Poté, na prázném objektu Pes zavolá funkci __init__ z dané třídy, která vyplní prázdná pole name a age předanými hodnotami. Této funkci říkáme konstruktor.

Pokud bychom chtěli zjistit jméno a věk nějakého psa, můžeme toho docílit pomocí pes1.name nebo pes1.age.

Chceme nyní napsat funkci stekni, které předáme objekt nějakého psa a vypíše jeho jméno a text “haf”. Rozhodne bychom mohli tuto funkci napsat a volat takto:

def stekni(pes):
    print(f"{pes.name}: HAF!")

stekni(pes1)
stekni(pes2)

To ale není pohodlné, protože funkce existuje mimo definici třídy, tedy může být úplně na druhém konci souboru. To lze vyřešit rozšířením definice třídy právě o funkci stekni. Celá třída pak bude vypadat takto:

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def stekni(self):
        print(f"{self.name}: HAF!")


pes1 = Pes("Azor", 3)
pes2 = Pes("Alík", 4)

pes1.stekni()
pes2.stekni()

Argument pes jsme přejmenovali na self, a to čistě z konvence: dáváme tím jasně najevo, že čteme objekt, na kterém jsme funkci zavolali.

Funkce, které definujeme na třídách, budeme odteď nazývat metody (anglicky method).

Tímto jsme popsali část první důležité vlastnosti, kterou nám třídy nabízí – zapouzdření (anglicky encapsulation). Všechny datové položky (jméno, věk) a metody jsou zabalené do jedné logické jednotky (třídy). K zapouzdření se ještě vrátíme v některé z dalších kapitol.


Podívejme se na další důležitou vlastnost tříd – dědičnost (anglicky inheritance). Ta nám umožňuje určit “předka” třídy, ze které třída nová zdědí všechny atributy a metody. Vytvoříme novou třídu Stene, která bude dědit ze třídy Pes:

class Pes:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def stekni(self):
        print(f"{self.name}: HAF!")

class Stene(Pes):
    ...

s1 = Stene("Azor", 3)
s1.stekni()

Třídu Stene teď můžeme používat úplně stejně, jako třídu Pes. Ve třídě Stene uděláme úpravu, a to přidání nového atribudu training_level.

class Stene(Pes):
    def __init__(self, name, age, training_level):
        super().__init__(name, age)

        self.training_level = training_level

s1 = Stene("Azor", 3, 0)
s1.stekni()

Lehce krkolomná konstrukce super().__init__(...) pouze volá konstruktor předka (kterého získáme pomocí super()). Ve třídě, která dědí z nějaké třídy, se vždy můžeme na předka odkázat. Zavolat konstruktor předka je důležité; kdybychom tak neučinili, atributy ze třídy Pes by se neinicializovaly a neměly by hodnotu.

Teď bychom chtěli změnit chování metody stekni v třídě Stene – stěňata přeci štěkají slaběji. Vytvoříme novou verzi metody stekni v třídě Stene:

class Stene(Pes):
    def __init__(self, name, age, training_level):
        super().__init__(name, age)

        self.training_level = training_level

    def stekni(self):
        print(f"{self.name}: haf.")

Tímto jsme přepsali metodu stekni ve třídě Stene, tradičně se tomuto řiká anglickým slovem override. Všechna štěňata odteď budou štěkat slabějším hláskem, než dospělí psi.

K čemu to vlastně je? Představme si, že máme pole psů a štěňat – tedy náš útulek. Pro každé zvíře bychom chtěli zavolat metodu stekni.

utulek = [
    Stene("Azor", 3, 0),
    Pes("Bella", 5),
    Stene("Cyril", 2, 1),
    Pes("Diana", 6)
]

for zvire in utulek:
    zvire.stekni()

Zde se na chvíli zastavíme: procházíme pole utulek a na každém prvku voláme metodu stekni, i když jsou ale prvky rozdílného typu (některé jsou instance třídy Stene, jiné instance třídy Pes). To je ale v pořádku, protože víme, že obě třídy obsahují metodu stekni.


V posledním odstavci jsme se setkali se třetí a poslední důležitou vlastností tříd, které říkáme polymorfismus. Do češtiny bychom toto slovo mohli přeložit jako mnohotvárnost. Říká nám, že třídy, které dědí z nějaké třídy (předka), mohou být použity na stejných místech, jako předek samotný.

To dává smysl, protože děděním můžeme vlastnosti a metody pouze přidávat nebo upravovat, nikoli mazat. I když se mohou metody chovat jinak, kontrakt (tedy definice metody ve třídě) je stále stejný, a můžeme ho bezpečně volat.