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 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 funkce eval() - Metoda __repr__() vrací textovou reprezentaci objektu, kterou je možné poté předat funkci eval() 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 34x (3.46 kB)
Aplikace je včetně zdrojových kódů v jazyce Python

 

Předchozí článek
Odkazy na objekty a Garbage collector v Pythonu
Všechny články v sekci
Objektově orientované programování v Pythonu
Přeskočit článek
(nedoporučujeme)
Kvíz - Odkazy na objekty a kopírování objektů v Pythonu
Článek pro vás napsal Karel Zaoral
Avatar
Uživatelské hodnocení:
143 hlasů
Karel Zaoral
Aktivity