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_aside_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_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í

  • 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řecuboidová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ě.

courses/b4b33rph/tutorialy/python/class_self.txt · Last modified: 2024/09/17 09:28 by xposik