Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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é :-D

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í.


 

Předchozí článek
Magické metody v Pythonu
Všechny články v sekci
Objektově orientované programování v Pythonu
Přeskočit článek
(nedoporučujeme)
Kvíz - Dekorátory, vlastnosti a magické metody v Pythonu
Článek pro vás napsal gcx11
Avatar
Uživatelské hodnocení:
297 hlasů
(^_^)
Aktivity