Moduł `copy` Pythona

Moduł `copy` Pythona

Ten artykuł wyjaśnia moduł copy Pythona.

Koncentrując się na różnicach między płytkimi a głębokimi kopiami, przedstawiamy jasne wyjaśnienie — od podstawowych mechanizmów duplikowania obiektów po zastosowania w klasach niestandardowych — wraz z praktycznymi przykładami.

YouTube Video

Moduł copy Pythona

Moduł copy w Pythonie jest standardowym modułem do obsługi duplikowania (kopiowania) obiektów. Celem jest zrozumienie różnicy między kopiami płytkimi i głębokimi oraz możliwość kontrolowania zachowania kopiowania dla obiektów niestandardowych.

Podstawy: czym jest płytka kopia?

Tutaj ilustrujemy zachowanie płytkich kopii dla obiektów mutowalnych, takich jak listy i słowniki. Płytka kopia duplikuje tylko obiekt najwyższego poziomu i współdzieli wewnętrzne referencje.

 1# Example: shallow copy with lists and dicts
 2import copy
 3
 4# A nested list
 5original = [1, [2, 3], 4]
 6shallow = copy.copy(original)
 7
 8# Mutate nested element
 9shallow[1].append(99)
10
11print("original:", original)  # nested change visible in original
12print("shallow: ", shallow)

W tym kodzie lista najwyższego poziomu jest zduplikowana, ale wewnętrzne podlisty są współdzielone przez odwołanie, więc shallow[1].append(99) znajduje odzwierciedlenie także w original. To typowe zachowanie płytkiej kopii.

Czym jest głęboka kopia?

Głęboka kopia rekurencyjnie duplikuje obiekt oraz wszystkie jego wewnętrzne referencje, tworząc niezależny obiekt. Używaj jej, gdy chcesz bezpiecznie zduplikować złożone, zagnieżdżone struktury.

 1# Example: deep copy with nested structures
 2import copy
 3
 4original = [1, [2, 3], {'a': [4, 5]}]
 5deep = copy.deepcopy(original)
 6
 7# Mutate nested element of the deep copy
 8deep[1].append(100)
 9deep[2]['a'].append(200)
10
11print("original:", original)  # original remains unchanged
12print("deep:    ", deep)

W tym przykładzie zmiany w deep nie wpływają na original. deepcopy kopiuje cały graf obiektów, tworząc niezależną replikę.

Jak zdecydować, której użyć

Wybór rodzaju kopii powinien wynikać ze struktury obiektu i celu, jaki chcesz osiągnąć. Jeśli nie chcesz, aby zmiany wewnętrznych elementów mutowalnych wpływały na obiekt oryginalny, właściwe będzie użycie deepcopy. Dzieje się tak, ponieważ rekurencyjnie duplikuje cały obiekt, tworząc całkowicie niezależną kopię.

Z drugiej strony, jeśli wystarczy zduplikować jedynie obiekt najwyższego poziomu i zależy ci na szybkości oraz oszczędności pamięci, bardziej odpowiednia będzie copy (kopia płytka).

  • Chcesz uniknąć zmian w oryginale podczas modyfikacji wewnętrznych elementów mutowalnych → użyj deepcopy.
  • Wystarczy duplikacja tylko najwyższego poziomu i chcesz priorytetowo traktować wydajność (szybkość/pamięć) → użyj copy (kopia płytka).

Obsługa obiektów wbudowanych i niemutowalnych

Obiekty niemutowalne (np. int, str oraz krotki o niemutowalnej zawartości) zazwyczaj nie wymagają kopiowania. copy.copy może, gdy to zasadne, zwrócić ten sam obiekt.

 1# Example: copying immutables
 2import copy
 3
 4a = 42
 5b = copy.copy(a)
 6print(a is b)  # True for small ints (implementation detail)
 7
 8s = "hello"
 9t = copy.deepcopy(s)
10print(s is t)  # True (strings are immutable)

Ponieważ kopiowanie obiektów niemutowalnych daje niewielkie korzyści wydajnościowe, ten sam obiekt może być ponownie użyty. Rzadko jest to coś, czym trzeba się przejmować przy projektowaniu aplikacji.

Klasy niestandardowe: definiowanie __copy__ i __deepcopy__

