Search
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é.
self
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:
a
b
c
compute_cuboid_volume(a, b, c)
compute_cuboid_surface(a, b, c)
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
compute_cuboid_surface
compute_surface
side_c
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.
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(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.)
self.a
cuboid.a
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.
compute_cuboid_volume
Sphere
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):
compute_cuboid_volume(cuboid)
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í.
compute_cuboid_xxx
compute_xxx
cuboid
Cuboid.compute_volume(cuboid)
V předchozím řešení provedeme ještě 3 změny:
compute_volume()
compute_surface()
compute_xxx()
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í
sphere.compute_volume()
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ě.