Secondi passi in Python: la maledizione della copia.
Un giorno scoprite che a = b non crea una seconda variabile. In Python, l'assegnazione a = b di default non crea una nuova variabile separata che occupa una zona di memoria diversa da b. Piuttosto, a diventa un alias per b.
Per molti è una scoperta dolorosa, magari dopo di aver perso un giorno o più col debugger (adesso esiste questo strumento... pensate a una quarantina di anni fa...) che dava errori incomprensibili (volete un esempio? provate a prendervi un'exception del tipo "la variabile non è un integer" su una riga di codice assolutamente avulsa come una try:)... e questo perchè siete andati a modificare un campo che non pensavate di modificare (magari un target di una for).
A parte oggetti che hanno pasticciato con i metodi __copy__ e __deepcopy__, in Python la copia fisica di un oggetto avviene solo quando utilizzi esplicitamente una delle tecniche per fare una copia (come .copy() o copy.deepcopy()) per oggetti mutabili, o se stai lavorando con oggetti immutabili, che sono di per sé "copie" in memoria separata quando assegnati. Senza una copia esplicita, quando usi a = b, entrambi condividono lo stesso oggetto in memoria. Quali sono gli oggetti immutabili? "nove", 9, (1, 2, 3) sono oggetti immutabili (si, anche le tuple, non sono modificabili). Quindi se a = 9 seguito da b = a e a = 0, b rimane 9.
Ma una lista no.
Per copiare una lista, quindi, si dovrà scrivere listB = listA.copy(), dove .copy() è un metodo dell'oggetto list, oppure listB = copy.deepcopy(listA) con copy proveniente dalla libreria copy (è necessario effettuare una import copy).
La differenza tra .copy() e copy.deepcopy() è semplice: la prima crea una variabile ma gli oggetti contenuti sono condivisi, la seconda crea una variabile con contenuto differente (è una bitcopy)... quindi:
- il metodo .copy() crea una copia superficiale della lista (shallow copy), che significa che la lista originale e la copia condividono gli stessi oggetti contenuti, ma la lista stessa è separata
- se la tua variabile (esempio: una lista) contiene oggetti complessi (ad esempio, altre liste o oggetti mutabili), e vuoi che anche gli oggetti contenuti vengano copiati (non solo la variabile, la lista stessa), allora devi fare una copia profonda.
Il problema è che
- non sempre è possibile effettuare una deepcopy
- anche la copy può generare exception
Perché avviene un errore di copia per impossibilità di serializzazione?
- deepcopy: la funzione copy.deepcopy() tenta di fare una copia completa e ricorsiva di un oggetto, copiando anche tutti gli oggetti contenuti al suo interno, compresi gli oggetti mutabili (liste, dizionari, set, ecc.)... se l'oggetto che stai cercando di copiare contiene un lock o un altro oggetto non serializzabile, deepcopy fallisce.
- oggetti non serializzabili: in Python, alcuni oggetti non possono essere serializzati o "pickled" (ad esempio, thread.lock, file, socket, oggetti che gestiscono risorse di sistema)... questi oggetti sono legati al sistema operativo e non possono essere copiati (neppure con .copy()) o trasferiti tra thread o processi senza un meccanismo speciale.
Banalmente, anche
d1 = 123 # Un numero intero, che non supporta .copy()
d2 = d1.copy() # Questo solleverà un errore
Ho scritto un pacchetto di funzioni all'interno delle mie classi (il "self" che trovate disseminato è per quello) che risolve i problemi di copia... per semplificare la copia tra oggetti (soprattutto per gente come me, che non vive col manuale di python sotto il braccio), ho scritto queste 4 funzioni tra loro ricorsive facili, semplici, chiare. Le spiegazioni sono nei commenti.
Enjoy!
def BR60_FS_SafeDeepcopy(self, unkInput):
"""
Funzione ricorsiva di copia (versione completa)
Args:
unkInput: oggetto in ingresso
Return:
unkOutput: oggetto in uscita
blnOKDeepcopy (bool): True se la copia profonda non ha avuto alcun problema,
None se la copia è superficiale in toto o in parte
False se la copia non è avvenuta correttamente
"""
unkOutput = None
blnOKDeepcopy = True
# primo metodo
try:
# Prima prova con deepcopy (funziona per la maggior parte dei casi)
unkOutput = copy.deepcopy(unkInput)
return unkOutput, blnOKDeepcopy
except Exception:
pass # Se fallisce, prova il prossimo metodo
# secondo metodo
try:
# Poi prova con la copia superficiale
unkOutput = unkInput.copy()
blnOKDeepcopy = None
return unkOutput, blnOKDeepcopy
except Exception:
pass # Se fallisce, prova i metodi personalizzati
# terzo metodo: se è una lista, un set o un dizionario, gestisci i casi specifici
blnOKDeepcopy = True
if isinstance(unkInput, list):
unkOutput, blnOKDeepcopy = self.__BR60_FS_SafeListDeepcopyStep(unkInput)
elif isinstance(unkInput, set):
unkOutput, blnOKDeepcopy = self.__BR60_FS_SafeSetDeepcopyStep(unkInput)
elif isinstance(unkInput, dict):
unkOutput, blnOKDeepcopy = self.__BR60_FS_SafeDictDeepcopyStep(unkInput)
else:
# Se non è un tipo gestito, restituisci l'oggetto stesso (potenzialmente problematico)
unkOutput = unkInput
blnOKDeepcopy = False
return unkOutput, blnOKDeepcopy
def __BR60_FS_SafeListDeepcopyStep(self, lstInput):
"""
Funzione nascosta step di copia di una lista
Args:
lstInput: lista in ingresso
Return:
lstOutput: lista in uscita
blnOKDeepcopy (bool): True se la copia non ha avuto alcun problema, None o False altrimenti
"""
lstOutput = []
blnOKDeepcopy = True
Consigliati da LinkedIn
for unkInput in lstInput:
try:
unkOutput, blnOKDeepcopyCurrent = self.BR60_FS_SafeDeepcopy(unkInput)
lstOutput.append(unkOutput)
if blnOKDeepcopyCurrent == False:
blnOKDeepcopy = blnOKDeepcopyCurrent
elif blnOKDeepcopy and (blnOKDeepcopyCurrent is None):
blnOKDeepcopy = blnOKDeepcopyCurrent
except Exception:
lstOutput.append(None)
blnOKDeepcopy = False
return lstOutput, blnOKDeepcopy
def __BR60_FS_SafeSetDeepcopyStep(self, setInput):
"""
Funzione nascosta step di copia di un set
Args:
setInput: set in ingresso
Return:
setOutput: set in uscita
blnOKDeepcopy (bool): True se la copia non ha avuto alcun problema, None o False altrimenti
"""
setOutput = set()
blnOKDeepcopy = True
for unkInput in setInput:
try:
unkOutput, blnOKDeepcopyCurrent = self.BR60_FS_SafeDeepcopy(unkInput)
setOutput.add(unkOutput)
if blnOKDeepcopyCurrent == False:
blnOKDeepcopy = blnOKDeepcopyCurrent
elif blnOKDeepcopy and (blnOKDeepcopyCurrent is None):
blnOKDeepcopy = blnOKDeepcopyCurrent
except Exception:
setOutput.add(None)
blnOKDeepcopy = False
return setOutput, blnOKDeepcopy
def __BR60_FS_SafeDictDeepcopyStep(self, dctInput):
"""
Funzione nascosta step di copia di un dizionario
Args:
dctInput: dizionario in ingresso
Return:
dctOutput: dizionario in uscita
blnOKDeepcopy (bool): True se la copia non ha avuto alcun problema, None o False altrimenti
"""
dctOutput = {}
blnOKDeepcopy = True
try:
# Usa direttamente il metodo update() se possibile (copia superficiale)
dctOutput.update(dctInput)
return dctOutput, blnOKDeepcopy
except Exception:
pass # Se fallisce, procedi con la copia ricorsiva
# Calcola la lunghezza da zfill in base alla posizione corrente
intZfillLength = len(str(len(dctInput))) # La lunghezza totale del dizionario in input
# Copia ricorsiva per ciascun elemento nel dizionario
for intCurrent, (unkKey, unkInput) in enumerate(dctInput.items()):
try:
# Copia ricorsiva del valore
unkOutput, blnOKDeepcopyCurrent = self.BR60_FS_SafeDeepcopy(unkInput)
dctOutput[unkKey] = unkOutput
if blnOKDeepcopyCurrent == False:
blnOKDeepcopy = blnOKDeepcopyCurrent
elif blnOKDeepcopy and (blnOKDeepcopyCurrent is None):
blnOKDeepcopy = blnOKDeepcopyCurrent
except Exception:
# Se fallisce, gestisci l'errore e crea una chiave unica con un numero zfill dinamico
dctOutput[f"ValoreNonCopiabile_{str(intCurrent).zfill(intZfillLength)}"] = None
blnOKDeepcopy = False
return dctOutput, blnOKDeepcopy