Python's `copy` Module

Python's `copy` Module

Dit artikel legt Python's copy-module uit.

Met de verschillen tussen ondiepe en diepe kopieën als uitgangspunt bieden we een duidelijke uitleg—van de basismechanismen van het dupliceren van objecten tot en met toepassingen in aangepaste klassen—met praktische voorbeelden.

YouTube Video

Python's copy Module

De copy-module van Python is de standaardmodule voor het behandelen van objectduplicatie (kopiëren). Het doel is het verschil tussen ondiepe en diepe kopieën te begrijpen en het kopieergedrag voor aangepaste objecten te kunnen sturen.

Basis: Wat is een ondiepe kopie?

Hier laten we het gedrag van ondiepe kopieën zien voor muteerbare objecten zoals lijsten en dictionaries. Een ondiepe kopie dupliceert alleen het object op het hoogste niveau en deelt de interne referenties.

 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)

In deze code wordt de lijst op het hoogste niveau gedupliceerd, maar de binnenste sublijsten worden door verwijzing gedeeld, waardoor shallow[1].append(99) ook wordt weerspiegeld in original. Dit is typisch gedrag van een ondiepe kopie.

Wat is een diepe kopie?

Een diepe kopie dupliceert het object en al zijn interne verwijzingen recursief, waardoor een onafhankelijk object ontstaat. Gebruik dit wanneer je complexe geneste structuren veilig wilt dupliceren.

 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)

In dit voorbeeld hebben wijzigingen aan deep geen effect op original. deepcopy kopieert de volledige objectgraaf en levert een onafhankelijke kopie op.

Hoe kies je welke je gebruikt

Welke kopie je gebruikt, wordt bepaald door de structuur van het object en je doel. Als je niet wilt dat wijzigingen aan interne muteerbare elementen het oorspronkelijke object beïnvloeden, is deepcopy geschikt. Dat komt omdat het het volledige object recursief dupliceert en zo een volledig onafhankelijke kopie maakt.

Als daarentegen het dupliceren van alleen het bovenliggende object voldoende is en je waarde hecht aan snelheid en geheugenefficiëntie, dan is copy (een ondiepe kopie) geschikter.

  • Je wilt voorkomen dat het origineel verandert wanneer je interne muteerbare elementen wijzigt → gebruik deepcopy.
  • Alleen het bovenste niveau dupliceren is genoeg en je wilt prestaties (snelheid/geheugen) prioriteren → gebruik copy (ondiepe kopie).

Omgaan met ingebouwde en onveranderlijke objecten

Onveranderlijke objecten (bijv. int, str en tuples waarvan de inhoud onveranderlijk is) hoeven doorgaans niet te worden gekopieerd. copy.copy kan wanneer passend hetzelfde object retourneren.

 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)

Omdat het kopiëren van onveranderlijke objecten weinig efficiëntiewinst oplevert, kan hetzelfde object worden hergebruikt. Dit is zelden iets waar je je in applicatieontwerp zorgen over hoeft te maken.

Aangepaste klassen: __copy__ en __deepcopy__ definiëren

Als het standaardkopiëren niet is wat je voor je klasse verwacht, kun je je eigen kopieerlogica aanbieden. Als je __copy__ en __deepcopy__ definieert, zullen copy.copy en copy.deepcopy die gebruiken.

 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
  • Door __copy__ en __deepcopy__ te implementeren, kun je het klassespecifieke kopieergedrag flexibel beheersen. Je kunt bijvoorbeeld gevallen afhandelen waarin bepaalde attributen naar hetzelfde object moeten verwijzen (delen), terwijl andere attributen als volledig nieuwe objecten gedupliceerd moeten worden.
  • Het argument memo dat aan __deepcopy__ wordt doorgegeven, is een dictionary die bijhoudt welke objecten tijdens het recursief kopiëren al zijn verwerkt en oneindige lussen door circulaire verwijzingen voorkomt.
 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)
  • In deze code wordt de variabele shallow_copy aangemaakt via een 'shallow copy', waardoor alleen het topniveau van het object wordt gedupliceerd en de objecten waar intern naar wordt verwezen (in dit geval de children-lijst) worden gedeeld met de oorspronkelijke root_node. Daardoor wordt, wanneer je een nieuw knooppunt aan shallow_copy toevoegt, de gedeelde children-lijst bijgewerkt en wordt de wijziging ook weerspiegeld in de inhoud van root_node.
  • Aan de andere kant wordt de variabele deep_copy aangemaakt via een 'deep copy', waardoor de interne structuur recursief wordt gedupliceerd en er een boom ontstaat die volledig onafhankelijk is van root_node. Daarom heeft het toevoegen van een nieuw knooppunt aan deep_copy geen invloed op root_node.

