Table of Contents

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í

Nevýhody řešení

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_aside_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.)

Výhody řešení

Nevýhody řešení

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í

Nevýhody řešení

4. Typické objektové řešení

V předchozím řešení provedeme ještě 3 změny:

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í

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ě.