Secondi passi in Python: la maledizione della copia.

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

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


Per visualizzare o aggiungere un commento, accedi

Altri articoli di Andrea Barbazza

Altre pagine consultate