4. díl - Odkazy na objekty, jejich kopírování a Garbage collector

Python Objektově orientované programování Odkazy na objekty, jejich kopírování a Garbage collector

V minulém tutoriálu pro Python jsme si vytvořili svůj první pořádný objekt, byla jím hrací kostka. Začínáme pracovat s objekty. Je důležité, abychom přesně věděli, co se uvnitř programu děje, jinak by nás v budoucnu mohlo leccos překvapit.

Aplikace (resp. její vlákno) má operačním systémem přidělenou paměť v podobě tzv. zásobníku (stack). Jedná se o velmi rychlou paměť s přímým přístupem, její velikost aplikace nemůže ovlivnit, prostředky jsou přidělovány operačním systémem.

Vytvořme si novou konzolovou aplikaci a vytvořme si v ní jednoduchou třídu, která bude reprezentovat uživatele nějakého systému. Pro názornost vypustím komentáře a nebudu řešit viditelnosti:

class Uzivatel:

    def __init__(self, jmeno, vek):
        self.jmeno = jmeno
        self.vek = vek

    def __str__(self):
        return str(self.jmeno)

Třída má 2 jednoduché veřejné atributy, metodu konstruktoru a __str__(), abychom mohli uživatele jednoduše vypisovat. Do programu přidejme vytvoření instance této třídy:

u = Uzivatel("Jan Novák", 28)

Proměnná u obsahuje odkaz na objekt. Podívejme se na situaci v paměti:

Zásobník a halda v paměti počítače

Zásobník i halda se nacházejí v paměti RAM. Rozdíl je v přístupu a velikosti. Halda je prakticky neomezená paměť, ke které je však přístup složitější a tím pádem pomalejší. Naopak zásobník je paměť rychlá, ale velikostně omezená.

Objekty jsou v paměti uloženy vlastně nadvakrát, jednou v zásobníku a jednou v haldě. V zásobníku je uložena pouze tzv. reference, tedy odkaz do haldy, kde se poté nalézá opravdový objekt.

Můžete se ptát, proč je to takto udělané. Důvodů je hned několik, pojďme si některé vyjmenovat:

  1. Místo ve stacku je omezené.
  2. Když budeme chtít použít objekt vícekrát (např. ho předat jako parametr do několika metod), nemusíme ho v programu předávat jako kopii. Předáme pouze referenci na objekt místo toho, abychom obecně paměťově náročný objekt kopírovali. Toto si vzápětí ukážeme.

Založme si 2 proměnné typu Uzivatel:

u = Uzivatel("Jan Novák", 28)
v = Uzivatel("Josef Nový", 32)

Situace v paměti bude následující:

Odkazy na objekt v Pythonu v paměti počítače

Nyní zkusme přiřadit do proměnné u proměnnou v. U objektu se zkopíruje pouze odkaz na objekt, ale objekt máme stále jen jeden. V kódu vykonáme tedy toto:

u = Uzivatel("Jan Novák", 28)
v = Uzivatel("Josef Nový", 32)
u = v

V paměti bude celá situace vypadat následovně:

Odkazy na objekt v paměti počítače

Přesvědčme se o tom, abyste viděli, že to opravdu tak je :) Nejprve si necháme obě dvě proměnné vypsat před a po změně. Navíc si vypíšeme i id objektů přes funkci id(). Protože budeme výpis volat vícekrát, napíši ho poněkud úsporněji. Upravme tedy kód na následující:

# založení proměnných
u = Uzivatel("Jan Novák", 28)
v = Uzivatel("Josef Nový", 32)
print("u: {0}\nv: {1}".format(u, v))
print("u: {0}\nv: {1}\n".format(id(u), id(v)))
# přiřazování
u = v
print("u: {0}\nv: {1}".format(u, v))
print("u: {0}\nv: {1}\n".format(id(u), id(v)))
input()

Výstup programu:

Uživatelé jako odkazy na objekt v Pythonu

Pojďme změnit jméno uživatele v a dle našich předpokladů by se měla změna projevit i v proměnné u. K programu připíšeme:

# změna
v.jmeno = "John Doe"
print("u: {0}\nv: {1}".format(u, v))
print("u: {0}\nv: {1}\n".format(id(u), id(v)))

