PyLadies Brno

Jestli jsi už naprogramovala hru Klondike, ale ve funkci popis_karty nepoužíváš slovníky, projdi si napřed tenhle návod.

OOP

Zatím jsme programovaly v procedurálním stylu: měly jsme nějaká data (seznamy, slovníky, řetězce, čísla), a nějaké funkce které s těmito daty pracovaly. Teď se podíváme na jiný styl: objektově orientované programování (angl. object-oriented programming, OOP). To je založené na objektech: strukturách, které obsahují jak informace (data), tak funkce (metody), které s těmi daty umí pracovat.

V Pythonu je všechno objekt. Přesněji řečeno: všechno, co můžeme uložit do Pythoní proměnné – každá hodnota – je objekt. Čísla, řetězce, funkce, seznamy, soubory, metody, moduly, třídy, matice – všechno jsou objekty.

Třeba takový řetězec obsahuje jak informace (seznam znaků, ze kterých je složený), tak spoustu funkcionality – metody jako lower(), split() nebo count(). Všechno je obsaženo v jednom objektu; samotný řetězec je všechno, co potřebuješ k tomu, abys ho převedla na malá písmena.

Ale i když je všechno objekt, je možné neprogramovat „objektově”.

Ve hře Klondike jsme karty reprezentovaly jako trojice (hodnota, barva, licem_nahoru), a funkce jako popis_karty a otoc_kartu byly zvlášť. Data v jednoduchých strukturách (n-tice, čísla, řetězce), a funkce, které s nimi pracují – to je procedurální přístup k programování.

Jak by se pracovalo s kartou, kdybychom z ní udělaly specializovaný objekt?

karta = Karta(3, 'Sr', licem_nahoru=True)
print(karta.hodnota)        # → 3
print(karta.barva)          # → 'Sr'
print(karta.licem_nahoru)   # → True
print(karta)                # → [3 ♥]
karta.otoc_licem_dolu()
print(karta)                # → [???]
karta.otoc_licem_nahoru()
print(karta)                # → [3 ♥]

Každý objekt v Pythonu má svůj typ (angl. type). Zatím jsme poznaly pár základních, vestavěných (angl. built-in) typů jako str, int, list, bool. Jméno třídy funguje zároveň jako „funkce”, která vytváří nový objekt daného typu.

Typy, které vytvoříme samy (t.j. nejsou vestavěné) se většinou pojmenovávají s velkým písmenem na začátku každého slova, bez oddělení slov: např. Tabulka, VesmirnaLod, pyglet.text.Label. (Je to konvence, kterou některé knihovny z různých důvodů nepoužívají, ale my se jí budeme držet.)

Třída pro kartu se bude jmenovat Karta. Na vytvoření karty potřebujeme tři kousky informací – hodnotu, barvu, a otočení – které se „zabudují” do objektu. Z objektu pak budou přístupné jako atributy, pomocí tečky: podobně jako metodu "upper" pro řetězec "abc" dostaneme výrazem "abc".upper, hodnotu karty dostaneme výrazem karta.hodnota.

Naše kartové objekty taky půjdou převést na řetězec (a tím pádem vypsat pomocí print), a budou mít metodu otoc().

Třídy

Každý objekt má – jak už bylo řečeno – dvě součásti: nějaká data specifická pro ten daný objekt, a nějakou funkcionalitu – popis chování, které je většinou společné všem objektům daného typu.

Každá karta má svoji vlastní hodnotu nebo barvu, ale postup otočení karty, nebo převodu na řetězec, či vytvoření karty, je společný všem kartám.

To, co je společné pro všechny objekty nějakého typu, definuje třída (angl. class). Třídy se vytváří příkazem class, ke kterému se napíše jméno třídy, a pak, v odsazeném bloku, popis chování.

První, co popíšeme, je jak se objekt Karta vytváří. Na to se používá trochu zvláštně pojmenovaná speciální metoda (angl. special method), __init__ (s dvojitými podtržítky na začátku a na konci):

class Karta:
    def __init__(self, hodnota, barva, licem_nahoru):
        self.hodnota = hodnota
        self.barva = barva
        self.licem_nahoru = licem_nahoru

