Il modulo `copy` di Python
Questo articolo spiega il modulo copy di Python.
Concentrandoci sulle differenze tra copie superficiali (shallow) e profonde (deep), forniamo una spiegazione chiara—dai meccanismi di base della duplicazione degli oggetti alle applicazioni nelle classi personalizzate—accompagnata da esempi pratici.
YouTube Video
Il modulo copy di Python
Il modulo copy di Python è il modulo standard per gestire la duplicazione (copia) degli oggetti. L'obiettivo è comprendere la differenza tra copie superficiali e profonde e saper controllare il comportamento di copia per oggetti personalizzati.
Basi: che cos'è una copia superficiale?
Qui illustriamo il comportamento delle copie superficiali per oggetti mutabili come liste e dizionari. Una copia superficiale duplica solo l'oggetto di primo livello e condivide i riferimenti al suo interno.
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 questo codice, la lista di primo livello è duplicata, ma le sottoliste interne sono condivise per riferimento, quindi shallow[1].append(99) si riflette anche in original. Questo è un comportamento tipico di una copia superficiale.
Che cos'è una copia profonda?
Una copia profonda duplica ricorsivamente l'oggetto e tutti i suoi riferimenti interni, producendo un oggetto indipendente. Usala quando vuoi duplicare in modo sicuro strutture annidate complesse.
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 questo esempio, le modifiche a deep non influenzano original. deepcopy copia l'intero grafo di oggetti, ottenendo una replica indipendente.
Come decidere quale usare
La scelta del tipo di copia dipende dalla struttura dell'oggetto e dallo scopo. Se non vuoi che le modifiche agli elementi mutabili interni influenzino l'oggetto originale, deepcopy è appropriato. Questo perché duplica ricorsivamente l'intero oggetto, creando una copia completamente indipendente.
D'altra parte, se è sufficiente duplicare solo l'oggetto di primo livello e dai priorità a velocità ed efficienza della memoria, copy (copia superficiale) è più adatto.
- Vuoi evitare che l'originale cambi quando modifichi elementi mutabili interni → usa
deepcopy. - È sufficiente duplicare solo il primo livello e vuoi dare priorità alle prestazioni (velocità/memoria) → usa
copy(copia superficiale).
Gestione degli oggetti integrati (built-in) e immutabili
Gli oggetti immutabili (ad es. int, str e tuple il cui contenuto è immutabile) di solito non necessitano di essere copiati. copy.copy può restituire lo stesso oggetto quando opportuno.
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)Poiché copiare oggetti immutabili offre pochi vantaggi in termini di efficienza, lo stesso oggetto può essere riutilizzato. Raramente è qualcosa di cui preoccuparsi nella progettazione di applicazioni.
Classi personalizzate: definire __copy__ e __deepcopy__
Se la copia predefinita non è quella che ti aspetti per la tua classe, puoi fornire una tua logica di copia. Se definisci __copy__ e __deepcopy__, copy.copy e copy.deepcopy li utilizzeranno.
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- Implementando
__copy__e__deepcopy__, puoi controllare in modo flessibile il comportamento di copia specifico della classe. Ad esempio, puoi gestire i casi in cui alcuni attributi devono fare riferimento (condividere) allo stesso oggetto, mentre altri attributi devono essere duplicati come oggetti completamente nuovi. - L'argomento
memopassato a__deepcopy__è un dizionario che registra gli oggetti già elaborati durante la copia ricorsiva e previene cicli infiniti causati da riferimenti circolari.
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 questo codice, la variabile
shallow_copyè creata tramite una 'copia superficiale', quindi solo il livello superiore dell'oggetto è duplicato e gli oggetti a cui fa riferimento internamente (in questo caso, la listachildren) sono condivisi con ilroot_nodeoriginale. Di conseguenza, quando aggiungi un nuovo nodo ashallow_copy, la listachildrencondivisa viene aggiornata e la modifica si riflette anche nel contenuto diroot_node. - D'altra parte, la variabile
deep_copyè creata tramite una 'copia profonda', quindi la struttura interna è duplicata ricorsivamente, producendo un albero completamente indipendente daroot_node. Pertanto, anche se aggiungi un nuovo nodo adeep_copy, ciò non influisce suroot_node.
Riferimenti ciclici (oggetti ricorsivi) e l'importanza di memo
Quando un grafo di oggetti complesso fa riferimento a sé stesso (riferimenti ciclici), deepcopy usa un dizionario memo per tracciare gli oggetti già copiati ed evitare loop infiniti.
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))- Internamente,
deepcopyutilizzamemoper riferirsi agli oggetti già duplicati, permettendo una copia sicura anche in presenza di cicli. Hai bisogno di un meccanismo simile quando esegui la ricorsione manualmente.
copyreg e copia personalizzata in stile serializzazione (avanzato)
Se vuoi registrare un comportamento di copia speciale in coordinamento con le librerie, puoi usare il modulo standard copyreg per registrare come gli oggetti vengono (ri)costruiti. Ciò è utile per oggetti complessi e tipi di estensione 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)- Usando
copyreg, puoi fornire regole di ricostruzione che influenzano siapicklesiadeepcopy. È un'API avanzata e, nella maggior parte dei casi,__deepcopy__è sufficiente.
Avvertenze pratiche e insidie
Ci sono diversi punti importanti per garantire un comportamento corretto quando si usa il modulo copy. Di seguito spieghiamo le insidie comuni che potresti incontrare in fase di sviluppo e come evitarle.
- Prestazioni
deepcopypuò consumare molta memoria e CPU, quindi valuta attentamente se sia necessario. - Gestione degli elementi che vuoi condividere
Se vuoi che alcuni attributi, come grandi cache, rimangano condivisi, evita intenzionalmente di copiarne i riferimenti all'interno di
__deepcopy__. - Stato interno immutabile Se mantieni internamente dati immutabili, la copia potrebbe essere superflua.
- Thread e risorse esterne Risorse che non possono essere copiate, come socket e file handle, sono prive di senso da copiare o causano errori; perciò è necessario evitarne la copia a livello di progettazione.
Esempio pratico: un pattern per aggiornare in modo sicuro i dizionari
Quando si aggiorna un oggetto di configurazione complesso, questo esempio usa deepcopy per proteggere le impostazioni originali.
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)Usando deepcopy, puoi creare in sicurezza configurazioni derivate senza rischiare di danneggiare le impostazioni predefinite. Ciò è particolarmente utile per strutture mutabili annidate come le configurazioni.
Buone pratiche
Per usare il modulo copy in modo sicuro ed efficace, è importante tenere a mente le seguenti linee guida pratiche.
- Scegli tra copie superficiali e profonde in base al fatto che avverranno modifiche e alla loro portata.
- Implementa
__copy__e__deepcopy__nelle classi personalizzate per ottenere il comportamento atteso. - Poiché le copie profonde sono costose, riduci quando possibile la necessità di copiare tramite la progettazione. Considera l'immutabilità e metodi di clonazione espliciti, tra le altre tecniche.
- Quando gestisci riferimenti ciclici, sfrutta
deepcopyoppure fornisci manualmente un meccanismo simile amemo. - Progetta in modo tale che risorse esterne come handle di file e thread non vengano copiate.
Conclusione
Il modulo copy è lo strumento standard per la duplicazione degli oggetti in Python. Comprendendo correttamente le differenze tra copie superficiali e profonde e implementando un comportamento di copia personalizzato quando necessario, puoi eseguire duplicazioni in modo sicuro e prevedibile. Chiarendo in fase di progettazione se la copia sia davvero necessaria e cosa debba essere condiviso, puoi evitare bug e problemi di prestazioni inutili.
Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.