Lekce 21 - Magické metody v Pythonu podruhé
V minulé lekci, Magické metody v Pythonu, jsme se věnovali magickým metodám.
V dnešním tutoriálu objektově orientovaného programování v Pythonu budeme pokračovat ve studiu magických metod.
Pokročilé dunder metody
Zaměříme se na atributové a indexové operace, volání objektů a správu instancí.
Atributové a indexové operace
Tyto metody se v Pythonu používají pro manipulaci s atributy a indexování objektů. Jsou klíčové pro pokročilé programování a umožňují nám vytvářet flexibilnější a dynamičtější objekty.
Metoda __getattr__()
Metoda __getattr__()
se volá, když se pokoušíme získat
přístup k atributu, který v objektu není definován. Je užitečná pro
vytváření objektů s dynamickými atributy nebo pro implementaci "proxy"
objektů, které delegují přístup k atributům na jiné objekty. Ukažme si
příklad:
class FlexibilniObjekt:
def __init__(self):
self._atributy = []
def __getattr__(self, name):
# Hledáme atribut podle jména
for nazev, hodnota in self._atributy:
if nazev == name:
return hodnota
# Pokud nebyl nalezen, vytvoříme nový
hodnota = f"Dynamicky vytvořený atribut '{name}'"
self._atributy.append((name, hodnota))
return hodnota
class ProxyLogger:
def __init__(self, cilovy_objekt):
self._cilovy_objekt = cilovy_objekt
def __getattr__(self, name):
print(f"Log: Přístup k atributu '{name}'")
return getattr(self._cilovy_objekt, name)
# Demonstrace FlexibilniObjekt
objekt = FlexibilniObjekt()
print(objekt.neexistujici_atribut) # Dynamicky vytvoří a vrátí hodnotu atributu
objekt.neexistujici_atribut = "Nová hodnota"
print(objekt.neexistujici_atribut) # Vrátí aktualizovanou hodnotu
# Demonstrace ProxyLogger
skoda = FlexibilniObjekt()
proxy = ProxyLogger(skoda)
print(proxy.model) # Loguje a dynamicky vytvoří atribut 'model'
skoda.model = "Octavia"
print(proxy.model) # Loguje a vypíše 'Octavia'
V příkladu třída FlexibilniObjekt
používá metodu
__getattr__()
pro dynamické vytváření atributů. Při
inicializaci instance FlexibilniObjekt
se vytvoří soukromý
seznam _atributy
pro uchovávání atributů. Když se pokusíme
poprvé přistoupit k neexistujici_atribut
, volá se metoda
__getattr__()
. Tato metoda nejdříve zkontroluje, zda atribut už
je v seznamu _atributy
. Pokud ne, vytvoří pro něj výchozí
hodnotu a uloží ji do _atributy
.
Třída ProxyLogger
slouží jako proxy, která loguje
přístupy k atributům na cílovém objektu. Při inicializaci
ProxyLogger
se předá cílový objekt a uloží se do atributu
_cilovy_objekt
. Když se pokusíme přistoupit k atributu instance
ProxyLogger
, Python nejprve zkontroluje, zda tento atribut existuje
přímo v instanci ProxyLogger
. Pokud ne, dojde k aktivaci metody
__getattr__()
. Ta pomocí funkce getattr()
předá
přístup k atributům na objekt _cilovy_objekt
. Funkce
getattr()
je standardní vestavěná funkce v Pythonu, která
umožňuje programu přistoupit k atributu objektu podle jeho názvu (zde jako
řetězec). V tomto případě getattr()
používáme k
přistoupení k atributu na objektu _cilovy_objekt
, který instance
ProxyLogger
zastupuje. Tím se tedy dosahuje toho, že pokud
atribut není nalezen přímo v instanci ProxyLogger
,
__getattr__()
ho hledá v objektu _cilovy_objekt
. To
je základní princip fungování proxy objektu - zprostředkování přístupu
k atributům jiného objektu.
Po spuštění kódu vytvoříme instanci třídy
FlexibilniObjekt
v objekt
a pokusíme se přistoupit k
neexistujici_atribut
. Tento atribut je následně dynamicky
vytvořen a vrácen. Když tento atribut později změníme, metoda
__getattr__()
vrátí jeho aktualizovanou hodnotu.
Dále vytvoříme instanci třídy FlexibilniObjekt
ve
skoda
a obalíme ji pomocí třídy ProxyLogger
. Když
poté přistoupíme k atributu proxy.model
, dojde k logování a
vytvoření atributu model
v instanci třídy
FlexibilniObjekt
(protože model
ještě neexistuje).
Pak nastavíme atribut skoda.model
na Octavia
. Tento
krok se děje přímo na instanci FlexibilniObjekt
, nikoli přes
ProxyLogger
. Při dalším přístupu k proxy.model
dojde opět k logování, ale tentokrát se již vrátí hodnota
Octavia
.
V tomto příkladu je důležité si uvědomit, že
__getattr__()
v instanci FlexibilniObjekt
se spouští
jen, když atribut neexistuje, zatímco __getattr__()
v instanci
ProxyLogger
se aktivuje při každém pokusu o přístup k
atributu, který není přímo v ProxyLogger
.
Volání objektů
Python umožňuje volat instance tříd jako funkce. Na tento zajímavý mechanismus má přímo vestavěnou metodu.
Metoda __call__()
Když v rámci třídy definujeme metodu __call__()
, instanci
této třídy pak lze používat podobně jako funkci. Toto chování je
užitečné v situacích, kdy chceme, aby náš objekt měl nějakou hlavní
funkcionalitu, kterou lze vyjádřit pomocí volání. Podívejme se na
příklad. Představme si, že máme třídu Scitac
, která má za
úkol sčítat čísla. Metodu __call__()
implementujeme tak, aby
přijímala libovolný počet čísel a vracela jejich součet:
class Scitac:
def __call__(self, *args):
return sum(args)
# Použití
scitac = Scitac()
vysledek = scitac(1, 2, 3, 4) # Vrací 10
print(f"Výsledek součtu je: {vysledek}")
Proč používat __call__()
:
- přehlednost a intuitivnost - metoda vytváří jasně definovanou hlavní funkci objektu, což je intuitivnější pro ostatní vývojáře, kteří náš kód používají.
- zapouzdření funkcionality - tento mechanismus umožňuje zapouzdřit funkcionalitu a související stav do jednoho objektu. Objekt také dokáže udržovat stav mezi jednotlivými voláními.
- flexibilita v návrhu - mechanismus poskytuje další vrstvu abstrakce a umožňuje návrhové vzory jako jsou Command nebo Strategy, kde jsou objekty používány jako reprezentace operací nebo strategií.
Správa instancí
V lekci Hrací
kostka v Pythonu - Zapouzdření a konstruktor hned na počátku kurzu jsme
se naučili vytvářet instance tříd pomocí konstruktorů. Řekli jsme si,
že k vytvoření nové instance máme k dispozici dvě metody a zaměřili se
na metodu __init__()
. Teď se podíváme na tu druhou. Současně
si poněkud ujasníme zjednodušení, kterého jsme se na začátku kurzu
dopustili.
Metoda __new__()
Metoda __new__()
v Pythonu je speciální statická metoda,
která je zodpovědná za vytváření nových instancí tříd. Na rozdíl od
metody __init__()
, která inicializuje již vytvořenou instanci
(toto je to zjednodušení, praktický význam ale v podstatě nemá), metoda
__new__()
se vlastně podílí na samotném procesu vytváření
každé instance. Tato metoda je volána ještě před __init__
a
je obvykle používána v pokročilejších scénářích, jako je kontrola nad
procesem vytváření objektů nebo implementace návrhových vzorů jako je Singleton.
Mějme třídu Jedinacek
, která využívá
__new__()
k zajištění, že bude vytvořena pouze jedna instance
této třídy:
class Jedinacek:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
# Použití
objekt1 = Jedinacek()
objekt2 = Jedinacek()
print(objekt1 is objekt2) # Vrací True, obě proměnné odkazují na stejnou instanci
V tomto příkladu __new__()
kontroluje, zda již existuje
instance třídy Jedinacek
. Pokud ano, vrací tuto existující
instanci místo vytváření nové. Předvedený kód je příkladem
implementace návrhového vzoru Singleton. Vzor je často používán v
situacích, kde je potřeba, aby objekt byl sdílený napříč různými
částmi programu, ale zároveň chceme mít jistotu, že existuje pouze jedna
jeho instance.
Tento vzor je třeba používat opatrně, protože často vede k problémům s designem softwaru, jako jsou obtíže při testování nebo porušení principů jako je oddělení závislostí (dependency separation).
Uživatelsky definované kontejnery
Python nám umožňuje vytvářet vlastní kontejnerové třídy, které se chovají podobně jako standardní datové struktury. Tedy jako seznamy nebo nám ještě neznámé slovníky a množiny. Používají se k tomu právě dunder metody. A kdo by nesouhlasil s tím, že vytvořit si vlastní seznam je fakt chladné
Nemá příliš velký smysl popisovat tyto metody samostatně. Lepší bude ukázat si souhrnný příklad:
class MujSeznam:
def __init__(self):
self._data = [] # Inicializace interního seznamu pro ukládání dat
def __len__(self):
# Vrací počet prvků v seznamu
return len(self._data)
def __getitem__(self, index):
# Vrací prvek na zadaném indexu
return self._data[index]
def __setitem__(self, index, value):
# Nastaví hodnotu prvku na zadaném indexu
self._data[index] = value
def __delitem__(self, index):
# Odstraní prvek na zadaném indexu
del self._data[index]
def pridat(self, value):
# Přidává nový prvek na konec seznamu
self._data.append(value)
# Použití třídy
muj_seznam = MujSeznam()
muj_seznam.pridat(1) # Přidá 1
muj_seznam.pridat(2) # Přidá 2
muj_seznam.pridat(3) # Přidá 3
print(len(muj_seznam)) # Vypíše 3 (délku seznamu)
print(muj_seznam[1]) # Vypíše 2 (prvek na indexu 1)
muj_seznam[1] = 20 # Nastaví prvek na indexu 1 na 20
del muj_seznam[0] # Odstraní prvek na indexu 0
V tomto příkladu třída MujSeznam
poskytuje základní funkce
standardního seznamu. Metoda __len__()
vrací počet prvků,
__getitem__()
slouží k získání prvku na zadaném indexu,
__setitem__()
umožňuje nastavení hodnoty prvku a
__delitem__()
slouží k odstranění prvku.
Výhodou oproti standardnímu seznamu je, že náš vlastní kontejner je libovolně rozšiřitelný o další metody a funkcionality.
Tímto způsobem dokážeme vytvářet třídy, které se chovají jako standardní datové struktury, ale s přidanou funkcionalitou nebo změněným chováním, které vyhovuje specifickým potřebám našich aplikací.
Vlastní iterátory a generátory
Vlastní iterátory a generátory jsou v Pythonu využívány k vytvoření
objektů, které mohou být procházeny pomocí cyklu for
. Metody
pro to máme dvě, konkrétně __iter__()
a
__next__()
. Toto téma je jedním z těch, které mají blízko ke
kolekcím, kde jej v kurzu Kolekce v Pythonu
důkladně probereme.
Správa paměti a Garbage Collector
V Pythonu se správa paměti a odstraňování nepotřebných objektů
obvykle provádí automaticky prostřednictvím mechanismu zvaného Garbage
Collector. Python ovšem také poskytuje speciální metodu
__del__()
, která umožňuje definovat chování objektu při jeho
odstraňování.
Metoda __del__()
Metoda __del__()
je tzv. destruktor. Je volána, když je objekt
na cestě k odstranění (tj. když na něj neexistují žádné další
odkazy). Využívá se pro uvolnění externích zdrojů nebo pro provedení
nějakého čištění (např. uzavření souborů nebo síťových spojení).
Nicméně doba, kdy je __del__()
zavolána, není pevně
daná. Python garbage collector běží podle vlastního harmonogramu a
tak může být objekt odstraněn ihned po ztrátě odkazu na něj, nebo až po
delší době. Metoda by se proto neměla používat pro kód, na jehož
spuštění v konkrétním okamžiku záleží. Podívejme se na příklad:
class Zaznam:
def __init__(self, hodnota):
self.hodnota = hodnota
print(f"Vytvářím záznam: {hodnota}")
def __del__(self):
print(f"Mažu záznam: {self.hodnota}")
# Příklad použití
zaznam = Zaznam("důležitý údaj")
# Další kód
del zaznam # Zde může, ale nemusí dojít k okamžitému volání __del__()
To je pro tuto lekci vše
V následujícím kvízu, Kvíz - Dekorátory, vlastnosti a magické metody v Pythonu, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.