Když napíšeš karta = Karta(3, 'Sr', licem_nahoru=True), zavolá se funkce __init__ se čtyřmi argumenty: vznikajícím objektem (který se tradičně pojmenovává self), a pak s těmi ostatními, které byly předány přímo. Funkce __init__ pak nastaví na vznikajícím objektu (self) tři atributy – stejně jako seznam[2] = 'xyz' nastaví seznam[2], tak karta.hodnota = 8 nastaví karta.hodnota.

Vyzkoušej si, že funguje:

karta = Karta(3, 'Sr', licem_nahoru=True)
print(karta.hodnota)        # → 3
print(karta.barva)          # → 'Sr'
print(karta.licem_nahoru)   # → True

Další speciální metoda je __str__, která převádí na řetězec. Tuhle metodu používají funkce jako str(karta) nebo print(karta). Jako jediný argument bere objekt (kartu), který převádí; opět se tradičně používá jméno self. Bude vypadat nějak takhle (přidej ji do bloku class Karta):

    def __str__(self):
        if not self.licem_nahoru:
            return '[???]'

        barvy = {'Pi': '♠ ', 'Sr': ' ♥', 'Ka': ' ♦', 'Kr':'♣ '}
        znak_barvy = barvy[self.barva]

        hodnoty = {1: 'A', 10: 'X', 11: 'J', 12: 'Q', 13: 'K'}
        znak_hodnoty = hodnoty.get(self.hodnota, str(self.hodnota))

        return '[{}{}]'.format(znak_hodnoty, znak_barvy)

Vyzkoušej si, že to funguje:

karta = Karta(3, 'Sr', licem_nahoru=True)
print(karta)                # → [3 ♥]

Speciálních metod existuje víc, ale naprostá většina tříd si vystačí s těmito dvěma. Ostatní chování se dá napsat jako normální metody – takové, které namají ve jménech dvojitá podtržítka. Pořád ale budou nappsané v definici třídy, a jako první argument budou brát self, „svůj” objekt:

    def otoc_licem_nahoru(self):
        self.licem_nahoru = True

    def otoc_licem_dolu(self):
        self.licem_nahoru = False

Při volání metody se argument self doplní automaticky, podle toho „na jakém objektu” metodu zavoláme. Vyzkoušej si to:

karta = Karta(3, 'Sr', licem_nahoru=True)
print(karta)                # → [3 ♥]
karta.otoc_licem_dolu()
print(karta)                # → [???]
karta.otoc_licem_nahoru()
print(karta)                # → [3 ♥]

A k čemu to je?

S objektem se většinou pracuje trochu líp než s n-ticí. Pamatuješ na všechny řádky typu hodnota, barva, licem_nahoru = karta, u kterých je potřeba si nesplést pořadí jednotlivých prvků? U objektů si stačí zapamatovat jména atributů.

Další zjednodušení bude znát, až budeš mít v programu více podobných typů objektů. Složitější hra může mít místo funkcí otoc_kartu a otoc_zeton dva typy věcí, každý s metodou otoc.*

Ne vždycky je dobré používat vlastní třídy objektů. Nevýhoda je hlavně v tom, že se definují složitěji než jednoduché datové struktury. Na jednoduché věci, např. balíček karet či bod v prostoru, je často vhodnější požívat základní typy (seznam, resp. trojice čísel – x, y, z souřadnic).

Další věc, kterou typy umožňují, je dědičnost – ale o tom zase příště.


* třetí varianta je napsat funkci otoc_cokoli:

def otoc_cokoli(neco):
    if len(neco) == 3:  # karta má tři prvky: hodnotu, barvu, otočení
        return otoc_kartu(neco)
    else:  # žeton má jen dva prvky: jméno a otočení
        return otoc_zeton(neco)

Podobný kód nikdy nepiš. Do takovéhle funkce se složitě přidávají další případy, špatně se to testuje, složitěji se mění (vylepšuje!) struktura programu. Když se přistihneš, že podobnou funkci píšeš, je čas přejít na třídy a metody.