19. diel - Vlastnosti v Pythone druhýkrát - Pokročilé vlastnosti a dedenie
V minulej lekcii, Vlastnosti v Pythone , sme si predstavili vlastnosti alebo gettery a settery, ktoré umožnia jednoduchšie nastavovanie a validáciu hodnôt atribútov.
V dnešnom tutoriále objektovo orientovaného programovania v Pythone budeme pokračovať v práci s vlastnosťami. Zameriame sa najmä na ich pokročilé použitie. Venovať sa budeme dedeniu, vytváraniu vlastných dekorátorov pre vlastnosti a častým chybám, ktorých sa pri práci s vlastnosťami programátori dopúšťajú.
Pokročilé vlastnosti sú už pomerne náročná téma. Je preto veľmi dôležité starostlivo analyzovať všetky ukážky kódu v lekcii, skúsiť si ich vo vlastnom IDE modifikovať a neprechádzať ďalej v tutoriáli, pokiaľ kód skutočne plne nepochopíte.
Použitie vlastností v dedení
Pozrime sa teda bližšie na dôležitý koncept využitia dekorátora
@property
v kontexte dedičnosti v Pythone. Dedičnosť umožňuje
odvodenej triede zdediť metódy a vlastnosti základnej (rodičovskej)
triedy. Pomocou dekorátora @property
v základnej triede
definujeme vlastnosti, ktoré je potom možné v odvodenej triede preťažovať
alebo prispôsobiť:
class Tvar:
def __init__(self, barva='červená'):
self._barva = barva
@property
def barva(self):
return self._barva
@barva.setter
def barva(self, hodnota):
self._barva = hodnota
class Kruh(Tvar):
def __init__(self, polomer, barva='červená'):
super().__init__(barva)
self._polomer = polomer
@property
def polomer(self):
return self._polomer
@polomer.setter
def polomer(self, hodnota):
if hodnota <= 0:
raise ValueError("Poloměr musí byt větší než 0")
self._polomer = hodnota
# Vytvoření instance Kruh
kruh = Kruh(5, "modrá")
# Získání a nastavení vlastností
print(f"Barva kruhu: {kruh.barva}")
print(f"Poloměr kruhu: {kruh.polomer}")
# Změna vlastností
kruh.barva = "zelená"
kruh.polomer = 10
print(f"Nová barva kruhu: {kruh.barva}")
print(f"Nový poloměr kruhu: {kruh.polomer}")
# Pokus o nastavení neplatného poloměru
try:
kruh.polomer = -3
except ValueError as e:
print(f"Chyba: {e}")
Trieda Tvar
je základná trieda, ktorá má vlastnosť
barva
s getterom a setterom. Používa @property
na
definovanie týchto metód ako vlastnosti triedy.
Trieda Kruh
je odvodená trieda, ktorá dedí z
Tvar
. Zahŕňa svoju vlastnosť polomer
s vlastným
getterom a setterom. Preberá (dedí) zo základnej (rodičovskej) triedy
vlastnosť barva
.
Tento príklad ukazuje základné použitie @property
v dedení.
Zároveň ilustruje, ako odvodená trieda rozširuje alebo mení správanie
základnej triedy.
Preťažovanie vlastností v odvodených triedach
Preťažovanie vlastností (property overriding) v odvodenej triede je
proces, kedy nahrádzame alebo rozširujeme chovanie getterov a setterov
základnej triedy. Vďaka tomu môžeme dosiahnuť väčšiu flexibilitu a
špecializácia v odvodených triedach. Pozrime sa na konkrétnu aplikáciu. V
triede Tvar
máme základnú implementáciu setteru pre farbu,
ktorá jednoducho nastavuje hodnotu. V triede Kruh
teraz pridáme
kontrolu, ktorá overí, či zadaná farba patrí do zoznamu vopred
definovaných povolených farieb pre kruhy. To je jednoduchý príklad toho, ako
v odvodenej triede preťažíme a rozšírime správanie vlastnosti z
rodičovskej triedy:
class Tvar:
def __init__(self, barva='červená'):
self._barva = barva
@property
def barva(self):
return self._barva
@barva.setter
def barva(self, hodnota):
self._barva = hodnota
class Kruh(Tvar):
povolene_barvy = ['červená', 'modrá', 'zelená', 'žlutá']
def __init__(self, polomer, barva='červená'):
super().__init__(barva)
self._polomer = polomer
@property
def polomer(self):
return self._polomer
@polomer.setter
def polomer(self, hodnota):
if hodnota <= 0:
raise ValueError("Poloměr musí být větší než 0")
self._polomer = hodnota
@Tvar.barva.setter
def barva(self, hodnota):
if hodnota not in Kruh.povolene_barvy:
raise ValueError(f"Barva '{hodnota}' není pro kruhy povolená.")
Tvar.barva.fset(self, hodnota)
# Vytvoření instance Kruh
kruh = Kruh(5, "modrá")
# Změna barvy na povolenou barvu
kruh.barva = "žlutá"
print(f"Nová barva kruhu: {kruh.barva}")
# Pokus o nastavení nepovolené barvy
try:
kruh.barva = "fialová"
except ValueError as e:
print(f"Chyba: {e}")
V kóde trieda Kruh
rozširuje správanie setteru pre farbu.
Overuje, či je zadaná farba v zozname povolených farieb. Ak nie, vyvolá
výnimku ValueError
. Takto je možné v odvodenej triede
preťažovať a prispôsobovať vlastnosti základnej triedy.
Kód je zrejmý až na metódu fset
. To je interná metóda
používaná na volanie setteru vlastnosti. Normálne by sme ju v bežnom kóde
nevideli, pretože @property
ju obvykle skryje a umožňuje nám na
volanie setteru používať bežné priradenie atribútu. My ale setter
preťažujeme. Preto musíme metódu volať sami. Priame priradenie
self.barva = hodnota
by totiž viedlo k nekonečnej rekurzii.
Pokročilé vlastnosti a dekorátory
Vytváranie vlastných dekorátorov pre vlastnosti je jedným z pokročilejších a zároveň užitočných aspektov programovania v Pythone. V tejto kapitole sa pozrieme na to, ako vytvoríme vlastné dekorátory, ktoré je možné použiť pre vlastnosti tried.
Predstavme si, že chceme vytvoriť dekorátor, ktorý loguje každú zmenu
hodnoty vlastnosti (vrátane tých, ktoré spôsobia chybu). V našej triede
Kruh
chceme logovať zmeny polomeru. Najprv si teda napíšeme
funkciu pre dekorátor:
def log_property(polomer): # (func) def obalena_funkce(self, nova_hodnota): puvodni_hodnota = getattr(self, '_polomer') # (self, '_' + func.__name__) if nova_hodnota != puvodni_hodnota: print(f"Změna poloměru: {puvodni_hodnota} -> {nova_hodnota}") return polomer(self, nova_hodnota) return obalena_funkce
Táto funkcia nepatrí do žiadnej triedy a musíme ju do kódu vložiť
pred miesto, kde neskôr použijeme dekorátor
@log_property
. Najlepšie na začiatok súboru alebo medzi triedy
Tvar
a Kruh
. Funkcia je bohužiaľ pomerne
komplikovaná vzhľadom na naše znalosti. Ide hlavne o časť
(self, '_' + func.__name__)
v komentári kódu. Funkcia
getattr()
je štandardná vstavaná funkcia v Pythone, ktorá sa
používa na dynamické získanie hodnoty atribútu objektu na základe jeho
názvu, ktorý je jej odovzdaný ako reťazec. A práve v konštrukcii reťazca
je zakopaný jazvečík. Aby bola funkcia univerzálna (a správne napísaná),
musela by v atribúte prijímať referenciu (func
), nie priamo
názov funkcie (polomer
). Ďalej by sme v getattr()
museli reťazec zložiť z podčiarkovníka a názvu funkcie. Práve na zistenie
názvu funkcie z referencie func
slúži to
func.__name__
v komentári. Bohužiaľ, túto látku ešte len
budeme preberať. Máme preto funkciu napísanú priamo s napevno vloženými
údajmi, a funkcia tak nie je univerzálna. O magických dunder metódach, to
sú tie s fakt veľa podčiarkovníky:-D , sa dozvieme neskôr v kurze.
Určite ale neuškodí, keď si vo svojom IDE skúsime funkciu upraviť na univerzálnu. V komentároch ku kódu je všetko potrebné uvedené.
Použitie dekorátora
Dekorátor aplikujeme na setter metódu v našej triede
Kruh
:
@polomer.setter @log_property def polomer(self, hodnota): if hodnota <= 0: raise ValueError("Poloměr musí být větší než 0") self._polomer = hodnota
Kedykoľvek teraz dôjde k zmene polomeru, funkcia dekorátora
log_property()
túto zmenu zaznamená:
class Tvar:
def __init__(self, barva='červená'):
self._barva = barva
@property
def barva(self):
return self._barva
@barva.setter
def barva(self, hodnota):
self._barva = hodnota
def log_property(polomer):
def obalena_funkce(self, nova_hodnota):
puvodni_hodnota = getattr(self, '_polomer')
if nova_hodnota != puvodni_hodnota:
print(f"Změna poloměru: {puvodni_hodnota} -> {nova_hodnota}")
return polomer(self, nova_hodnota)
return obalena_funkce
class Kruh(Tvar):
povolene_barvy = {'červená', 'modrá', 'zelená', 'žlutá'}
def __init__(self, polomer, barva='červená'):
super().__init__(barva)
self._polomer = polomer
@property
def polomer(self):
return self._polomer
@polomer.setter
@log_property
def polomer(self, hodnota):
if hodnota <= 0:
raise ValueError("Poloměr musí být větší než 0")
self._polomer = hodnota
@Tvar.barva.setter
def barva(self, hodnota):
if hodnota not in Kruh.povolene_barvy:
raise ValueError(f"Barva '{hodnota}' není povolena pro kruhy.")
Tvar.barva.fset(self, hodnota)
# Vytvoření instance Kruh
kruh = Kruh(5, "modrá")
kruh.polomer = 7 # Vypíše: Změna poloměru: 5 -> 7
kruh.polomer = 7 # Tento řádek nevypíše nic, protože nedochází ke změně hodnoty
kruh.polomer = 17 # Vypíše: Změna poloměru: 7 -> 17
Vďaka vlastnému dekorátoru teda dokážeme pridávať zložitejšie
správanie k vlastnostiam tried bez zásahu do ich vnútornej
implementácie. Vlastné dekorátory v praxi obvykle pridávajú
dodatočné správanie (napríklad logovanie, overovanie, transformácia dát) k
operáciám, ktoré sú spojené s vlastnosťami definovanými pomocou
@property
. Toto je veľmi mocná vlastnosť jazyka Python, ktorá
umožňuje písať čistý, modulárny a ľahko udržiavateľný kód.
Bežné chyby a nástrahy
Existuje niekoľko notoricky sa opakujúcich chýb, ktorých sa programátori dopúšťajú. Poďme sa na tie dve hlavné pozrieť bližšie.
Nekonečné rekurzie pri používaní setterov
Tejto pasce na nepozorné už sme sa v lekcii dotkli. Nekonečná rekurzia v setteroch nastane, keď setter neúmyselne zavolá sám seba. To sa často stáva, ak sa v settere pre nejakú vlastnosť pokúsime priamo priradiť hodnotu tejto vlastnosti, namiesto toho, aby sme priradili súkromný atribút. Príklad nám to ozrejmí:
class Kruh: def __init__(self, polomer): self.polomer = polomer # Volá setter @property def polomer(self): return self._polomer @polomer.setter def polomer(self, hodnota): if hodnota <= 0: raise ValueError("Poloměr musí být větší než 0") self.polomer = hodnota # ZDE nastane nekonečná rekurze! Měli jsme použít self._polomer s podtržítkem!
Interná implementácia triedy má vždy využívať
priamy prístup k interným atribútom
(self._polomer
), zatiaľ čo všetok externý prístup má
prebiehať cez definované rozhranie
(self.polomer
).
Poradie dekorátorov
Poradie, v ktorom sa aplikujú dekorátory, je kľúčové. Už vieme, že
dekorátory sa aplikujú odspodu nahor. V prípade kombinácie
@property
, getteru/setteru a ďalších vlastných dekorátorov je
dôležité si uvedomiť, ktorý dekorátor vykoná svoj kód ako prvý a ako to
ovplyvní ďalšie správanie kódu. Napríklad, ak máme vlastný dekorátor
pre logovanie a chceme ho použiť spoločne s @property
, musí sa
@property
spustiť ako posledný. Tým zaistíme, že logovanie
bude zachytávať operácie na úrovni vlastnosti, nie na úrovni metódy:
class Kruh: @log_property # Tento dekorátor se spustí jako první @property # Poté bude spuštěn @property def polomer(self): return self._polomer
Keď kód pristupuje k vlastnosti polomer
, najskôr sa aktivuje
správanie dekorátora @log_property
(pretože je napísaný hore a
spúšťa sa ako prvý). Až potom sa vykoná predvolená operácia gettera
definovaného @property
dekorátorom.
Dekorátory sa najprv aplikujú vo vzostupnom poradí, ale spúšťajú sa v zostupnom poradí.
Zdrojový kód z lekcie je na stiahnutie v archíve:-)
V budúcej lekcii, Magické metódy v Pythone , sa pozrieme na magické metódy objektov.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 53x (1.87 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Python