Il modulo `copy` di Python

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 memo passato 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 lista children) sono condivisi con il root_node originale. Di conseguenza, quando aggiungi un nuovo nodo a shallow_copy, la lista children condivisa viene aggiornata e la modifica si riflette anche nel contenuto di root_node.
  • D'altra parte, la variabile deep_copy è creata tramite una 'copia profonda', quindi la struttura interna è duplicata ricorsivamente, producendo un albero completamente indipendente da root_node. Pertanto, anche se aggiungi un nuovo nodo a deep_copy, ciò non influisce su root_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, deepcopy utilizza memo per 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 sia pickle sia deepcopy. È 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 deepcopy può 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 deepcopy oppure fornisci manualmente un meccanismo simile a memo.
  • 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.

YouTube Video