====== Třídy, objekty, "self" ====== 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é. ===== Zadání úlohy ===== 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.** ===== 1. Řešení zcela bez objektů ===== 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. ==== Výhody řešení ==== * Krátký, jednoduchý a přehledný kód. ==== Nevýhody řešení ==== * Protože proměnné obsahující délky stran jsou definované v hlavním jmenném prostoru, je dobré, aby měly dobře popisná jména, proto raději ''side_a'' než jen ''a''. * Obě funkce jsou také definované v hlavním jmenném prostoru, a (nejen) proto mají dlouhá jména typu ''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é. * Proměnné ''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í. ===== 2. Řešení pomocí funkcí pracujících s objekty ===== 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 [[https://docs.python.org/3/library/dataclasses.html|@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 [[https://docs.python.org/3/library/dataclasses.html|@dataclass]].) ==== Výhody řešení ==== * Vytvořili jsme nový datový typ, třídu ''Cuboid'', která obaluje délky stran tak, že s nimi můžeme pracovat jako s jedním celkem, kvádrem. * Uvnitř třídy ''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. ==== Nevýhody řešení ==== * Protože jsme délky stran "zanořili" do třídy ''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''. * Obě funkce jsou stále definované v hlavním jmenném prostoru, a proto je vhodné aby měly dlouhá jména typu ''compute_cuboid_surface'', a ne jen třeba ''compute_surface''... ===== 3. Naivní objektové řešení ===== 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í. ==== Výhody řešení ==== * Náš datový typ, třída ''Cuboid'', nyní obsahuje nejen data (délky stran kvádru), ale i operace (metody), co s těmito daty lze dělat. ==== Nevýhody řešení ==== * Z funkcí se staly metody, které už nejsou definované v hlavním jmenném prostoru. Je u nich jasné, že patří k třídě ''Cuboid'', a proto je možné, ba dokonce vhodné jejich dlouhá jména ''compute_cuboid_xxx'' zkrátit na prosté ''compute_xxx''. * Ve volání ''Cuboid.compute_cuboid_volume(cuboid)'' je poněkud pře''cuboid''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)''. ===== 4. Typické objektové řešení ===== V předchozím řešení provedeme ještě 3 změny: * Zestručníme jména metod na ''compute_volume()'' a ''compute_surface()''. * Jméno parametru obou metod změníme z ''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. * Změníme způsob volání metod ''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í * sdružuje/obaluje data, která definují konkrétní instanci třídy (zde strany kvádru), a operace, které s instancemi dané třídy lze provádět (zde počítat objem a povrch kvádru); * umožňuje snadno vytvořit objekt (instanci třídy) a volat jeho metody způsobem, který není náročnější na místo, než volání vhodně pojmenovaných funkcí z prvního řešení; * díky zanoření jmen ''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ě.