Jeśli domyślne kopiowanie nie odpowiada potrzebom twojej klasy, możesz dostarczyć własną logikę kopiowania. Jeśli zdefiniujesz __copy__ i __deepcopy__, copy.copy i copy.deepcopy będą z nich korzystać.

 1# Example: customizing copy behavior
 2import copy
 3
 4class Node:
 5    def __init__(self, value, children=None):
 6        self.value = value
 7        self.children = children or []
 8
 9    def __repr__(self):
10        return f"Node({self.value!r}, children={self.children!r})"
11
12    def __copy__(self):
13        # Shallow copy: create new Node, but reuse children list reference
14        new = self.__class__(self.value, self.children)
15        return new
16
17    def __deepcopy__(self, memo):
18        # Deep copy: copy value and deepcopy each child
19        new_children = [copy.deepcopy(child, memo) for child in self.children]
20        new = self.__class__(copy.deepcopy(self.value, memo), new_children)
21        memo[id(self)] = new
22        return new
  • Implementując __copy__ i __deepcopy__, możesz elastycznie kontrolować specyficzne dla klasy zachowanie kopiowania. Na przykład możesz obsłużyć przypadki, w których niektóre atrybuty powinny odwoływać się do (współdzielić) tego samego obiektu, podczas gdy inne atrybuty powinny być duplikowane jako zupełnie nowe obiekty.
  • Argument memo przekazywany do __deepcopy__ to słownik, który zapisuje obiekty już przetworzone podczas rekurencyjnego kopiowania i zapobiega nieskończonym pętlom spowodowanym przez cykliczne referencje.
 1# Build tree
 2root_node = Node('root', [Node('child1'), Node('child2')])
 3
 4shallow_copy = copy.copy(root_node)   # shallow copy
 5deep_copy = copy.deepcopy(root_node)  # deep copy
 6
 7# Modify children
 8# affects both root_node and shallow_copy
 9shallow_copy.children.append(Node('child_shallow'))
10# affects deep_copy only
11deep_copy.children.append(Node('child_deep'))
12
13# Print results
14print("root_node:", root_node)
15print("shallow_copy:", shallow_copy)
16print("deep_copy:", deep_copy)
  • W tym kodzie zmienna shallow_copy jest utworzona poprzez 'płytką kopię', więc duplikowany jest tylko najwyższy poziom obiektu, a obiekty, do których odwołuje się on wewnętrznie (w tym przypadku lista children), są współdzielone z oryginalnym root_node. W rezultacie, gdy dodasz nowy węzeł do shallow_copy, współdzielona lista children zostaje zaktualizowana i zmiana jest również odzwierciedlona w zawartości root_node.
  • Z kolei zmienna deep_copy jest tworzona poprzez 'głęboką kopię', więc wewnętrzna struktura jest rekurencyjnie duplikowana, tworząc drzewo całkowicie niezależne od root_node. Dlatego nawet jeśli dodasz nowy węzeł do deep_copy, nie wpływa to na root_node.

Cykliczne referencje (obiekty rekurencyjne) i znaczenie memo

Gdy złożony graf obiektów odwołuje się do siebie (cykliczne referencje), deepcopy używa słownika memo do śledzenia już skopiowanych obiektów i zapobiegania nieskończonym pętlom.

 1# Example: recursive list and deepcopy memo demonstration
 2import copy
 3
 4a = []
 5b = [a]
 6a.append(b)  # a -> [b], b -> [a]  (cycle)
 7
 8# deepcopy can handle cycles
 9deep = copy.deepcopy(a)
10print("deep copy succeeded, length:", len(deep))
  • Wewnątrz deepcopy używa memo do odwoływania się do już zduplikowanych obiektów, co umożliwia bezpieczne kopiowanie nawet w obecności cykli. Podczas ręcznego wykonywania rekurencji potrzebujesz podobnego mechanizmu.

copyreg i niestandardowe kopiowanie w stylu serializacji (zaawansowane)

Jeśli chcesz zarejestrować specjalne zachowanie kopiowania we współpracy z bibliotekami, możesz użyć standardowego modułu copyreg, aby zarejestrować sposób (re)konstrukcji obiektów. Jest to przydatne dla złożonych obiektów i typów rozszerzeń w C.

 1# Example: using copyreg to customize pickling/copy behavior (brief example)
 2import copy
 3import copyreg
 4
 5class Wrapper:
 6    def __init__(self, data):
 7        self.data = data
 8
 9def reduce_wrapper(obj):
