Jestli jsi už naprogramovala hru Klondike,
ale ve funkci popis_karty
nepoužíváš slovníky,
projdi si napřed tenhle návod.
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()
.
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 ♥]
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.