Začínající programátoři v Pythonu, kteří rovnou naskočí do objektově orientovaného programování, mívají potíž s chápáním toho, co je to “třída”, proč by je měli používat, jaký je rozdíl mezi funkcí a metodou, a co vlastně znamená to podivné self
. Zde si ukážeme 4 verze téhož programu, z nichž by měly být rozdíly a podobnosti poměrně zřejmé.
Zkusíme ukázat různé verze řešení následující úlohy: Vytvořte program, který bude umět vypočítat objem a povrch kvádru.
Víme, že kvádr je definován délkami svých 3 stran. Kvádr tedy budeme reprezentovat pomocí 3 proměnných, a
, b
a c
. Pro výpočet objemu a povrchu vytvoříme obyčejné funkce compute_cuboid_volume(a, b, c)
a compute_cuboid_surface(a, b, c)
. Celý program může vypadat asi takto:
def compute_cuboid_volume(a, b, c): return a*b*c def compute_cuboid_surface(a, b, c): return 2 * (a*b + a*c + b*c) if __name__ == '__main__': side_a, side_b, side_c = 5, 4, 3 print(compute_cuboid_volume(side_a, side_b, side_c)) print(compute_cuboid_surface(side_a, side_b, side_c))
Poznámka: Tento přístup, tedy programování bez vlastních tříd, je v praxi velmi obvyklý, hlavně u jednodušších skriptů, kde si vystačíme s vestavěnými datovými typy.
side_a
než jen a
.
compute_cuboid_surface
, a ne jen třeba compute_surface
. Pokud bychom totiž chtěli přidat jiné geometrické objekty (kvádr, kouli, …), chtěli bychom asi definovat i funkce compute_surface
pro tyto objekty, což by nešlo, jméno by bylo už obsazené.
side_a
až side_c
k sobě pojí jen podobné jméno, ale jsou to nezávislé proměnné. Pouze všechny tři dohromady definují kvádr, ale ve skutečnosti je k sobě nic nepojí.
Zkusíme nejprve odstranit poslední zmíněnou nevýhodu předchozího řešení (proměnné side_a
až side_c
k sobě nic nepojí). Vytvoříme si třídu Cuboid
, která v tuto chvíli bude sloužit jen jako kontajner pro uložení délek stran kvádru (bude obsahovat jen data, nebude mít žádné chování). Vstupem funkcí pro výpočet objemu a povrchu pak nebude trojice stran, ale kvádr, který bude obsahovat délky svých stran.
class Cuboid: def __init__(self, a, b, c): self.a = a self.b = b self.c = c def compute_cuboid_volume(cuboid): return cuboid.a*cuboid.b*cuboid.c def compute_cuboid_surface(cuboid): return 2 * (cuboid.a*cuboid.b + cuboid.a*cuboid.c + cuboid.b*cuboid.c) if __name__ == '__main__': cuboid = Cuboid(5, 4, 3) print(compute_cuboid_volume(cuboid)) print(compute_cuboid_surface(cuboid))
Poznámka 1: Slouží-li třída jen a především jako kontajner na data, doporučujeme použít anotaci @dataclass a příslušně změnit definici třídy.
Poznámka 2: Kombinace vlastních objektů, které slouží jen jako kontajnery pro data, a obyčejných funkcí, které s nimi pracují, je v praxi taky poměrně obvyklá, především v případě, kdy není jasné, se kterou třídou daná funkce souvisí. Ale pokud jsou funkce zřetelně svázané s nějakou třídou, dává dobrá smysl z nich udělat metody, viz. řešení 3 a 4. (A to lze udělat i v případě anotace @dataclass.)
Cuboid
, která obaluje délky stran tak, že s nimi můžeme pracovat jako s jedním celkem, kvádrem.
Cuboid
je možné použít kratší jména, protože je jasné, že členské proměnné a
, b
a c
se vztahují (patří) přímo k danému kvádru.
Cuboid
, je třeba k nim přitupovat mírně složitějším způsobem: prostřednictvím self.a
uvnitř definice třídy, a prostřednictvím (např.) cuboid.a
mimo třídu Cuboid
.
compute_cuboid_surface
, a ne jen třeba compute_surface
…
Funkce compute_cuboid_volume
a compute_cuboid_surface
nejsou obecné, jsou logicky svázané s třídou Cuboid
, a správně fungují jen tehdy, pokud jako svůj argument dostanou instanci třídy Cuboid
. Pokud jim předáte argument jiného typu, např. Sphere
, nejspíš selžou s chybou. Dává proto dobrý smysl, aby i tyto funkce byly vnořeny do jmenného prostoru třídy Cuboid
.
Zkusme to udělat nejprve tím nejnaivnějším a nejjednodušším způsobem: ve zdrojovém kódu prostě zvýšíme odsazení obou funkcí, čímž se z nich stanou metody třídy Cuboid
. Jména těchto metod se stanou součástí jmenného prostoru této třídy, a proto k nim tak i musíme přistupovat: místo prostého compute_cuboid_volume(cuboid)
budeme teď volat Cuboid.compute_cuboid_volume(cuboid)
:
class Cuboid: def __init__(self, a, b, c): self.a = a self.b = b self.c = c def compute_cuboid_volume(cuboid): return cuboid.a*cuboid.b*cuboid.c def compute_cuboid_surface(cuboid): return 2 * (cuboid.a*cuboid.b + cuboid.a*cuboid.c + cuboid.b*cuboid.c) if __name__ == '__main__': cuboid = Cuboid(5, 4, 3) print(Cuboid.compute_cuboid_volume(cuboid)) print(Cuboid.compute_cuboid_surface(cuboid))
Poznámka: Toto řešení je sice zcela funkční, ale v praxi na něj nenarazíte. Nesnažte se jej ani napodobovat. Zde je uvedeno jen proto, aby bylo zřejmé, že metody nejsou nic jiného, než funkce zanořené do jmenného prostoru třídy, takže jediná další věc, která se v kódu musí změnit, je způsob jejich volání.
Cuboid
, nyní obsahuje nejen data (délky stran kvádru), ale i operace (metody), co s těmito daty lze dělat.
Cuboid
, a proto je možné, ba dokonce vhodné jejich dlouhá jména compute_cuboid_xxx
zkrátit na prosté compute_xxx
.
Cuboid.compute_cuboid_volume(cuboid)
je poněkud přecuboid
ováno. I pokud zkrátíme jméno metody, jak bylo navrženo v předchozím bodu, vypadalo by volání Cuboid.compute_volume(cuboid)
.
V předchozím řešení provedeme ještě 3 změny:
compute_volume()
a compute_surface()
.
cuboid
na self
. Tuto změnu bychom dělat nemuseli. Ale existuje nepsaná dohoda mezi Pythonisty, že první parametr metod se bude vždy jmenovat self
. Dává to smysl, protože první parametr metody (proměnná self
) vždy obsahuje právě tu konkrétní instanci třídy (zde nějaký konkrétní kvádr), jejíž data chceme k výpočtu použít.
compute_xxx()
. Python nabízí zjednodušenou syntaxi (syntactic sugar): místo Cuboid.compute_volume(cuboid)
lze psát prostě cuboid.compute_volume()
.
class Cuboid: def __init__(self, a, b, c): self.a = a self.b = b self.c = c def compute_volume(self): return self.a*self.b*self.c def compute_surface(self): return 2 * (self.a*self.b + self.a*self.c + self.b*self.c) if __name__ == '__main__': cuboid = Cuboid(5, 4, 3) print(cuboid.compute_volume()) print(cuboid.compute_surface())
Poznámka: Toto je typická ukázka objektově orientovaného kódu, kdy třída (nový datový typ) obsahuje jak data, která definují jednotlivé instance/objekty, tak i obecné operace, které je možné se všemi objekty daného typu dělat.
Výsledné řešení
compute_xxx
do jmenného prostoru třídy je možné použít stejná jména i pro jiné třídy (jiné typy geometrických objektů), např. sphere.compute_volume()
nebo cube.compute_volume()
.
Ono podivné self
není nic jiného než lokální proměnná uvnitř metody, obsahující instanci třídy, se kterou má metoda právě pracovat. Ve třetím, “naivním” řešení se tato proměnná jmenovala jinak (cuboid
) a vše fungovalo zcela stejně.