10    # Return callable and args so object can be reconstructed
11    return (Wrapper, (obj.data,))
12
13copyreg.pickle(Wrapper, reduce_wrapper)
14
15w = Wrapper([1, 2, 3])
16w_copy = copy.deepcopy(w)
17print("w_copy.data:", w_copy.data)
  • Za pomocą copyreg możesz dostarczyć reguły rekonstrukcji, które wpływają zarówno na pickle, jak i na deepcopy. To zaawansowane API i w większości przypadków __deepcopy__ jest wystarczające.

Praktyczne uwagi i pułapki

Istnieje kilka ważnych kwestii, aby zapewnić poprawne zachowanie podczas używania modułu copy. Poniżej wyjaśniamy typowe pułapki, na które możesz natrafić podczas tworzenia oprogramowania, oraz jak ich unikać.

  • Wydajność deepcopy może pochłaniać dużo pamięci i czasu CPU, dlatego dokładnie rozważ, czy jest konieczne.
  • Obsługa elementów, które chcesz współdzielić Jeśli chcesz, aby niektóre atrybuty, takie jak duże pamięci podręczne, pozostały współdzielone, celowo unikaj kopiowania ich referencji w __deepcopy__.
  • Niemutowalny stan wewnętrzny Jeśli wewnętrznie przechowujesz dane niemutowalne, kopiowanie może być zbędne.
  • Wątki i zasoby zewnętrzne Zasoby, których nie można kopiować, takie jak gniazda sieciowe i uchwyty plików, są albo bezcelowe do kopiowania, albo spowodują błędy, dlatego należy z założenia unikać ich kopiowania.

Praktyczny przykład: wzorzec bezpiecznej aktualizacji słowników

Podczas aktualizowania złożonego obiektu konfiguracji ten przykład używa deepcopy, aby chronić oryginalne ustawienia.

 1# Example: safely update a nested configuration using deepcopy
 2import copy
 3
 4default_config = {
 5    "db": {"host": "localhost", "ports": [5432]},
 6    "features": {"use_cache": True, "cache_sizes": [128, 256]},
 7}
 8
 9# Create a working copy to modify without touching defaults
10working = copy.deepcopy(default_config)
11working["db"]["ports"].append(5433)
12working["features"]["cache_sizes"][0] = 512
13
14print("default_config:", default_config)
15print("working:", working)

Dzięki deepcopy możesz bezpiecznie tworzyć konfiguracje pochodne bez ryzyka uszkodzenia ustawień domyślnych. Jest to szczególnie przydatne w przypadku zagnieżdżonych, mutowalnych struktur, takich jak konfiguracje.

Najlepsze praktyki

Aby bezpiecznie i skutecznie korzystać z modułu copy, ważne jest, aby pamiętać o następujących praktycznych wskazówkach.

  • Wybieraj między kopiami płytkimi a głębokimi w zależności od tego, czy zajdą zmiany i jaki będzie ich zakres.
  • Zaimplementuj __copy__ i __deepcopy__ w klasach niestandardowych, aby uzyskać oczekiwane zachowanie.
  • Ponieważ kopie głębokie są kosztowne, kiedy to możliwe ograniczaj potrzebę kopiowania już na etapie projektu. Rozważ m.in. niemutowalność i jawne metody klonowania.
  • Przy cyklicznych referencjach skorzystaj z deepcopy lub ręcznie zapewnij mechanizm podobny do memo.
  • Projektuj w taki sposób, aby zewnętrzne zasoby, takie jak uchwyty plików i wątki, nie były kopiowane.

Wnioski

Moduł copy jest standardowym narzędziem do duplikowania obiektów w Pythonie. Dzięki poprawnemu zrozumieniu różnic między kopiami płytkimi i głębokimi oraz wdrożeniu niestandardowego zachowania kopiowania w razie potrzeby możesz duplikować obiekty bezpiecznie i przewidywalnie. Precyzując już na etapie projektowania, czy kopiowanie jest rzeczywiście konieczne i co powinno być współdzielone, możesz uniknąć zbędnych błędów i problemów z wydajnością.

Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.

YouTube Video