Historique de la question :
Le passage de paramètres en Python met en œuvre le principe de "call by object reference" (parfois appelé call by sharing). Cela signifie que la variable à l'intérieur de la fonction commence à pointer vers le même objet en mémoire que l'argument passé de l'extérieur.
Problème :
Si la fonction modifie l'objet mutable passé (par exemple, une liste ou un dictionnaire), les modifications sont également visibles à l'extérieur de la fonction. Cela peut conduire à des bugs difficiles à déceler, surtout si l'on s'attend à ce que la fonction ne modifie pas les données d'entrée.
Solution :
Pour éviter les effets secondaires, il est conseillé de faire une copie de l'objet à l'intérieur de la fonction ou d'utiliser des structures de données immuables. Pour copier, on utilise des méthodes standard (par exemple, list.copy() pour les listes, dict.copy() pour les dictionnaires ou copy.deepcopy()).
Exemple de code :
def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # Comment éviter les modifications ? Faire une copie : def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]
Points clés :
Peut-on être sûr qu'une copie de liste via .copy() est complètement indépendante de la liste d'origine ?
Non — .copy() crée une copie superficielle. Si elle contient des objets mutables imbriqués, les modifications de ceux-ci seront également visibles dans l'original.
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]]
Le fait de retourner un nouvel objet basé sur l'entrée garantit-il l'absence de modifications dans l'original ?
Pas toujours. Si l'objet nouveau utilise des parties de l'original (par exemple, une référence à une liste imbriquée), l'objet original peut être modifié.
def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]
Les arguments par défaut pour des objets mutables peuvent-ils causer des problèmes lors d'appels multiples d'une fonction ?
Oui — la valeur par défaut est calculée une seule fois lors de la définition de la fonction.
def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
Cas négatif
Dans une bibliothèque de traitement de configuration, une liste a été utilisée comme valeur par défaut, entraînant un cumul d'éléments entre différents appels de fonction. Le comportement était imprévisible et a pris longtemps à se révéler.
Avantages :
Moins de code pour des appels répétés, économie de mémoire visible.
Inconvénients :
Comportement implicite, difficultés de débogage, erreurs à long terme.
Cas positif
Utilisation de None comme valeur par défaut et création explicite d'un nouvel objet à chaque appel.
Avantages :
Prévisibilité, absence d'effets secondaires inattendus, fiabilité.
Inconvénients :
Nécessite de la conscience et un peu plus de code.