Cyclische verwijzingen (recursieve objecten) en het belang van memo

Wanneer een complexe objectgraaf naar zichzelf verwijst (cyclische verwijzingen), gebruikt deepcopy een memo-dictionary om reeds gekopieerde objecten bij te houden en oneindige lussen te voorkomen.

 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))
  • Intern gebruikt deepcopy memo om te verwijzen naar al gedupliceerde objecten, zodat veilig kopiëren mogelijk is, zelfs in het bijzijn van cycli. Je hebt een vergelijkbaar mechanisme nodig wanneer je recursie handmatig uitvoert.

copyreg en aangepaste kopieerlogica in serialisatiestijl (geavanceerd)

Als je speciaal kopieergedrag in samenhang met bibliotheken wilt registreren, kun je de standaardmodule copyreg gebruiken om vast te leggen hoe objecten worden (her)opgebouwd. Dit is nuttig voor complexe objecten en C-extensietypen.

 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)
  • Met copyreg kun je reconstructieregels aanbieden die zowel pickle als deepcopy beïnvloeden. Het is een geavanceerde API, en in de meeste gevallen volstaat __deepcopy__.

Praktische kanttekeningen en valkuilen

Er zijn meerdere belangrijke punten om correct gedrag te waarborgen bij het gebruik van de copy-module. Hieronder lichten we veelvoorkomende valkuilen toe die je tijdens ontwikkeling kunt tegenkomen en hoe je ze vermijdt.

  • Prestaties deepcopy kan veel geheugen en CPU gebruiken, dus overweeg zorgvuldig of het nodig is.
  • Elementen die je wilt delen Als je sommige attributen, zoals grote caches, gedeeld wilt laten, vermijd dan bewust het kopiëren van hun referenties in __deepcopy__.
  • Onveranderlijke interne toestand Als je intern onveranderlijke gegevens aanhoudt, is kopiëren mogelijk overbodig.
  • Threads en externe bronnen Bronnen die niet kunnen worden gekopieerd, zoals sockets en filehandles, zijn ofwel zinloos om te kopiëren of veroorzaken fouten; vermijd dus per ontwerp dat je ze kopieert.

Praktisch voorbeeld: een patroon voor het veilig bijwerken van dictionaries

Bij het bijwerken van een complex configuratieobject gebruikt dit voorbeeld deepcopy om de oorspronkelijke instellingen te beschermen.

 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)

Met deepcopy kun je veilig afgeleide configuraties maken zonder risico de standaardinstellingen te beschadigen. Dit is vooral nuttig voor geneste muteerbare structuren zoals configuraties.

Aanbevolen werkwijzen

Om de copy-module veilig en effectief te gebruiken, is het belangrijk de volgende praktische richtlijnen in gedachten te houden.

  • Kies tussen ondiepe en diepe kopieën op basis van of er wijzigingen zullen optreden en wat hun reikwijdte is.
  • Implementeer __copy__ en __deepcopy__ in aangepaste klassen om het verwachte gedrag te bereiken.
  • Omdat diepe kopieën kostbaar zijn, verminder waar mogelijk de behoefte aan kopiëren via je ontwerp. Overweeg onder andere onveranderlijkheid en expliciete kloonmethoden.
  • Bij cyclische verwijzingen: maak gebruik van deepcopy of voorzie handmatig in een memo-achtig mechanisme.
  • Ontwerp het zodanig dat externe bronnen zoals bestandshandles en threads niet worden gekopieerd.

Conclusie

De copy-module is het standaardhulpmiddel voor objectduplicatie in Python. Door de verschillen tussen ondiepe en diepe kopieën goed te begrijpen en waar nodig aangepast kopieergedrag te implementeren, kun je veilig en voorspelbaar dupliceren. Door in de ontwerpfase te verduidelijken of kopiëren echt nodig is en wat gedeeld moet worden, kun je onnodige bugs en prestatieproblemen voorkomen.

Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.

YouTube Video