Attention au piège Python : Arguments par défaut mutables (Python gotcha)

Python, réputé pour sa simplicité et sa lisibilité, réserve parfois des surprises inattendues pour les programmeurs non expérimentés. Un de ces cas est le piège des arguments par défaut mutables dans les fonctions.

Présentation du piège : une particularité Python !

Imaginons un instant : vous rédigez une fonction, visant l’efficacité et la flexibilité, et vous optez pour un argument par défaut – une liste vide. Prenons pour exemple une application de gestion des ressources humaines (RH) pour une entreprise. La mission ? Élaborer une fonction pour générer des profils d’employés incluant leurs noms, âges et (ici réside le défi) leurs compétences.

Dans notre quête d’une fonction robuste, nous pourrions l’architecturer ainsi :

def creerEmploye(nom, age, competences=[]):
    return {
        'nom': nom,
        'age': age,
        'competences': competences
    }

Nous initialisons deux employés, Sophie et Lucas, en supposant qu’ils débutent sans compétences enregistrées :

sophie = creerEmploye('Sophie', 28)
lucas = creerEmploye('Lucas', 25)

Maintenant vient le moment où la réalité prend une tournure inattendue. Alors que Sophie et Lucas évoluent dans leurs rôles, nous décidons d’ajouter des compétences à leurs profils à l’aide d’une méthode semblable à celle-ci :

def ajouterCompetence(employe, competence):
    employe['competences'].append(competence)
    print(employe['competences'])

Normalement, l’ajout de compétences à ces employés diligents devrait aboutir à des listes de compétences distinctes pour chacun. Cependant, la réalité nous réserve une surprise qui pourrait même dérouter les programmeurs chevronnés :

ajouterCompetence(sophie, 'Python')
ajouterCompetence(lucas, 'Data Analysis')

L’issue anticipée, [Python] pour Sophie et [Data Analysis] pour Lucas, se transforme en réalité inattendue de [Python] pour Sophie et [Python, Data Analysis] pour Lucas. Voilà le problème méconnu.

# Résultat du programme
['Python']
['Python', 'Data Analysis']

Ceci, chers développeurs, est un “gotcha” classique de Python – une particularité surprenante du langage qui peut entraîner des erreurs dans les programmes. Poursuivons pour comprendre ce phénomène et trouver une solution à ce piège.

Mutables et fonctions

Pour comprendre ce piège, commençons par établir ce que Python considère comme mutable. Un objet mutable fait référence à divers conteneurs en Python qui sont destinés à être modifiés. Par exemple, une liste a des opérations telles que “append” et “remove” qui modifient les éléments de la liste. Les ensembles et les dictionnaires sont également des objets mutables en Python, car ils peuvent être modifiés dynamiquement.

Il est utile de noter quelques objets en Python qui ne sont pas mutables (et donc acceptables en tant qu’arguments par défaut). Les entiers (int), les nombres à virgule flottante (float) et d’autres types numériques ne peuvent pas être modifiés (les opérations arithmétiques renverront un nouveau nombre). Les tuples sont une sorte de liste immuable, et les chaînes de caractères (strings) sont également immuables, car les opérations qui mettent à jour une chaîne de caractères renverront toujours une nouvelle chaîne (un nouveau objet string). Voici ci-dessous un exemple qui montre qu’en changeant une chaîne de caractère, on obtient une nouvelle chaîne :

test = "test"
print (f"test ID : {id(test)}")
test1 = "test1"
print (f"test1 ID : {id(test1)}")

# Modification de test
test =  test + test1
print (f"test ID after modification : {id(test)}")

# Resultat du code : on remarque qu'on a un nouveau ID d'un nouveau objet, après modification de test.
# test ID : 139686139589616
# test1 ID : 139686139578672
# test ID after modification : 139686139816496 

Note : Les identifiants affichés varieront selon l’ordinateur utilisé mais ils seront toujours les mêmes !

Lorsque vous utilisez un mutable en tant qu’argument de fonction, notez ce qui suit (selon la documentation officielle) :

Les valeurs par défaut des paramètres sont évaluées de gauche à droite lorsque la définition de la fonction est exécutée. Cela signifie que l’expression est évaluée une fois, lorsque la fonction est définie, et que la même valeur “précalculée” est utilisée pour chaque appel.

Cela signifie que lorsque nous appelons une fonction, les valeurs par défaut que nous fournissons pour les paramètres ne sont créées qu’une seule fois et utilisées pour chaque appel ultérieur de la fonction. Cela signifie que notre grades=[] de notre fonction précédente n’a été créé qu’une seule fois et chaque fois que nous avons essayé d’y accéder, la même liste était modifiée. Nous pouvons même constater que l’identifiant mémoire (memory id) de la propriété grades pour les deux étudiants est le même (en utilisant la fonction intégrée id()) :

# Les identifiants imprimés varieront selon l'ordinateur utilisé.
print(id(sophie['competences']))
print(id(lucas['competences']))

le résultat sera:

140121637357120
140121637357120

Note : Les identifiants affichés varieront selon l’ordinateur utilisé mais ils seront toujours les mêmes !

Bien que cela puisse sembler déconcertant et même susciter des débats parmi les passionnés de Python, il existe une solution spécifique qui nous aide à contourner ce piège si jamais nous voulons utiliser un argument par défaut mutable. Jetons un œil à une solution qui utilise la valeur None pour éviter ce piège.

Solution avec None :

La Solution avec None Si nous voulons une liste vide comme valeur d’argument par défaut potentielle, nous pouvons utiliser None comme valeur spéciale pour indiquer que nous n’avons rien reçu. Après avoir vérifié si un argument a été fourni, nous pouvons instancier une nouvelle liste si tel n’était pas le cas. Voici à quoi ressemble la solution pour notre programme précédent :

def creerEmploye(nom, age, competences=None):
  if competences is None:
    competences = []
  return {
    'nom': nom,
    'age': age,
    'competences': competences
  }

def ajouterCompetence(employe, competence):
    employe['competences'].append(competence)
    # Pour visualiser les compétences ajoutées
    print(employe['competences'])

Maintenant, si nous recréons nos employés et leur ajoutons des compétences :

sophie = creerEmploye('Sophie', 28)
lucas = creerEmploye('Lucas', 25)

ajouterCompetence(sophie, 'Python')
ajouterCompetence(lucas, 'Data Analysis')

Le résultat sera :

['Python']
['Data Analysis']

Vous vous demandez peut-être : pourquoi utiliser ces arguments mutables par défaut ?
Trois cas d’utilisation se démarquent :

  • Lier une variable locale à la valeur actuelle de la variable externe dans un rappel (callback).
  • Cache.
  • Rebinding local de noms globaux (pour un code hautement optimisé).

Pour ceux qui souhaitent en savoir plus, cette page explique en détail ces cas d’utilisation : http://effbot.org/zone/default-values.htm.

Connaissez-vous ce piège des arguments mutables par défaut en Python ? Que vous soyez familier avec ces concepts ou que vous découvriez cette notion, partagez vos pensées ! Laissez-nous savoir si vous avez déjà rencontré ces situations dans votre pratique de programmation ou partagez votre point de vue sur l’article. Votre expérience peut enrichir la discussion et aider d’autres lecteurs. Nous sommes impatients de lire vos commentaires !

One Response

  1. Nicolas December 15, 2023

Laisser un commentaire