Změnili jsme objekt v proměnné v a znovu vypíšeme u a v:

Uživatelé jako odkazy na objekt v Pythonu

Spolu se změnou v se změní i u, protože proměnné ukazují na ten samý objekt. Připomeňme si situaci v paměti ještě jednou a zaměřme se na Jana Nováka.

Uživatelé jako odkazy na objekt v Pythonu

Co se sním stane? "Sežere" ho tzv. Garbage collector.

Garbage collector

Garbage collector a dynamická správa paměti

Paměť můžeme v programech alokovat staticky, to znamená, že ve zdrojovém kódu předem určíme, kolik jí budeme používat. Ale také nemusíme určit kolik paměti budeme potřebovat. V tomto případě hovoříme o dynamické správě paměti.

V minulosti, hlavně v dobách jazyků C, Pascal a C++, se k tomuto účelu používaly tzv. pointery, neboli přímé ukazatele do paměti. Vesměs to fungovalo tak, že jsme si řekli operačnímu systému o kus paměti o určité velikosti. On ji pro nás vyhradil a dal nám její adresu. Na toto místo v paměti jsme měli pointer, přes který jsme s pamětí pracovali. Problém byl, že nikdo nehlídal, co do paměti dáváme (ukazatel směřoval na začátek vyhrazeného prostoru). Když jsme tam dali něco většího, zkrátka se to stejně uložilo a přepsala se data za naším prostorem, která patřila třeba jinému programu nebo operačnímu systému (v tom případě by naši aplikaci OS asi zabil - zastavil). Často jsme si však my v paměti přepsali nějaká další data našeho programu a program se začal chovat chaoticky. Představte si, že si uložíte uživatele do pole a v tu chvíli se vám najednou změní barva uživatelského prostředí, tedy něco, co s tím vůbec nesouvisí. Hodiny strávíte tím, že kontrolujete kód pro změnu barvy, poté zjistíte, že je chyba v založení uživatele, kdy dojde k přetečení paměti a přepsání hodnot barvy. Když naopak nějaký objekt přestaneme používat, musíme po něm místo sami uvolnit, pokud to neuděláme, paměť zůstane blokovaná. Pokud toto děláme např. v nějaké metodě a zapomeneme paměť uvolňovat, naše aplikace začne padat, případně zasekne celý operační systém. Taková chyba se opět špatně hledá, proč program přestane po několika hodinách fungovat? Kde tu chybu v několika tisících řádků kódu vůbec hledat? Nemáme jedinou stopu, nemůžeme se ničeho chytit, musíme projet celý program řádek po řádku. Brrr. Podobný problém nastane, když si někde paměť uvolníme a následně pointer opět použijeme (zapomeneme, že je uvolněný, to se může lehce stát), povede někam, kde je již uloženého něco jiného a tato data budou opět přepsána. Povede to k nekontrolovatelnému chování naší aplikace a může to dopadnout i takto:

Blue Screen Of Death – BSOD ve Windows

Až na malou skupinu géniů lidi přestalo bavit řešit neustálé a nesmyslné chyby. Za cenu mírného snížení výkonu vznikly řízené jazyky (managed) s tzv. garbage collectorem, jedním z nich je i Python. C++ se samozřejmě nadále používá, ale pouze na specifické programy, např. části operačního systému nebo 3D enginy komerčních her, kde je potřeba z počítače dostat maximální výkon. Na 99% všech ostatních aplikací se více hodí Python nebo jiný řízený jazyk, kvůli automatické správě paměti.

Garbage collector

Garbage collector je vlastně program, který běží paralelně s naší aplikací, v samostatném vlákně. Občas se spustí a podívá se, na které objekty již v paměti nevedou žádné reference. Ty potom odstraní. Ztráta výkonu je minimální a značně to sníží procento sebevražd programátorů, ladících po večerech rozbité pointery. Zapnutí a vypnutí GC můžeme dokonce z kódu ovlivnit, i když to není v 99% případů vůbec potřeba. Protože je jazyk řízený a nepracujeme s přímými pointery, není vůbec možné paměť nějak narušit, nechat ji přetéct a podobně, interpret se o paměť automaticky stará.

