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.