Table of Contents
Introduction
Python est un langage elegant et puissant, mais il cache quelques pieges subtils qui peuvent surprendre meme les developpeurs experimentes. Ces pieges ne sont pas des bugs du langage, mais plutot des comportements qui decoulent de choix de conception specifiques a Python.
En tant que developpeur, vous avez probablement deja rencontre des bugs mysterieux ou des comportements inattendus dans votre code Python. Souvent, ces problemes proviennent de trois sources principales que nous allons explorer en detail dans cet article.
Pourquoi ces pieges sont-ils si courants ?
- Les arguments mutables par defaut semblent intuitifs mais cachent un comportement contre-intuitif
- La modification d’une collection pendant son iteration est une erreur naturelle a commettre
- La difference entre
iset==n’est pas toujours evidente pour les nouveaux developpeurs
Les sujets couverts seront :
- Les valeurs par defaut mutables - Pourquoi elles sont evaluees une seule fois
- La modification pendant l’iteration - Comment eviter les resultats inattendus
- L’identite vs l’egalite - Quand utiliser
iset quand utiliser==
Nous allons fournir des exemples de code detailles, des explications approfondies, et surtout des solutions pratiques que vous pourrez appliquer immediatement dans vos projets.
1. Les valeurs par defaut de fonctions et methodes
Comprendre le probleme fondamental
En Python, les arguments par defaut sont evalues une seule fois, au moment de la definition de la fonction, et non a chaque appel. Cette caracteristique est souvent source de bugs difficiles a detecter.
La valeur par defaut est partagee entre instances
Imaginez une classe Car avec une propriete wheels definie comme attribut de classe :
class Wheel:
def __init__(self, size=17):
self.size = size
# PROBLEME : attribut de classe mutable
class Car:
wheels = [] # Partage entre TOUTES les instances !
def add_wheel(self, wheel):
self.wheels.append(wheel)
# Demonstration du probleme
car1 = Car()
car1.add_wheel(Wheel())
print(f"Car1 wheels: {len(car1.wheels)}") # 1
car2 = Car()
car2.add_wheel(Wheel())
print(f"Car2 wheels: {len(car2.wheels)}") # 2 - Surprise !
print(f"Car1 wheels: {len(car1.wheels)}") # 2 - Car1 aussi affecte !
Dans cet exemple, toutes les instances de Car partagent la meme liste de roues car wheels est un attribut de classe.
La bonne facon d’initialiser les valeurs par defaut
Pour eviter ce probleme, initialisez toujours les attributs mutables dans le constructeur (__init__) :
class Car:
def __init__(self):
self.wheels = [] # Nouvelle liste pour CHAQUE instance
def add_wheel(self, wheel):
self.wheels.append(wheel)
# Verification
car1 = Car()
car1.add_wheel(Wheel())
print(f"Car1 wheels: {len(car1.wheels)}") # 1
car2 = Car()
car2.add_wheel(Wheel())
print(f"Car2 wheels: {len(car2.wheels)}") # 1 - Correct !
print(f"Car1 wheels: {len(car1.wheels)}") # 1 - Toujours correct !
Les valeurs par defaut de fonctions
Le meme probleme existe avec les fonctions. Les arguments par defaut sont evalues a la definition, pas a chaque appel :
def foo(li=[]):
li.append(1)
print(li)
# Avec argument explicite - fonctionne comme attendu
foo([2]) # [2, 1]
foo([3]) # [3, 1]
# Sans argument - comportement inattendu
foo() # [1]
foo() # [1, 1] - La liste s'accumule !
foo() # [1, 1, 1] - Continue a s'accumuler !
La solution idiomatique Python
Utilisez None comme valeur par defaut et creez l’objet mutable a l’interieur de la fonction :
def foo(li=None):
if li is None:
li = []
li.append(1)
print(li)
foo() # [1]
foo() # [1] - Correct !
foo() # [1] - Toujours correct !
Cas d’utilisation avance : exploiter ce comportement
Dans certains cas rares, ce comportement peut etre utile, par exemple pour implementer un cache simple :
def fibonacci(n, cache={}):
if n in cache:
return cache[n]
if n <= 1:
return n
result = fibonacci(n-1) + fibonacci(n-2)
cache[n] = result
return result
# Le cache persiste entre les appels
print(fibonacci(10)) # 55
print(fibonacci(100)) # Tres rapide grace au cache
2. La modification d’une sequence pendant une iteration
Comprendre pourquoi c’est dangereux
Modifier une liste pendant qu’on l’itere est l’une des erreurs les plus courantes en Python. Le probleme vient du fait que l’iterateur utilise un index interne qui devient desynchronise lorsque la taille de la liste change.
Probleme lors de la suppression d’elements
Imaginons que vous voulez supprimer tous les elements d’une liste :
alist = [0, 1, 2]
for index, value in enumerate(alist):
print(f"Index: {index}, Value: {value}, List: {alist}")
alist.pop(index)
print(f"Resultat final: {alist}") # [1] - Pas vide !
Que se passe-t-il ?
- Iteration 0 : on supprime l’element a l’index 0 (valeur 0), la liste devient
[1, 2] - Iteration 1 : l’index est maintenant 1, on supprime l’element a l’index 1 (valeur 2), la liste devient
[1] - Iteration 2 : l’index 2 n’existe plus, la boucle se termine
L’element 1 n’a jamais ete visite car il est “passe” a l’index 0 pendant que l’iterateur avancait a l’index 1.
Solutions recommandees
Solution 1 : Iterer en sens inverse
En iterant de la fin vers le debut, les suppressions n’affectent pas les indices des elements restants :
alist = [1, 2, 3, 4, 5]
for index, item in reversed(list(enumerate(alist))):
if item % 2 == 0:
alist.pop(index)
print(alist) # [1, 3, 5]
Solution 2 : List comprehension (recommande)
La facon la plus pythonique et la plus performante :
alist = [1, 2, 3, 4, 5]
alist = [item for item in alist if item % 2 != 0]
print(alist) # [1, 3, 5]
Solution 3 : Utiliser filter()
Pour les cas plus complexes ou avec des fonctions existantes :
alist = [1, 2, 3, 4, 5]
alist = list(filter(lambda x: x % 2 != 0, alist))
print(alist) # [1, 3, 5]
Solution 4 : Creer une nouvelle liste
Si vous devez effectuer des operations plus complexes :
zlist = [1, 2, 3, 4, 5]
z_temp = []
for item in zlist:
if item % 2 != 0:
z_temp.append(item)
zlist = z_temp
print(zlist) # [1, 3, 5]
Attention aux dictionnaires aussi
Le meme probleme existe avec les dictionnaires :
# ERREUR : RuntimeError: dictionary changed size during iteration
d = {'a': 1, 'b': 2, 'c': 3}
# for key in d:
# if d[key] == 2:
# del d[key]
# SOLUTION : copier les cles
d = {'a': 1, 'b': 2, 'c': 3}
for key in list(d.keys()): # list() cree une copie
if d[key] == 2:
del d[key]
print(d) # {'a': 1, 'c': 3}
3. L’identite des entiers et des chaines de caracteres
Comprendre is vs ==
Avant d’aborder le caching, clarifions la difference fondamentale :
==compare les valeurs (egalite)iscompare les identites (meme objet en memoire)
a = [1, 2, 3]
b = [1, 2, 3]
c = a
print(a == b) # True - memes valeurs
print(a is b) # False - objets differents en memoire
print(a is c) # True - c pointe vers le meme objet que a
Les entiers internes sont stockes en cache
Pour des raisons de performance, Python pre-cree et met en cache les entiers de -5 a 256 (plage frequemment utilisee). Cela peut conduire a des comportements surprenants :
# Dans la plage du cache (-5 a 256)
a = 100
b = 100
print(a is b) # True - meme objet en cache
# Hors de la plage du cache
x = 1000
y = 1000
print(x is y) # False - objets differents
# Exemples avec calculs
-8 is (-7 - 1) # False (hors cache)
-3 is (-2 - 1) # True (dans cache)
(255 + 1) is 256 # True (dans cache)
(256 + 1) is 257 # False (hors cache)
Regle d’or : Utilisez toujours == pour comparer des valeurs :
-8 == (-7 - 1) # True
-3 == (-2 - 1) # True
(255 + 1) == 256 # True
(256 + 1) == 257 # True
Les chaines de caracteres et l’interning
Python utilise egalement un mecanisme appele string interning pour les chaines qui ressemblent a des identifiants (lettres, chiffres, underscores) :
# Chaines simples - internees
a = 'hello'
b = 'hello'
print(a is b) # True
# Concatenation a la compilation
'python' is 'py' + 'thon' # True (optimise par le compilateur)
# Chaines avec espaces - pas internees
x = 'hello world'
y = 'hello world'
print(x is y) # Resultat imprevisible !
# Concatenation a l'execution
s1 = 'py'
s2 = 'thon'
print('python' is s1 + s2) # False (pas optimise)
Regle d’or : Utilisez toujours == pour comparer des chaines :
'python' == 'py' + 'thon' # True - toujours fiable
Cas particulier : None
None est un singleton en Python. Il n’existe qu’une seule instance de None, donc utiliser is est non seulement correct mais recommande :
x = None
# Recommande
if x is None:
print("x est None")
# Fonctionne mais moins idiomatique
if x == None:
print("x est None")
Bonnes Pratiques
Resume des patterns a adopter
-
Arguments par defaut mutables
# MAL def func(items=[]): items.append(1) return items # BIEN def func(items=None): if items is None: items = [] items.append(1) return items -
Modification pendant iteration
# MAL for item in items: if condition(item): items.remove(item) # BIEN items = [item for item in items if not condition(item)] -
Comparaison de valeurs
# MAL (pour les valeurs) if x is 100: pass # BIEN if x == 100: pass # Exception : None if x is None: # Correct ! pass
Pieges Courants
Les erreurs les plus frequentes
| Piege | Symptome | Solution |
|---|---|---|
| Liste par defaut | Donnees partagees entre appels | Utiliser None |
| Dict par defaut | Idem | Utiliser None |
| Supprimer en iterant | Elements manques | List comprehension ou reversed |
is avec entiers | Resultats incoherents | Utiliser == |
is avec chaines | Resultats imprevisibles | Utiliser == |
Comment detecter ces problemes
# Verifier les arguments par defaut d'une fonction
def func(items=[]):
pass
print(func.__defaults__) # ([],) - meme objet a chaque appel !
# Verifier l'identite des objets
print(id(257)) # Adresse memoire
print(id(257)) # Adresse differente !
Conclusion
Ces trois pieges Python sont parmi les plus courants et les plus frustrants pour les developpeurs. Ils decoulent de decisions de conception legitimes du langage (performance, simplicite), mais peuvent causer des bugs subtils si on ne les comprend pas.
Points cles a retenir :
-
Arguments mutables par defaut : Toujours utiliser
Nonecomme valeur par defaut pour les listes, dictionnaires et autres objets mutables. Creez l’objet dans le corps de la fonction. -
Modification pendant l’iteration : Ne jamais modifier une collection pendant qu’on l’itere. Preferez les list comprehensions ou iterez en sens inverse.
-
Identite vs Egalite : Utilisez
==pour comparer des valeurs, reservezisuniquement pourNone,True,Falseet les comparaisons de types.
En appliquant ces bonnes pratiques, vous eviterez des heures de debogage et ecrirez du code Python plus robuste et plus maintenable. Ces concepts sont egalement des questions classiques en entretien technique, donc les maitriser vous sera doublement utile !
In-Article Ad
Dev Mode
Tags
Mahmoud DEVO
Senior Full-Stack Developer
I'm a passionate full-stack developer with 10+ years of experience building scalable web applications. I write about Vue.js, Node.js, PostgreSQL, and modern DevOps practices.
Enjoyed this article?
Subscribe to get more tech content delivered to your inbox.
Related Articles
Recherche de valeurs dans les listes, tuples et dictionnaires Python
Apprenez a rechercher des elements dans les sequences Python : methode index(), mot-cle in, recherche dans les dictionnaires et algorithme bisect pour listes triees.
Expressions Regulieres en Python : Guide Complet pour Maitriser le Module re
Apprenez a maitriser les expressions regulieres en Python avec le module re. Decouvrez comment extraire des donnees, valider des formats, manipuler des chaines et eviter les pieges courants avec des exemples pratiques.
Gestion des packages Python : creer et utiliser requirements.txt efficacement
Guide complet pour gerer vos dependances Python avec pip. Apprenez a creer un fichier requirements.txt, utiliser les environnements virtuels et maitriser la gestion des packages pour des projets Python professionnels et reproductibles.