La copia degli oggetti è un concetto fondamentale in Python e la sua incomprensione porta spesso a bug difficili da individuare. Quando modifichi un oggetto e un altro cambia inaspettatamente, il problema risiede molto probabilmente nel modo in cui è stato copiato. Facciamo chiarezza una volta per tutte.
🔎 Perché la copia degli oggetti è così importante?
In Python, le variabili non sono “scatole” che contengono dati, ma piuttosto “etichette” o riferimenti che puntano a oggetti in memoria. Quando si esegue una semplice assegnazione, come questa:
lista_a = [1, 2, 3]
lista_b = lista_a
Non stai creando una nuova lista. Stai semplicemente creando un altro riferimento (lista_b) che punta esattamente allo stesso oggetto lista.
👉 Qualsiasi modifica a lista_a si rifletterà immediatamente su lista_b (e viceversa), perché sono lo stesso oggetto.
Analogia: Immagina di avere un documento Google Docs. Se invii un link a un amico, lui modificherà il tuo documento originale. Questa è un’assegnazione. Se invece selezioni “File” → “Crea una copia”, verrà creato un documento completamente indipendente. Il modulo copy in Python esiste proprio per creare questo tipo di copie indipendenti.
⚙️ Due tipi di copia
Il modulo copy fornisce due funzioni chiave:
copy.copy()— crea una copia superficiale (shallow copy).copy.deepcopy()— crea una copia profonda (deep copy).
La differenza principale tra le due risiede nel modo in cui gestiscono gli oggetti annidati (come liste all’interno di liste o dizionari all’interno di dizionari).
🟦 Copia superficiale (copy.copy())
copy.copy() crea un nuovo oggetto contenitore di primo livello, ma invece di copiare gli oggetti annidati, inserisce al suo interno dei riferimenti agli oggetti originali.
import copy
lista_originale = [1, 2, [3, 4]]
copia_superficiale = copy.copy(lista_originale)
# Modifica la lista annidata nell'originale
lista_originale[2][0] = 99
print(f"Originale: {lista_originale}") # Output: Originale: [1, 2, [99, 4]]
print(f"Copia superficiale: {copia_superficiale}") # Output: Copia superficiale: [1, 2, [99, 4]]
Come puoi vedere, la modifica della lista annidata ha interessato sia l’originale che la copia, perché condividono la stessa lista annidata.
Diagramma ASCII:
lista_originale → [ 1, 2, ───► lista_interna ]
copia_superficiale → [ 1, 2, ───► lista_interna ]
lista_interna → [ 99, 4 ]
🟩 Copia profonda (copy.deepcopy())
copy.deepcopy() risolve il problema dei riferimenti condivisi. Questa funzione attraversa ricorsivamente l’intero oggetto e crea copie complete e indipendenti di tutto ciò che incontra, inclusi tutti gli oggetti annidati.
import copy
lista_originale = [1, 2, [3, 4]]
copia_profonda = copy.deepcopy(lista_originale)
# Modifica la lista annidata nell'originale
lista_originale[2][0] = 99
print(f"Originale: {lista_originale}") # Output: Originale: [1, 2, [99, 4]]
print(f"Copia profonda: {copia_profonda}") # Output: Copia profonda: [1, 2, [3, 4]]
Ora, copia_profonda è completamente indipendente.
Diagramma ASCII:
lista_originale → [ 1, 2, ───► lista_a ]
copia_profonda → [ 1, 2, ───► lista_b ]
lista_a → [ 99, 4 ]
lista_b → [ 3, 4 ]
📑 Tabella riassuntiva: copy.copy() vs copy.deepcopy()
| Tipo di oggetto | Esempio | copy.copy() (superficiale) | copy.deepcopy() (profonda) |
|---|---|---|---|
| list (lista) | [1, 2, [3, 4]] | Una nuova lista, ma la lista annidata [3, 4] è condivisa | Una copia completamente indipendente; anche la lista annidata viene copiata |
| dict (dizionario) | {"a": 1, "b": {"c": 2}} | Un nuovo dizionario, ma il {"c": 2} annidato è condiviso | Un dizionario indipendente e tutti i dizionari annidati |
| set | {1, 2, (3, 4)} | Un nuovo set; gli elementi mutabili annidati sarebbero condivisi | Un nuovo set; tutti gli elementi annidati vengono copiati ricorsivamente |
| tuple (tupla) | (1, 2, [3, 4]) | Restituisce la stessa tupla, ma la lista annidata è condivisa | Una nuova tupla con una copia della lista annidata |
| str, int, float, bool | "ciao", 42, 3.14, True | Restituisce lo stesso oggetto (sono immutabili, non serve copiarli) | Come la copia superficiale |
| classe personalizzata | Nodo(1, Nodo(2)) | Un nuovo oggetto di primo livello; gli oggetti annidati sono condivisi | Una copia completamente indipendente dell’oggetto e di tutti gli oggetti annidati |
oggetto con __slots__ | Una classe con __slots__ | Viene copiato solo l’oggetto di primo livello | Vengono copiati tutti gli oggetti annidati se supportano la copia |
| file / socket / stream | open("file.txt") | Errore (oggetto non copiabile) | Errore (oggetto non copiabile) |
🧪 Esempi pratici per la tabella
📌 list
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
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
import copy
a = {1, 2, (3, 4)} # I set possono contenere solo tipi immutabili
b = copy.copy(a)
c = copy.deepcopy(a)
print(b == c) # True (ma sono due oggetti indipendenti in memoria)
📌 tuple
import copy
a = (1, 2, [3, 4]) # La tupla è immutabile, ma il suo elemento lista no
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])
📌 Classe personalizzata
import copy
class Nodo:
def __init__(self, valore, figlio=None):
self.valore = valore
self.figlio = figlio
a = Nodo(1, Nodo(2))
b = copy.copy(a)
c = copy.deepcopy(a)
a.figlio.valore = 99
print(f"copy: {b.figlio.valore}") # 99
print(f"deepcopy: {c.figlio.valore}") # 2
📌 Personalizzazione del comportamento: __copy__ e __deepcopy__
Puoi controllare il modo in cui i tuoi oggetti vengono copiati definendo metodi speciali nella loro classe.
import copy
class Personalizzata:
def __init__(self, x):
self.x = x
def __copy__(self):
print("Chiamato __copy__!")
# Restituisce una nuova istanza, ma con lo stesso riferimento ai dati interni
return Personalizzata(self.x)
def __deepcopy__(self, memo):
# memo è un dizionario per tenere traccia degli oggetti già copiati (evita la ricorsione)
print("Chiamato __deepcopy__!")
# Crea una copia completamente nuova, inclusi i dati annidati
return Personalizzata(copy.deepcopy(self.x, memo))
obj = Personalizzata([1, 2])
c1 = copy.copy(obj) # Output: Chiamato __copy__!
c2 = copy.deepcopy(obj) # Output: Chiamato __deepcopy__!
⚡ Prestazioni e insidie
- Velocità:
copy.copy()è significativamente più veloce perché non attraversa ricorsivamente l’intera struttura dei dati.copy.deepcopy()può essere molto lento per oggetti grandi e profondamente annidati. - Oggetti non copiabili: Le risorse di sistema come file aperti, socket o stream non possono essere copiate. Tentare di farlo solleverà un errore.
- Riferimenti ciclici:
deepcopyè abbastanza intelligente da gestire i riferimenti ciclici (dove un oggettoAfa riferimento aB, eBa sua volta fa riferimento adA) senza cadere in un ciclo infinito.
Quando usare cosa: Le regole principali
- Usa una copia superficiale (
copy.copy()) se il tuo oggetto contiene solo dati immutabili o se vuoi intenzionalmente che la copia e l’originale condividano oggetti annidati. È veloce ed efficiente. - Usa una copia profonda (
copy.deepcopy()) quando hai bisogno di una copia completa e indipendente al 100% dall’originale, specialmente quando lavori con strutture dati complesse (liste, dizionari, istanze di classe). - Per gli oggetti immutabili (numeri, stringhe, tuple senza elementi mutabili), la copia è generalmente inutile — Python ottimizza già il loro uso.
- Ricorda che puoi personalizzare il processo con
__copy__e__deepcopy__per un controllo completo.