Kopiowanie obiektów to fundamentalna koncepcja w Pythonie, a jej niezrozumienie często prowadzi do trudnych do znalezienia błędów. Kiedy modyfikujesz jeden obiekt, a inny nieoczekiwanie się zmienia, problem najprawdopodobniej leży w sposobie, w jaki został skopiowany. Wyjaśnijmy to raz na zawsze.
🔎 Dlaczego kopiowanie obiektów jest w ogóle ważne?
W Pythonie zmienne to nie „pudełka” z danymi, ale raczej „etykiety” lub referencje (odwołania) wskazujące na obiekty w pamięci. Kiedy wykonujesz proste przypisanie, takie jak to:
lista_a = [1, 2, 3]
lista_b = lista_a
Nie tworzysz nowej listy. Tworzysz po prostu kolejną referencję (lista_b), która wskazuje na dokładnie ten sam obiekt listy.
👉 Każda zmiana w lista_a zostanie natychmiast odzwierciedlona w lista_b (i na odwrót), ponieważ jest to ten sam obiekt.
Analogia: Wyobraź sobie, że masz dokument w Google Docs. Jeśli wyślesz znajomemu link do niego, będzie on edytował Twój oryginalny dokument. To jest właśnie przypisanie. Jeśli jednak wybierzesz „Plik” → „Utwórz kopię”, powstanie całkowicie niezależny dokument. Moduł copy w Pythonie istnieje właśnie po to, aby tworzyć tego rodzaju niezależne kopie.
⚙️ Dwa rodzaje kopiowania
Moduł copy dostarcza dwie kluczowe funkcje:
copy.copy()— tworzy kopię płytką (shallow copy).copy.deepcopy()— tworzy kopię głęboką (deep copy).
Główna różnica między nimi polega na tym, jak traktują obiekty zagnieżdżone (takie jak listy wewnątrz list lub słowniki wewnątrz słowników).
🟦 Kopia płytka (copy.copy())
copy.copy() tworzy nowy obiekt-kontener na najwyższym poziomie, ale zamiast kopiować obiekty zagnieżdżone, wstawia do niego referencje do oryginalnych obiektów.
import copy
lista_oryginalna = [1, 2, [3, 4]]
kopia_plytka = copy.copy(lista_oryginalna)
# Zmodyfikuj zagnieżdżoną listę w oryginale
lista_oryginalna[2][0] = 99
print(f"Oryginał: {lista_oryginalna}") # Wydruk: Oryginał: [1, 2, [99, 4]]
print(f"Kopia płytka: {kopia_plytka}") # Wydruk: Kopia płytka: [1, 2, [99, 4]]
Jak widać, modyfikacja zagnieżdżonej listy wpłynęła zarówno na oryginał, jak i na kopię, ponieważ współdzielą one tę zagnieżdżoną listę.
Diagram ASCII:
lista_oryginalna → [ 1, 2, ───► lista_wewnetrzna ]
kopia_plytka → [ 1, 2, ───► lista_wewnetrzna ]
lista_wewnetrzna → [ 99, 4 ]
🟩 Kopia głęboka (copy.deepcopy())
copy.deepcopy() rozwiązuje problem współdzielonych referencji. Ta funkcja rekurencyjnie przechodzi przez cały obiekt i tworzy pełne, niezależne kopie wszystkiego, co napotka, włączając w to wszystkie obiekty zagnieżdżone.
import copy
lista_oryginalna = [1, 2, [3, 4]]
kopia_gleboka = copy.deepcopy(lista_oryginalna)
# Zmodyfikuj zagnieżdżoną listę w oryginale
lista_oryginalna[2][0] = 99
print(f"Oryginał: {lista_oryginalna}") # Wydruk: Oryginał: [1, 2, [99, 4]]
print(f"Kopia głęboka: {kopia_gleboka}") # Wydruk: Kopia głęboka: [1, 2, [3, 4]]```
Teraz `kopia_gleboka` jest całkowicie niezależna.
**Diagram ASCII:**
lista_oryginalna → [ 1, 2, ───► lista_a ]
kopia_gleboka → [ 1, 2, ───► lista_b ]
lista_a → [ 99, 4 ]
lista_b → [ 3, 4 ]
---
#### **📑 Ściągawka: `copy.copy()` vs `copy.deepcopy()`**
| Typ obiektu | Przykład | `copy.copy()` (płytka) | `copy.deepcopy()` (głęboka) |
| :--- | :--- | :--- | :--- |
| **list** (lista) | `[1, 2, [3, 4]]` | Nowa lista, ale zagnieżdżona lista `[3, 4]` jest **współdzielona** | Całkowicie niezależna kopia; zagnieżdżona lista również jest kopiowana |
| **dict** (słownik) | `{"a": 1, "b": {"c": 2}}` | Nowy słownik, ale zagnieżdżony `{"c": 2}` jest **współdzielony** | Niezależny słownik i wszystkie zagnieżdżone słowniki |
| **set** (zbiór) | `{1, 2, (3, 4)}` | Nowy zbiór; zagnieżdżone elementy modyfikowalne byłyby **współdzielone** | Nowy zbiór; wszystkie zagnieżdżone elementy są kopiowane rekurencyjnie |
| **tuple** (krotka) | `(1, 2, [3, 4])` | Zwraca tę samą krotkę, ale zagnieżdżona lista jest **współdzielona** | Nowa krotka z **kopią** zagnieżdżonej listy |
| **str, int, float, bool** | `"cześć"`, `42`, `3.14`, `True` | Zwraca ten sam obiekt (są niemutowalne, nie ma potrzeby kopiować) | To samo co kopia płytka |
| **klasa niestandardowa** | `Wezel(1, Wezel(2))` | Nowy obiekt na najwyższym poziomie; obiekty zagnieżdżone są **współdzielone** | Całkowicie niezależna kopia obiektu i wszystkich zagnieżdżonych obiektów |
| **obiekt z `__slots__`** | Klasa z `__slots__` | Kopiowany jest tylko obiekt najwyższego poziomu | Kopiowane są wszystkie zagnieżdżone obiekty, jeśli wspierają kopiowanie |
| **plik / gniazdo / strumień**| `open("file.txt")` | **Błąd** (obiekt niekopiowalny) | **Błąd** (obiekt niekopiowalny) |
---
#### **🧪 Przykłady na żywo do tabeli**
📌 **list**
python
import copy
a = [1, 2, [3, 4]]
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f”copy: {b}”) # [1, 2, [99, 4]]
print(f”deepcopy: {c}”) # [1, 2, [3, 4]]
📌 **dict**
python
import copy
a = {„a”: 1, „b”: {„c”: 2}}
b = copy.copy(a)
c = copy.deepcopy(a)
a[„b”][„c”] = 42
print(f”copy: {b}”) # {’a’: 1, 'b’: {’c’: 42}}
print(f”deepcopy: {c}”) # {’a’: 1, 'b’: {’c’: 2}}
📌 **set**
python
import copy
a = {1, 2, (3, 4)} # Zbiory mogą zawierać tylko typy niemutowalne
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (ale to są dwa niezależne obiekty w pamięci)
📌 **tuple**
python
import copy
a = (1, 2, [3, 4]) # Krotka jest niemutowalna, ale jej element-lista już nie
b = copy.copy(a)
c = copy.deepcopy(a)
a[2][0] = 99
print(f”copy: {b}”) # (1, 2, [99, 4])
print(f”deepcopy: {c}”) # (1, 2, [3, 4])
📌 **Klasa niestandardowa**
python
import copy
class Wezel:
def init(self, wartosc, dziecko=None):
self.wartosc = wartosc
self.dziecko = dziecko
a = Wezel(1, Wezel(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.dziecko.wartosc = 99
print(f”copy: {b.dziecko.wartosc}”) # 99
print(f”deepcopy: {c.dziecko.wartosc}”) # 2
---
#### **📌 Dostosowywanie zachowania: `__copy__` i `__deepcopy__`**
Możesz kontrolować sposób kopiowania własnych obiektów, definiując w ich klasie specjalne metody.
python
import copy
class Niestandardowa:
def init(self, x):
self.x = x
def copy(self):
print(„Wywołano copy!”)
# Zwróć nową instancję, ale z tą samą referencją do danych wewnętrznych
return Niestandardowa(self.x)
def deepcopy(self, memo):
# memo to słownik śledzący już skopiowane obiekty (zapobiega rekurencji)
print(„Wywołano deepcopy!”)
# Stwórz całkowicie nową kopię, włączając dane zagnieżdżone
return Niestandardowa(copy.deepcopy(self.x, memo))
obj = Niestandardowa([1, 2])
c1 = copy.copy(obj) # Wydruk: Wywołano copy!
c2 = copy.deepcopy(obj) # Wydruk: Wywołano deepcopy!
„`
⚡ Wydajność i pułapki
- Szybkość:
copy.copy()jest znacznie szybsze, ponieważ nie przechodzi rekurencyjnie przez całą strukturę danych.copy.deepcopy()może być bardzo wolne dla dużych i głęboko zagnieżdżonych obiektów. - Obiekty niekopiowalne: Zasoby systemowe, takie jak otwarte pliki, gniazda (sockets) czy strumienie, nie mogą być kopiowane. Próba zrobienia tego spowoduje błąd.
- Referencje cykliczne:
deepcopyjest wystarczająco inteligentne, aby poradzić sobie z referencjami cyklicznymi (gdy obiektAodnosi się doB, aBz powrotem doA) bez wpadania w nieskończoną pętlę.
Kiedy czego używać: Główne zasady
- Używaj kopii płytkiej (
copy.copy()), jeśli Twój obiekt zawiera tylko dane niemutowalne lub jeśli celowo chcesz, aby kopia i oryginał współdzieliły zagnieżdżone obiekty. Jest to szybkie i wydajne. - Używaj kopii głębokiej (
copy.deepcopy()), gdy potrzebujesz pełnej, 100% niezależności kopii od oryginału, zwłaszcza podczas pracy ze złożonymi strukturami danych (listami, słownikami, instancjami klas). - Dla obiektów niemutowalnych (liczby, ciągi znaków, krotki bez modyfikowalnych elementów) kopiowanie jest generalnie bezcelowe — Python i tak optymalizuje ich użycie.
- Pamiętaj, że możesz dostosować proces za pomocą
__copy__i__deepcopy__dla pełnej kontroli.