Storia della domanda:
Il passaggio dei parametri in Python implementa il principio "call by object reference" (a volte chiamato call by sharing). Ciò significa che la variabile all'interno della funzione inizia a puntare allo stesso oggetto in memoria dell'argomento passato dall'esterno.
Problema:
Se la funzione modifica l'oggetto mutabile passato (ad esempio, una lista o un dizionario), le modifiche sono visibili anche all'esterno della funzione. Questo può portare a bug difficili da rilevare, specialmente se si presume che la funzione non modifichi i dati in ingresso.
Soluzione:
Per evitare effetti collaterali, è necessario creare una copia dell'oggetto all'interno della funzione oppure utilizzare strutture dati immutabili. Per la copia si usano metodi standard (ad esempio, list.copy() per le liste, dict.copy() per i dizionari o copy.deepcopy()).
Esempio di codice:
def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # Come evitare modifiche? Creare una copia: def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]
Caratteristiche chiave:
Si può essere certi che una copia di una lista tramite .copy() sia completamente indipendente dalla lista originale?
No — .copy() crea una copia superficiale. Se ci sono oggetti mutabili annidati, le modifiche a questi saranno visibili anche nell'originale.
import copy lst = [[1, 2], [3, 4]] shallow = lst.copy() shallow[0][0] = 42 print(lst) # [[42, 2], [3, 4]] deep = copy.deepcopy(lst) deep[0][0] = 100 print(lst) # [[42, 2], [3, 4]]
Restituire un nuovo oggetto basato sull'input garantisce l'assenza di modifiche nell'originale?
Non sempre. Se il nuovo oggetto utilizza parti dell'originale (ad esempio, un riferimento a una lista interna), l'oggetto originale può essere modificato.
def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]
Possono i valori di default per gli argomenti mutabili causare problemi durante chiamate multiple della funzione?
Sì — il valore di default viene calcolato solo una volta durante la definizione della funzione.
def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
Caso negativo
In una libreria di gestione delle configurazioni è stata utilizzata una lista come valore di default, che ha portato all'accumulo di elementi tra diverse chiamate della funzione. Il comportamento era imprevedibile e difficile da rilevare nel lungo periodo.
Pro:
Meno codice per chiamate ripetute, risparmio di memoria visibile.
Contro:
Comportamento implicito, difficoltà nella debug, errori a lungo termine.
Caso positivo
Uso di None come valore di default e creazione esplicita di un nuovo oggetto ad ogni chiamata.
Pro:
Prevedibilità, assenza di effetti collaterali imprevisti, affidabilità.
Contro:
Richiede consapevolezza e un po' più di codice.