Logo Wedge Digital
Python Dev Troubleshooting Development Python Programming

Python troubleshooting : paramètres de fonctions

Python troubleshooting : paramètres de fonctions
Cet article fait suite à une après-midi de galère passée à chercher la cause d’un bug… mystique ? De débutant ? Ridicule ?

Voici le message d’erreur, point de départ de cet après-midi d’investigation :

TypeError: cannot concatenate ‘dict’ and ‘list’ objects
L’erreur était levée dans le corps d’une méthode où 2 dictionnaires étaient mergés. Et effectivement, un des 2 dictionnaires n’était plus un dictionnaire mais une liste.

Cette méthode fait partie d’une chaîne de traitements qui a bien les 2 dictionnaires comme point de départ. Hum, hum, hum… Ok super, je suis bien avancé là !

Alors, je vais vous la faire courte, après quelques heures de pas à pas en mode debug, nous avons trouvé le coupable. Et j’ai bien dis NOUS ! Chez Wedge Digital, nous ne laissons pas un dev se débrouiller tout seul. Il peut faire appel “à un ami” pour l’aider. Et si 2 devs ne suffisent pas, on passe à 3 devs et ainsi de suite jusqu’à résoudre le problème. Heureusement, nous n’avons pas eu besoin de mobiliser toute l’équipe de devs.

Bref, revenons à notre coupable : la façon dont sont passés les paramètres à une fonction/méthode Python.

Il faut savoir que les paramètres d’un type de base (int, str, float …) sont passés par valeur alors que les autres sont passés par référence (list, dict, tuple…).

Par valeur ? Par référence ? What?

On dit qu’une variable est passée par valeur à une fonction/méthode quand le paramètre reçoit une copie de la valeur.

On dit qu’une variable est passé par référence à une fonction/méthode quand le paramètre reçoit une référence. Ah super, merci captain obvious ! C'est-à-dire : le paramètre reçoit l’adresse de la variable. Pour ceux qui sont familiers avec le C, on parle bien de l’équivalent d’un pointeur.

Ok et donc ? Dans le premier cas, toutes les modifications appliquées au paramètre ne sortent pas de la fonction/méthode. Alors que dans le passage par référence, le paramètre pointe sur la variable (vers l’adresse mémoire) donc toutes modifications du paramètre s’appliquent aussi à la variable.

Illustrons ‘le passage par valeur’ :

variable = "moi, je suis une variable"
print(variable)
>>> moi, je suis une variable

def passe_par_valeur(parametre):
parametre = parametre.replace("une variable", "un paramètre")
return parametre

parametre = passe_par_valeur(variable)
print(f"variable='{variable}'\nparametre='{parametre}'")
>>> variable='moi, je suis une variable'
parametre='moi, je suis un paramètre'

Lors de l’appel de la fonction ‘passe_par_valeur’ la valeur de ‘variable’ est copiée dans ‘parametre’. Donc toute modification de ‘parametre’ n’impactera pas ‘variable’.

Par contre, lors d’un passage par référence, ce n’est plus le cas.

Illustrons maintenant ‘le passage par référence’ :

meilleures_bieres = ['chouffe', 'tripel karmeliet', 'trappiste']
print(meilleures_bieres)
>>> ['chouffe', 'tripel karmeliet', 'trappiste']

def ajoute(bieres, biere):
bieres.append(biere)
return bieres

# Parce que les irlandais sont jaloux des belges
meilleures_bieres_selon_les_irlandais = ajoute(meilleures_bieres, "guinness")

print(meilleures_bieres_selon_les_irlandais)
>>> ['chouffe', 'tripel karmeliet', 'trappiste', 'guinness']

# mais voilà, là, on est sur un passage par référence, donc
print(meilleures_bieres)
>>> ['chouffe', 'tripel karmeliet', 'trappiste', 'guinness']
# 'guinness' apparait aussi meilleures_bieres

Et voilà comment une variable peut être altérée à tort si on ne fait pas attention et vous coûter une après midi.

Alors comment on aurait pu éviter ça ? Tout d’abord, en étant conscient que cela existe. Ensuite, en étant vigilant lorsqu’on modifie un paramètre dans une fonction/méthode. Enfin, en clonant le paramètre avant de le modifier.

Solution 1 : ne jamais altérer un paramètre

Surtout s’il s’agit d’un paramètre reçu par référence. Ok, très bien, mais comment je fais si j’ai besoin de modifier le contenu du paramètre ?

Solution 2 : cloner le paramètre

En fonction du type du paramètre il existe une façon de le cloner/copier. Je vais particulièrement insister sur le clonage d’un dictionnaire car il y a encore une subtilité qui peut être piégeuse.

En effet, si on fait une recherche sur comment cloner un dictionnaire, nous allons tomber sur la méthode copy(). Attention, cette méthode ne fait qu’une copie superficielle. Alors qu’est-ce que cela veut dire ?

Une copie superficielle construit un nouvel objet composé puis (dans la mesure du possible) insère dans l’objet composé des références aux objets trouvés dans l’original.
Autrement dit, si le dictionnaire copié est lui-même composé d’autres dictionnaires ou listes, ceux-ci seront copiés par référence ! Ce qui nous ramène au pb sujet de cet article.

Prenons un exemple pour comprendre :

my_dict = {'a': [1, 2, 3], 'b': {'key': 'value'}}
my_copy = my_dict.copy()

# n'impacte pas my_copy
my_dict = {'c': 'only here'}
print('c' in my_copy)
>>> False

# modifie my_copy
my_dict['a'][2] = 7
print(my_copy['a'][2])
>>> 7

# modifie my_copy
my_dict['b']['key'] = 'another value'
print(my_(the copy['b']['key'])
>>> another value

La solution est donc d’utiliser plutôt deepcopy() pour faire une copie récursive.

Une copie récursive (ou profonde) construit un nouvel objet composé puis, récursivement, insère dans l’objet composé des copies des objets trouvés dans l’objet original.
notre exemple précédent :

# il faut importer la librairie copy
import copy

my_dict = {'a': [1, 2, 3], 'b': {'key': 'value'}}
my_copy = copy.deepcopy(my_dict)

# n'impacte pas my_copy
my_dict = {'c': 'only here'}
print('c' in my_copy)
>>> False

# n'impacte plus my_copy
my_dict['a'][2] = 7
print(my_copy['a'][2])
>>> 3

# n'impacte plus my_copy
my_dict['b']['key'] = 'another value'
print(my_(the copy['b']['key'])
>>> value

Conclusion

Dans une fonction/méthode, il est important de rester vigilant aux types de nos paramètres lorsqu’on souhaite y apporter des modifications car cela pourrait entrainer des effets de bord difficile à identifier par la suite…