Poslední věc, o které se zmíníme, je tzv. hodnota None. Referenční typy mohou, na rozdíl od hodnotových, nabývat speciální hodnoty a to None. None je klíčové slovo a označuje, že reference neukazuje na žádná data. Když nastavíme proměnnou v na None, zrušíme pouze referenci. Pokud na náš objekt existuje ještě nějaká reference, bude i nadále existovat. Pokud ne, bude uvolněn GC. Změňme ještě poslední řádky našeho programu na:

# další změna
v.jmeno = "John Doe"
v = None

Výstup:

Uživatelé jako odkazy na objekt v Pythonu

Vidíme, že objekt stále existuje a ukazuje na něj proměnná u, v proměnné v již není reference. None se využívá např. při testování příslušnosti pomocí klíčového slova is, ale o tom někdy jindy.

Kopírování objektů

Jestli se ptáte, jak vytvořit opravdovou kopii objektu, tak můžeme objekt znovu vytvořit pomocí konstruktoru a dát do něj stejná data, nebo můžeme použít hlubokou kopii. Vrátíme se k naší kostce z minulého dílu:

class Kostka:
    """
    Třída reprezentuje hrací kostku.
    """

    def __init__(self, pocet_sten=6):
        self.__pocet_sten = pocet_sten

    def __str__(self):
        """
        Vrací textovou reprezentaci kostky.
        """
        return str("Kostka s {0} stěnami".format(self.__pocet_sten))

    def vrat_pocet_sten(self):
        return self.__pocet_sten

    def hod(self):
        """
        Vykoná hod kostkou a vrátí číslo od 1 do
        počtu stěn.
        """
        import random as _random
        return _random.randint(1, self.__pocet_sten)

Do kostky přidáme metodu podobnou metodě __str__() a to metodu __repr__(). Tato metoda také vrací řetězec, který můžeme poté předat funkci eval(). Tato funkce se používá k dynamickému provádění kódu. Ovšem nechávat našeho uživatele zadat výraz je dost nebezpečné. Naše metoda __repr__() bude vracet konstruktor kostky:

def __repr__(self):
    """
    Vrací v řetězci kód konstruktoru pro funkci eval().
    """
    return str("Kostka({0})".format(self.__pocet_sten))

Kód pro vytvoření nové kostky:

jina_sestistenna = eval(repr(sestistenna))

Nebo můžeme použít modul copy. A z něj funkci deepcopy():

jina_sestistenna = copy.deepcopy(sestistenna)

Příště si zase něco praktického naprogramujeme, ať si znalosti zažijeme. Prozradím, že půjde o objekt bojovníka do naší arény. To je zatím vše :)


 

Stáhnout

Staženo 85x (1.01 kB)
Aplikace je včetně zdrojových kódů v jazyce python

 

  Aktivity (1)

Článek pro vás napsal gcx11
Avatar
(^_^)

Jak se ti líbí článek?
Celkem (3 hlasů) :
55555


 



 

 

Komentáře

Avatar
honza.tomsu
Člen
Avatar
honza.tomsu:

Tak paměť na stacku je rychlejší jo? Hovno, je stejně rychlá jako zbytek ramky. Jediné co je rychlejší je alokace paměti na stacku.. Jedná se o pouhé odečtení čísla v registru rsp na archytekture x86. A na stacku je taktéž lepší využití cache. Lze snadno predikovat paměť, ke které bude v budoucnu pristoupeno.

Odpovědět  -3 16.6.2014 6:28
rm -rf ...
Avatar
hanpari
Redaktor
Avatar
Odpovídá na honza.tomsu
hanpari:

Doporučoval bych si napřed zopakovat pravopis a slušné chování. Ideální by bylo zapnout si kontrolu pravopisu a nechat si od rodičů nafackovat.

 
Odpovědět 16.6.2014 10:46
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na honza.tomsu
David Čápka:

Právě jsi napsal, že je rychlejší její alokace a že se lépe cachuje. To znamená, že můžeme klidně napsat že je rychlejší a to aniž bys nám musel psát vulgární komentáře.

Editováno 16.6.2014 12:33
Odpovědět  +1 16.6.2014 12:33
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 3 zpráv z 3.