Lekce 5 - Zapouzdření atributů podrobně v Pythonu
V minulé lekci, Hrací kostka v Pythonu podruhé - Překrývání metod a random, jsme se naučili překrývat metody, používat vnitřní import a dokončili jsme hrací kostku.
V tomto tutoriálu objektově orientovaného programování v Pythonu se podrobněji zaměříme na práci s privátními atributy a jejich zapouzdřením. Ukážeme si jak vytvářet atributy za běhu i jak tomu lze v kódu našich tříd zabránit.
Zapouzdření v Pythonu detailně
Nyní už máme dostatek znalostí, abychom se důkladně zahloubali nad možnostmi zapouzdření. Řekli jsme si, že i když nám Python umožňuje nastavovat atributy privátně, nejsou od chtivých rukou programátora zcela oddělené.
Uveďme si příklad s naší kostkou. Upravíme atribut
pocet_sten
na skutečně privátní pomocí dvou
podtržítek a zkusme k němu přistoupit v kódu, změnit jeho hodnotu a tu
vypsat:
class Kostka:
def __init__(self,pocet_sten=6):
self.__pocet_sten = pocet_sten
def vrat_pocet_sten(self):
"""
Vrátí počet stěn kostky.
"""
return self.__pocet_sten
def __str__(self):
"""
Vrací textovou reprezentaci kostky.
"""
return str(f"Kostka s {self.__pocet_sten} stěnami.")
desetistenna = Kostka(10)
print(f"Před pokusem o úpravu privátního atributu: {desetistenna}")
desetistenna.__pocet_sten = 365
print(f"Upravili jsme atribut na hodnotu: {desetistenna.__pocet_sten}")
print(f"Po pokusu o úpravu privátního atributu: {desetistenna}")
V konzoli uvidíme výstup:
Pokus o změnu privátního atributu zvenčí:
Před pokusem o úpravu privátního atributu: Kostka s 10 stěnami.
Upravili jsme atribut na hodnotu: 365
Po pokusu o úpravu privátního atributu: Kostka s 10 stěnami.
To je velmi zajímavé chování. Zdá se, že Python nám umožňuje bez vyvolání chyby změnit privátní atribut, ale na tuto změnu pak nebere ohled. Bohužel, je to poněkud komplikovanější.
Už jsme si řekli, že v Pythonu neexistuje striktní privátnost, jak ji
známe z jiných jazyků (jako je Java nebo C++). Místo toho Python používá konvenci zvanou "name
mangling". Když deklarujeme atribut s dvojitým podtržítkem, např.
__pocet_sten
, Python tento název automaticky změní na
_NazevTridy__pocet_sten
. V případě našeho kódu se
__pocet_sten
interně stává atributem s názvem
_Kostka__pocet_sten
.
Náš pokus o přiřazení nové hodnoty tomuto atributu
(kostka.__pocet_sten = 365
) tak vůbec neupravil původní
privátní atribut __pocet_sten
třídy Kostka
.
Vytvořil totiž atribut zcela nový. Původní privátní
atribut, který se díky dvojitému podtržítku přejmenoval na
_Kostka__pocet_sten
, zůstává nezměněný. Proto poslední
výpis v našem pokusu potvrdil, že kostka je stále desetistěnná.
Přístup k privátnímu atributu
Pokud bychom opravdu chtěli počet stěn kostky změnit, museli bychom použít tento kód:
desetistenna._Kostka__pocet_sten = 365 print(desetistenna)
V konzoli pak uvidíme:
Změna privátního atributu:
Kostka s 365 stěnami.
Proč jde o špatnou praktiku
Hlavním důvodem, proč jsou některé atributy označeny jako privátní, je zapouzdření. Zapouzdření je jedním z klíčových principů OOP. Přímý přístup k privátním atributům tuto koncepci narušuje. Představme si, že se vývojář třídy později rozhodne změnit implementaci nebo strukturu interního atributu a my mu v jiné části kódu "natvrdo" měníme jeho hodnotu. Máloco je lepší způsob, jak do kódu zanést neočekávané chyby. Přístup k privátním atributům také výrazně zhorší čitelnost kódu.
I když můžeme technicky přistupovat k privátním atributům přes "name mangling", měli bychom to dělat jen v opravdu výjimečných situacích, a raději vůbec ne. Téměř vždy je lepší respektovat zapouzdření a pracovat s objekty tak, jak byly navrženy.
Vytváření atributů za běhu
Řekli jsme si, že pokus o přímé přepsání privátního atributu skončil tak, že Python místo toho vytvořil atribut nový. V Pythonu je povolena velká míra dynamiky, a to zahrnuje schopnost přidávat atributy k objektům za běhu. Platí to i pro metody, ale tam už jde o náročnější postup, který zatím výrazně přesahuje naše znalosti a zde se jím nebudeme zabývat. Tato schopnost je jedním z rysů, které dělají Python obzvláště flexibilním a mocným, ale současně je skvělým kandidátem na to být zdrojem potenciálních chyb.
Třídy v Pythonu jsou tedy modifikovatelné za běhu. Když vytvoříme instanci třídy, Python nám umožňuje této instanci přidat nové atributy (a metody), i když nebyly definovány v původní třídě.
Ukažme si příklad pro atribut:
class Zvire:
pass
pes = Zvire()
pes.rasa = "Jsem nový jezevčík přidaný za běhu!"
print(pes.rasa)
Ve výstupu konzole uvidíme:
Přidání atributu za běhu:
Jsem nový jezevčík přidaný za běhu!
Je zřejmé, že tato vlastnost Pythonu vede do nebezpečných vod Naprogramujeme si dokonalou
třídu, jen aby nám do ní kolega vložil své vlastní atributy, které
nemusí být úplně konzistentní s naším návrhem. Potenciální moře
problémů je jasné. Naštěstí pro to má Python řešení.
Využití atributu __slots__
V Pythonu je __slots__
speciální atribut třídy, který
definuje pevný seznam atributů, které instance této třídy může mít.
Použitím __slots__
tedy vyloučíme možnost vytvářet
nové atributy mimo definovaný seznam:
class Auto:
__slots__ = ['znacka', 'model']
def __init__(self, znacka="Skoda", model="Superb"):
self.znacka = znacka
self.model = model
auto = Auto()
print(auto.znacka) # Skoda
print(auto.model) # Superb
auto.rok_vyroby = "2023" # Tohle vyvolá chybu, protože 'rok_vyroby' není ve __slots__
Pokud se pokusíme přidat atribut rok_vyroby
, který není v
definici __slots__
, v konzoli uvidíme chybu:
Přidání atributu mimo __slots__:
AttributeError: 'Auto' object has no attribute 'rok_vyroby'
To je pro tuto lekci vše
V následujícím kvízu, Kvíz - Konstruktory, zapouzdření a překrývání v Pythonu, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.