Lekce 6 - Odkazy na objekty a Garbage collector v Pythonu
V předešlém cvičení, Řešené úlohy k 3.-5. lekci OOP v Pythonu, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
V tomto tutoriálu objektově orientovaného programování v Pythonu se zaměříme na odkazy (reference) na objekty, jejich kopírování a představíme si Garbage collector.
Aplikace a paměť
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, protože prostředky jsou přidělovány operačním systémem.
Vytvořme si novou konzolovou aplikaci a v ní jednoduchou třídu
Uzivatel
, která bude reprezentovat uživatele nějakého systému.
Pro názornost vypustíme komentáře a nebudeme řešit zapouzdření:
class Uzivatel: def __init__(self, jmeno, vek): self.jmeno = jmeno self.vek = vek def __str__(self): return str(self.jmeno)
Třída má dva jednoduché veřejné atributy, metodu konstruktoru a metodu
__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
teď obsahuje odkaz na objekt.
Zásobník a halda
Podívejme se na situaci v paměti:

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 ale uložena pouze tzv. reference, tedy odkaz do haldy, kde se poté nalézá opravdový objekt.
Můžeme se ptát, proč je to takto udělané. Důvodů je hned několik, pojďme si některé vyjmenovat:
- Místo ve stacku je omezené.
- 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 za chvíli ukážeme.
Chování dat v paměti
Založme si dvě proměnné typu Uzivatel
:
u = Uzivatel("Jan Novák", 28) v = Uzivatel("Josef Nový", 32)
Situace v paměti bude následující:

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 tedy vykonáme toto:
u = Uzivatel("Jan Novák", 28) v = Uzivatel("Josef Nový", 32) u = v
V paměti bude celá situace vypadat následovně:

Přesvědčme se o tom, abychom 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íšeme si jej poněkud úsporněji. Upravme tedy kód na
následující:
{PYTHON}
class Uzivatel:
def __init__(self, jmeno, vek):
self.jmeno = jmeno
self.vek = vek
def __str__(self):
return str(self.jmeno)
# založení proměnných
u = Uzivatel("Jan Novák", 28)
v = Uzivatel("Josef Nový", 32)
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
# přiřazování
u = v
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
{/PYTHON}
Pojďme změnit jméno uživatele v proměnné v
a dle našich
předpokladů by se měla změna projevit i v proměnné u
. K
programu připíšeme:
{PYTHON}
class Uzivatel:
def __init__(self, jmeno, vek):
self.jmeno = jmeno
self.vek = vek
def __str__(self):
return str(self.jmeno)
# založení proměnných
u = Uzivatel("Jan Novák", 28)
v = Uzivatel("Josef Nový", 32)
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
# přiřazování
u = v
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
# změna jména
v.jmeno = "John Doe"
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
{/PYTHON}
Změnili jsme objekt v proměnné v
a znovu vypíšeme
u
a v
. 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:

Co se s ním stane? "Sežere" ho tzv. 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. Brzy se ale budeme setkávat s aplikacemi (a už jsme se vlastně i setkali), kdy nebudeme před spuštěním přesně vědět, kolik paměti budeme potřebovat. V takovém 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 v paměti přepsali nějaká další data našeho programu a program se začal chovat chaoticky. Představme si, že si uložíme uživatele do pole a v tu chvíli se najednou změní barva uživatelského prostředí, tedy něco, co s tím vůbec nesouvisí. Hodiny strávíme kontrolou kódu pro změnu barvy. A pak zjistíme, že chyba je 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ý, což se může lehce stát). Ten ale povede někam, kde je již uloženého něco jiného. Tato data budou opět přepsána. Povede to k nekontrolovatelnému chování naší aplikace a může to dopadnout i takto:

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. Mimo jiné právě kvůli automatické správě paměti.

Garbage collector je vlastně program, který běží paralelně s naší aplikací v samostatném vlákně. Občas se aktivuje 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á.
Hodnota None
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 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:
{PYTHON}
class Uzivatel:
def __init__(self, jmeno, vek):
self.jmeno = jmeno
self.vek = vek
def __str__(self):
return str(self.jmeno)
# založení proměnných
u = Uzivatel("Jan Novák", 28)
v = Uzivatel("Josef Nový", 32)
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
# přiřazování
u = v
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
# změna jména
v.jmeno = "John Doe"
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
# druhá změna
v.jmeno = "John Doe"
v = None
print(f"u: {u}\nv: {v}")
print(f"u: {id(u)}\nv: {id(v)}\n")
{/PYTHON}
Vidíme, že objekt stále existuje a ukazuje na něj proměnná
u
, zatímco 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.
V příští lekci, Kopírování objektů v Pythonu, se zaměříme na způsoby, kterými lze kopírovat objekty.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 565x (2.42 kB)
Aplikace je včetně zdrojových kódů v jazyce Python