Objektově orientované programování
Co je to objektově-orientované programování?¶
Pro objektové (...ě-orientové) programování (OOP) je zásadní koncept objektu -- ten představuje ucelenou kombinaci dat a operací, jež s nimi jdou dělat. Objekt obvykle (ne vždy) data schovává a navenek poskytuje jen několik přístupných operací, tzv. metod. Přitom typicky toho, kdo k objektu přistupuje, nezajímá implementace oněch metod (objekt klidně jejich vykonání může delegovat na jiné objekty), ani to, jakým způsobem jsou v objektu uspořádána data.
K objektově-orientovanému programování patří i několik dalších konceptů, různé programovací jazyky si z nich berou jen některé, také je interpretují různě. Ukážeme si, které koncepty mají v Pythonu smysl a jak je využít.
Obvyklé koncepty: zapouzdření, dědičnost, polymorfismus
Objekty¶
V Pythonu je všechno objekt (na rozdíl od C++, Javy). Objektem jsou všechny hodnoty vestavěných typů (čísla, řetězce, ...), všechny kontejnery, i funkce, moduly, i typy objektů. Úplně vše poskytuje nějaké veřejné metody.
Typ objektu¶
Každý objekt má nějaký typ, přičemž typy rozdělujeme na vestavěné typy (list, tuple, int, ...) a třídy (typy definované pomocí klíčového slova class
). Typ určuje, jaké metody objekt nabízí, představuje jakousi šablonu (obecné vlastnosti), od které se pak individuální objekt liší vnitřním stavem (konkrétní vlastnosti). Říkáme, že objekt je instancí daného typu. Pro zjištění typu objektu zlouží vestavěná funkce type
.
print(type("Babička"))
print(type(46878678676848648486)) # long v Pythonu 2, int v Pythonu 3
print(type(list())) # instance typu list
print(type(list)) # samotný typ list
print(isinstance(list(), list)) # Funkce "isinstance" kontroluje typ objektu (včetně dědičnosti)
Vytvoření instance¶
Pokud máme datový typ (třídu), jeho instanci vytvoříme podobně, jako kdybychom jej chtěli zavolat, tj. pomocí kulatých závorek. Ostatně, to už jsme dělali u vestavěných typů typu tuple, dict, list.
Fakticky se přitom volá konstruktor (viz níže).
objekt = list() # Vytvoření nové instance typu "list"
objekt2 = list # Toto není vytvoření instance! Jen nové jméno pro tentýž typ
# Podíváme se, co jsme dostali
print(f"objekt = {objekt}")
print(f"objekt2 = {objekt2}")
objekt2()
Metody - použití¶
Metoda objektu je funkce, která je s nějakým objektem svázaná (bez něj nemá význam) a operuje s jeho daty. Může také měnit vnitřní stav objektu, tj. hodnoty atributů.
Volání metod se v Pythonu provádí pomocí tečkového zápisu, tj. mezi objekt a volanou metodu se vloží tečka: objekt.metoda(argumenty)
objekt = [45, 46, 47, 48] # Seznam je objekt, "list" je typ
objekt.append(49) # voláme metodu "append" na seznam
objekt
Metoda append
nemá význam sama o sobě, pouze ve spojení s konkrétním seznamem - přidává do seznamu nový prvek.
Třídy¶
Třídou je jakýkoliv uživatelský typ. Podobně jako vestavěné typy nabízí metody a data (atributy), ovšem můžeme je libovolně definovat.
Nejjednodušší definice prázdné třídy (klíčové slůvko pass
slouží pro prázdné třídy a metody - obchází nevýhody odsazování):
class Trida: # Třída se bude jmenovat Trida (velké písmeno je zvyk, ne nutnost)
pass # Třída bude prázdná
Definice metody¶
Definice metody musí být uvnitř bloku třídy. (Pozn. Metody lze do třídy přidat i později, ale není to preferovaný způsob.)
Běžné metody (instance method) se volají na konkrétním objektu. Kromě nich existují i tzv. metody třídy a statické metody, které zde nebudeme probírat.
Zvláštnost (Pozn. Ano, je to opravdu divné.) definice metod (narozdíl od C++, Javy a dalších jazyků) je ta, že první argument metody je objekt, na kterém je metoda volána. Bez toho by metoda vůbec nevěděla, se kterým objektem pracuje! Dle konvence (která se snad nikdy neporušuje) se tento argument nazývá self. Při volání metody se pak vynechává a Python jej automaticky doplní.
Konstruktor¶
Metoda, která inicializuje objekt - zavolá se na prázdném objektu ve chvíli, kdy vytvoříme novou instanci. Můžeme jej definovat, ale nemusíme - v takovém případě se použije výchozí konstruktor, který jednoduše nedělá nic (zvláštního). Konstruktor se v Pythonu vždy jmenuje __init__ (dvě podtržítka před i po).
Data¶
Python příliš nerozlišuje mezi metodami a daty (tak jako obecně u proměnných -- vše je objekt). Pro něj je vše atribut daného objektu. Nastavení hodnoty se provádí podobně jako ukládání do proměnné, ale musíme přidat objekt a tečku. (Pozn. Interně jsou atributy uložené ve slovnících a při přístupu k nim se prochází slovník samotného objektu, jeho třídy, jejích nadřazených tříd, ...). Atribut daného jména nemusí přitom vůbec existovat, nemusí se nijak deklarovat.
class Auto:
def __init__(self, spotreba): # Konstruktor s argumentem
self.spotreba = spotreba # Argument uložíme jako atribut objektu (objekt je zde self)
def ujed(self, vzdalenost): # pozor - první argument metody je vždy "self"
# Zde použiji atribut "spotreba"
spotrebovane_litry = vzdalenost / 100 * self.spotreba
# spotrebovane_litry je lokální proměnná, nikoliv atribut
print(f"Jedu {vzdalenost} kilometru a spotrebuju {spotrebovane_litry} litru benzinu.")
auto = Auto(7.5) # Vytvoření instance třídy Auto se spotřebou 7.5
auto.ujed(100) # Při volání se *self* už nepíše
auto.ujed(auto, 100) # Chyba! Všimněte si, na jaký počet argumentů si Python stěžuje.
rachotina = Auto(15)
print(f"Moje rachotina má spotřebu {rachotina.spotreba} l/100 km.")
rachotina.ujed(150)
Seznam všech atributů získáme pomocí dir
# My zde filtrujeme na běžné atributy (ty s podtržítky jsou vždy něčím výjimečné)
", ".join(item for item in dir(rachotina) if not item.startswith("_"))
Cvičení¶
Rozšiřte třídu Auto
o správu najetých kilometrů. Měli bychom tedy mít možnost u auto:
- Podívat se na celkový počet najetých kilometrů.
- Zjistit celkovou spotřebu paliva.
Bonus: Přidejte ještě podporu servisních intervalů.
- Konstruktor bude mít parametr
servisni_interval
s výchozí hodnotou 20 000. - Přidejte metodu
je_potreba_servis
, která bude vracetTrue
neboFalse
podle toho, zda je potřeba servis. - Přidejte metodu
proved_servis
, která zaznamená provedení servisu. - Rozšiřte metodu
ujed
, aby hlásila potřebu servisu 1000 km předem.
Vlastnosti (properties)¶
Vlastnosti jsou "chytřejší" data. Umožnují vstoupit do procesu čtení nebo nastavování atributu. Hodí se to například tehdy, pokud objekt má několik navzájem závislých parametrů a my je nechceme ukládat nezávisle; pokud chceme kontrolovat, jaká hodnota se ukládá; či pokud chceme s ukládanou nebo čtenou hodnotou ještě něco zajímavého provést (viz příklad pro kruh).
Ze syntaktického hlediska musíme nejdříve definovat metodu, která nese jméno vlastnosti a která tuto vlastnost "čte" (resp. vrací její hodnotu). O řádek výše musíme umístit tzv. dekorátor (tento koncept teď nebudeme podrobně vysvětlovat, jen jej pasivně použijeme) @property. Chceme-li, můžeme pak vytvořit i metodu pro zápis - ta se musí jmenovat stejně, požadovat jeden argument (ukládaná hodnota) a být uvedena dekorátorem @jmenovlastnosti.setter. Podobně bychom mohli vytvořit i metodu pro mazání (dekorátor @jmenovlastnosti.deleter), ale to se již běžně nedělá.
Jakmile máme takto vytvořené vlastnosti, přistupujeme k nim jako k běžným datovým atributům - voláme je bez závorek a přiřazujeme do nich pomocí znaménka "rovná se".
Pozn. Vlastnosti fungují podobně jako properties v C# či javabeans v Javě. Povšimněte si však, že pro přístup k vlastnostem se používá úplně stejný zápis jako pro přístup k datovým atributům. Pokud tedy budeme chtít někdy změnit chování datového atributu a udělat z něj vlastnost, klient naší třídy to nepozná a nebude muset dělat žádné změny v kódu. Není tedy vhodné přespříliš iniciativně vytvářet triviální vlastnosti, které jen obalují přístup k atributům (jako by se to jistě dělalo v Javě).
import math
import numbers
class Kruh:
def __init__(self, r):
self.polomer = r
@property # Chceme definovat vlastnost pro čtení
def obsah(self): # Vypadá jako obyčejná metoda
return math.pi * self.polomer ** 2
@obsah.setter # Chceme nastavit zápis do dříve definované vlastnosti
def obsah(self, s):
print("Měním obsah na {}".format(s))
if not isinstance(s, numbers.Number): # Kontrolujeme, že jde o číslo
raise TypeError("Obsah musi byt cislo")
self.polomer = math.sqrt(s / math.pi)
@obsah.deleter
def obsah(self):
raise Exception("Mazat obsah kruhu nedava smysl.")
kruh = Kruh(1)
print("r = {}".format(kruh.polomer)) # Normální datový atribut
print("S = {}".format(kruh.obsah)) # Property
kruh.obsah = 3 # Změníme obsah pomocí zapisovatelné vlastnosti
print("r = {}".format(kruh.polomer)) # Změnil se nám i poloměr
print("S = {}".format(kruh.obsah)) # Property
# Zkusíme, co nám udělá kontrola v "setteru" vlastnosti obsah
kruh.obsah = "Asi jako největší český rybník, který se jmenuje Rožmberk."
# A další nesmyslná operace, které naše vlastnost zabrání
del kruh.obsah
Zapouzdření¶
V Pythonu není tento (pro OOP klíčový) koncept příliš dodržen. Zásady OOP tvrdí, že data by neměla být zvenčí přístupná, jiné jazyky obvykle nabízejí způsob, jímž lze schovat i některé metody (jako klíčová slova private, protected v C++, Javě). Python toto neřeší, ve výchozím nastavení je vše přístupno. Obvyklá konvence:
- Na data objektů (pokud třída není vyloženě jednoduchá) se zvenčí nesahá.
- Metody, jejichž jméno začíná podtržítkem, se zvenku nevolají (protože nejsou součástí "veřejného" rozhraní objektu).
- Pokud chceme data chránit, můžeme pro ně vytvořit vlastnost (property).
- Případné odlišnosti a obecně způsob, jakým se s metodami a daty nakládá, by měly být uvedeny v dokumentaci třídy.
- Existují způsoby, jak zapouzdření lze vynutit (předefinování způsobu přístupu k atributům, ...), které se však příliš nepoužívají.
Python ovšem na oplátku nabízí velmi vysokou úroveň introspekce, neboli schopnosti dozvědět se informace o objektech (jejich typ, atributy apod.) za běhu.
Podtržítkové konvence¶
V Pythonu obecně jsou konvence velice silně zakořeněné. Na objektech to je vidět snad nejvíce.
- "Soukromé" atributy (atributem se v Pythonu často rozumí jak data tak metody -- vše je objekt) se pojmenovávají s podtržítkem na začátku, tj. např.
_soukroma_metoda
. - Dvě podtžítka na začátku názvu atributu jej navíc přejmenují, takže je opravdu těžké se na něj odkazovat mimo kontext dané třídy.
- Atributy se dvěma podtžítky na začátku i na konci mají speciální význam (viz dokumentace). Už jsme viděli
__init__
, podíváme se na několik dalších.-
__repr__
a__str__
převádějí objekt na řetězec. -
__getattr__
a__setattr__
slouží pro čtení a ukládání nenalezených atributů. -
__call__
se zavolá pokud použijeme objekt jako funkci. -
__doc__
obsahuje dokumentaci (docstring). -
__dict__
obsahuje slovník se jmenným prostorem obejktu. - ... dále existují speciální funkce pro logické operátory, pro emulaci funkcionality kontejnerů (iterace, položky, řezy), pro aritmetické operace atd.
-
# co všechno obsahuje instace typu object
dir(object())
# a co obsahuje jednoduchá funkce
def foo(x):
"""Toto je funkce foo"""
return x
dir(foo)
Dědičnost¶
Třída může svoje chování (i data) odvozovat od nějaké jiné třídy, čímž si ušetříme spoustu práce při opakování společných rysů. V takovém případě řekneme, že naše nová třída (dceřinná) od té původní (rodičovské) dědí.
- V dceřinné třídě můžeme změnit definici některé metody z rodičovské třídy.
- Konstruktory se standardně dědí (Na rozdíl od C++ či Javy, kde se musí explicitně volat, v Pythonu se volají jen pokud definujeme nový konstruktor a chceme zavolat i nadřazený.)
- Instance dceřinné třídy se mohou použít kdekoliv, kde počítá s objektem rodičovské třídy. Toto platí v Pythonu ještě obecněji - obvykle se nekontrolují konkrétní typy, projde jakýkoliv objekt, který nabízí používané atributy/metody.
Syntax: Jméno rodičovské třídy se dává do závorky za jméno (místo object, od kterého třídy obvykle dědí).
class Clovek:
def __init__(self, jmeno): # Konstruktor, který nastaví atribut "jmeno"
self.jmeno = jmeno
def rekni(self, veta): # Výchozí definice metody "rekni"
print(type(self).__name__ + ": " + veta)
def predstav_se(self):
self.rekni("Jmenuji se %s." % self.jmeno)
def pozdrav(self):
self.rekni("Dobrý den.")
def rozluc_se(self):
self.rekni("Nashledanou.")
class Elektrikar(Clovek):
def oprav_televizi(self): # Nová metoda v rodičovské třídě - jiný Clovek ji neumí
self.rekni("Bude to v cuku letu.")
print("---Elektrikar něco šudlá.---")
self.rekni("A je to.")
def predstav_se(self): # Předefinovaná metoda "predstav_se" využívá atribut rodičovské třídy
self.rekni("Já sem ňákej %s." % self.jmeno)
class Nemocny(Clovek):
def rekni(self, veta): # Předefinovaná metoda, která staví na rodičovské metodě.
"""Nemocný má plný nos a co chvíli kýchne."""
trantab = "".maketrans("nmNM", "dbDB")
Clovek.rekni(self, veta.translate(trantab)) # Volání rodičovské metody
self.kychni()
def kychni(self): # Nová metoda v rodičovské třídě - jiný Clovek ji neumí
print("---Heeeepččččíííík---")
elektikar = Elektrikar("Franta Vopička")
nemocny = Nemocny("Tomáš Marný")
# Rozhovor ze života
nemocny.pozdrav() # Všimněte si, že "pozdrav" je rodičovská metoda, ale volá se "rekni" z dceřinné třídy.
elektikar.pozdrav()
nemocny.predstav_se()
elektikar.predstav_se()
nemocny.rekni("Opravte mi, prosím, televizi.")
elektikar.oprav_televizi()
nemocny.rekni("Máte mé neskonalé díky.")
elektikar.rozluc_se()
nemocny.rozluc_se()
nemocny.oprav_televizi() # Nemocný neumí opravit televizi
# A elektrikář neumí kýchnout
Pokud bychom chtěli nemocného elektrikáře, tak bychom museli sáhnout po vícenásobné dědičnosti a museli bychom ošetřit, že se správně volají rodičovské metody. Ještě lépe bychom využili tzv. mixiny a do objektů jejich stav přidávali dynamicky, ale to už je opravdu pokročilé téma, zabývat se jím nebudeme.
Dědění od vestavěných typů¶
Lze dědit i od vestavěných typů (a je to mnohdy i užitečné, ačkoliv to náš příklad nedokazuje).
# Ukázka seznamu, který nevrátí svoji položku, pokud jej nepoprosíme.
class NerudnySeznam(list):
def __getitem__(self, index): # Předefinujeme speciální metodu, která slouží k přístupu k položkám
if isinstance(index, tuple) and index[1].lower()[:6] in ("prosim"):
return list.__getitem__(self, index[0]) # Voláme původní metodu z typu "list"
else:
print("A že bys poprosil!?")
return None
s = NerudnySeznam((1, 2, 3, 4))
print(s[1])
print(s[1, "prosim"])
Pokročilá témata¶
Všechna následující témata jsou hrozně zajímavá a příšerně užitečná, nicméně nemáme na OOP tolik času, abychom se jim mohli věnovat. Přesto doporučujeme, abyste si o nich něco přečetli, budete-li mít chvilku času.
- Vícenásobná dědičnost
- Metody třídy
- Statické metody
- Abstraktní třídy
- Polymorfismus
- Metatřídy
- Návrhové vzory
Komentáře
Comments powered by Disqus