Lekce 7 - Kopírování objektů v Pythonu
V minulé lekci, Odkazy na objekty a Garbage collector v Pythonu, jsme si vysvětlili jak fungují reference na objekty.
V dnešním tutoriálu objektově orientovaného programování v Pythonu si důkladně vysvětlíme způsoby, kterými lze vytvořit kopie objektu. Ukážeme si jejich výhody i nevýhody a také rizika, která jsou s nimi spojená.
Kopírování objektů
V běžném programování často pracujeme s objekty a manipulujeme s nimi. To zahrnuje i vytváření jejich kopií. Z předchozích lekcí už víme, že když máme dvě reference na stejný objekt a změníme jednu, druhá se změní také, protože obě reference ukazují na stejnou paměťovou oblast. Jenže v některých situacích chceme, aby původní data zůstala nedotčena, například když provádíme nějaké dočasné změny pro analýzu nebo zpracování dat. Mít kopii objektu nám umožní provádět změny právě jen na kopii, zatímco původní data zůstanou nezměněna. Nemusíme se také obávat vedlejších efektů spojených se sdílením referencí na objekty mezi různými částmi programu. Jak tedy můžeme vytvořit opravdovou kopii objektu?
Možností, jak kopírovat objekty v Pythonu, máme několik:
- Kopie pomocí konstruktoru - Vytvoří novou instanci objektu pomocí stávajícího konstruktoru a předání potřebných atributů.
- Mělká kopie pomocí funkce
copy()
- Rychlé kopírování, které vytváří nový objekt, ale v případě složených objektů (např. seznamů v seznamu) kopíruje pouze odkazy na vnitřní objekty. - Hluboká kopie pomocí funkce
deepcopy()
- Vytváří nový objekt a rekurzivně kopíruje všechny vnitřní objekty. Výsledkem je zcela nezávislá kopie původního objektu. - Kopie pomocí metody
__repr__()
a funkceeval()
- Metoda__repr__()
vrací textovou reprezentaci objektu, kterou je možné poté předat funkcieval()
pro dynamické vytvoření nové instance objektu.
V následujících kapitolách lekce se podíváme na každou z těchto možností podrobněji, abychom lépe pochopili jejich funkci a vhodné použití.
Kopie pomocí konstruktoru
Když chceme vytvořit kopii objektu v Pythonu, jedním z přímých způsobů je využít již existující konstruktor třídy. Toto je často intuitivní metoda, protože se jedná o vytvoření nové instance objektu s původními hodnotami.
Uveďme si příklad na základě naší třídy Kostka
.
Máme-li již existující instanci s určitým počtem stěn, dokážeme snadno
vytvořit novou instanci této třídy, předat jí stejný počet stěn a tím
pádem vytvořit její kopii:
class Kostka:
def __init__(self, pocet_sten=6):
self._pocet_sten = pocet_sten
def __str__(self):
return str(f"Kostka s {self._pocet_sten} stěnami.")
def vrat_pocet_sten(self):
return self._pocet_sten
def hod(self):
import random as _random
return _random.randint(1, self._pocet_sten)
# Vytvoříme instanci kostky s 6 stěnami
moje_kostka = Kostka(6)
print(moje_kostka)
# Vytvoříme kopii instance moje_kostka pomocí konstruktoru
kopie_kostky = Kostka(moje_kostka.vrat_pocet_sten())
print(kopie_kostky)
Ve výstupu uvidíme:
Kopie objektu pomocí konstruktoru:
Kostka s 6 stěnami.
Kostka s 6 stěnami.
Kopii instance puvodni_kostka
jsme vytvořili jednoduše tím,
že jsme znovu vytvořili instanci třídy Kostka
a předali ji
počet stěn z původního objektu. Výsledkem jsou dva různé objekty s
identickými hodnotami, ale oba objekty jsou na sobě zcela nezávislé.
Je důležité si uvědomit, že při kopírování objektu tímto způsobem musíme zkopírovat všechny jeho atributy, aby kopie byla opravdu kompletní a přesně odpovídala originálu.
Mělká kopie pomocí funkce
copy()
Mělká kopie objektu vytvoří nový objekt, ale nekopíruje vnořené objekty, na které původní objekt odkazuje. V mnoha případech může být mělká kopie dostačující, avšak je důležité chápat její omezení.
V případě naší třídy Kostka
, která má pouze základní
atributy, bude mělká kopie fungovat bez problémů, protože neexistují
žádné vnořené objekty ke sdílení.
Zde je ukázka, jak vytvořit mělkou kopii objektu třídy
Kostka
pomocí funkce copy()
z modulu
copy
:
import copy as cp # Vytvoříme instanci kostky s 6 stěnami puvodni_kostka = Kostka(6) print(puvodni_kostka) # Vytvoříme mělkou kopii instance puvodni_kostka melka_kopie_kostky = cp.copy(puvodni_kostka) print(melka_kopie_kostky)
Ve výstupu uvidíme:
Kopie objektu pomocí funkce copy():
Kostka s 6 stěnami.
Kostka s 6 stěnami.
Uveďme si ještě příklad s vnořenými objekty:
Máme třídu Kostka
a třídu Hrac
, kde hráč
bude mít svou kostku:
import copy as cp class Kostka: def __init__(self, pocet_sten=6): self._pocet_sten = pocet_sten def __str__(self): return str(f"Kostka s {self._pocet_sten} stěnami.") ... class Hrac: def __init__(self, jmeno, kostka): self.jmeno = jmeno self.kostka = kostka def __str__(self): return f"{self.jmeno}: {self.kostka}"
Nyní vytvoříme hráče s konkrétní kostkou:
kostka = Kostka(6) hrac = Hrac("Pavel", kostka) print(hrac)
Ve výstupu uvidíme:
Kopie objektu pomocí funkce copy():
Pavel: Kostka s 6 stěnami.
Vytvoříme mělkou kopii hráče a poté změníme počet stěn. A ano, porušujeme zde pravidlo o privátních atributech, ale jen pro účely objasnění mechanismu kopie:
kopie_hrace = cp.copy(hrac) # Změníme původní kostku kostka._pocet_sten = 8 print(hrac) print(kopie_hrace)
Ve výstupu uvidíme:
Kopie objektu pomocí funkce copy():
Pavel: Kostka s 8 stěnami.
Pavel: Kostka s 8 stěnami.
Z výstupu je zřejmé, že i když jsme změnili původní kostku až po
vytvoření kopie hráče, změnil se počet jejích stěn i u této kopie.
Důvodem je to, že mělká kopie sice vytvořila nový objekt třídy
Hrac
, ale vnořený objekt třídy Kostka
zůstal
stejný a oba hráči na něj odkazují.
Hluboká kopie pomocí funkce
deepcopy()
K vytvoření kompletní kopie objektu včetně všech vnořených objektů
máme k dispozici v modulu copy
funkci deepcopy()
.
Tato funkce prochází celým objektem a rekurzivně vytváří kopie všech
vnořených objektů.
Podívejme se na náš předchozí příklad s hráčem a kostkou:
import copy as cp class Kostka: def __init__(self, pocet_sten=6): self._pocet_sten = pocet_sten def __str__(self): return str(f"Kostka s {self._pocet_sten} stěnami.") ... class Hrac: def __init__(self, jmeno, kostka): self.jmeno = jmeno self.kostka = kostka def __str__(self): return f"{self.jmeno}: {self.kostka}" kostka = Kostka(6) hrac = Hrac("Pavel", kostka) # Vytvoříme hlubokou kopii hráče kopie_hrace = cp.deepcopy(hrac) # Změníme původní kostku kostka._pocet_sten = 8 print(hrac) print(kopie_hrace)
Ve výstupu uvidíme:
Kopie objektu pomocí funkce deepcopy():
Pavel: Kostka s 8 stěnami.
Pavel: Kostka s 6 stěnami.
Jak vidíme, změna původní kostky nijak neovlivnila kostku u kopie
hráče. To je zásluha hluboké kopie, která zajišťuje, že všechny
vnořené objekty jsou také zkopírovány, a tedy jsou úplně nezávislé na
originálech. Tyto vlastnosti dělají z deepcopy()
nejjednodušší variantu vytvoření kopie objektu. Pozor ale na fakt, že
metoda deepcopy()
je velmi náročná na
výkon.
Tato metoda je vhodná zejména v případech, kdy pracujeme s komplexními objekty s mnoha vnořenými atributy nebo objekty, které mohou být měněny v budoucnu a chceme se vyvarovat nežádoucím efektům.
Kopie pomocí metody
__repr__()
a funkce eval()
V Pythonu existují dvě základní metody pro vytváření textové
reprezentace objektů. Nám už dobře známá metoda __str__()
a
také zatím tajemná metoda __repr__()
. Zatímco
__str__()
je určená pro "hezkou" textovou reprezentaci objektu
pro koncového uživatele, __repr__()
má za cíl vytvořit
"oficiální" textovou reprezentaci objektu. Jak si to představit? Metoda
__repr__()
vrací řetězec, který po předání funkci
eval()
vytvoří objekt, jež je ekvivalentní původnímu objektu.
Tedy kopii
Zde vzniká otázka: je __repr__()
jakýmsi "dumpem" části
paměti s objektem, uložené v textovém řetězci? Odpověď je ne. Místo
toho je to spíše kódový výraz, který, když je vyhodnocen, vrátí novou
instanci stejného objektu se stejnými daty. Mějme tedy třídu, jejíž
__repr__()
metoda vrátí kód potřebný k vytvoření nové
instance s týmiž daty. Funkce eval()
pak vytvoří její novou
instanci. Výsledný objekt bude mít stejné metody, protože je stále
instancí stejné třídy.
Ukažme si příklad:
class Kostka: def __init__(self, pocet_sten=6): self._pocet_sten = pocet_sten def __str__(self): return f"Kostka s {self._pocet_sten} stěnami." ... def __repr__(self): return f"Kostka({self._pocet_sten})" puvodni_kostka = Kostka(8) # 8-stěnná kostka kopie_kostky = eval(repr(puvodni_kostka)) print(puvodni_kostka) print(kopie_kostky)
Ve výstupu uvidíme:
Kopie objektu pomocí metody __repr__():
Kostka s 8 stěnami.
Kostka s 8 stěnami.
Metodu __repr__()
jsme přidali tak, aby reprezentovala instanci
třídy Kostka
ve formátu, který by bylo možné použít k
opětovnému vytvoření stejné instance. V našem případě metoda
__repr__()
vrací řetězec ve formátu
Kostka(pocet_sten)
, kde pocet_sten
je aktuální
počet stěn kostky.
Tato metoda kopírování se zaměřuje především na data a ne na složité chování objektu nebo vnořené objekty.
Je ale třeba mít na paměti, že funkce eval()
je ze své
podstaty (tvorba kódu z řetězce) riziková a snadno se stane zdrojem
vážných bezpečnostních problémů.
Nikdy proto nepoužívejme funkci eval()
na
datech, u kterých si nejsme jistí, že pochází z ověřených zdrojů.
To je pro tuto lekci vše
V následujícím kvízu, Kvíz - Odkazy na objekty a kopírování objektů v Pythonu, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.
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 106x (3.46 kB)
Aplikace je včetně zdrojových kódů v jazyce Python