Le problème
Avec des entiers, == fait exactement ce qu'on attend : 3 == 3 est vrai, point. Avec des nombres à virgule, la même question peut répondre False alors que, sur papier, les deux nombres sont égaux.
Au laboratoire, ça arrive tout le temps. Trois billes de 0,1 g sur la balance, et le programme refuse de reconnaître 0,3 g :
masseTotale = 0.1 + 0.1 + 0.1 # trois billes de 0,1 g attendu = 0.3 if masseTotale == attendu: print("Les masses concordent") else: print("Les masses diffèrent") # c'est cette branche qui s'exécuteMême piège avec une division toute simple : 3.3 / 1.1 donne 2.9999999999999996, donc 3.3 / 1.1 == 3.0 est faux.
Pourquoi, en trois lignes
L'ordinateur écrit les nombres en binaire, dans une boîte de taille fixe. Or 0.1 en binaire, c'est comme 1/3 en décimal : des chiffres à l'infini. La machine coupe, et garde un nombre très proche de 0,1, mais pas 0,1.
Chaque calcul ajoute une minuscule coupure. Deux nombres qui devraient être égaux se retrouvent à 0.00000000000000005 l'un de l'autre, et ==, qui exige l'égalité parfaite bit à bit, dit non.
Pour voir la mécanique et jouer avec une calculatrice à double affichage : les erreurs d'arrondi.
La solution : comparer avec un seuil
On ne demande plus « sont-ils exactement égaux ? » mais « sont-ils assez proches pour moi ? ». On mesure la distance entre les deux nombres avec abs(), et on la compare à un seuil (on dit aussi une tolérance, ou epsilon).
abs(a - b) donne la distance entre a et b, toujours positive. Si cette distance est plus petite que le seuil, on déclare les deux nombres égaux.
Comment choisir le seuil ? Il répond à la question : « à partir de quel écart la différence compte pour moi ? ». En sciences, la réponse vient de l'instrument.
| Situation | Seuil raisonnable | Pourquoi |
|---|---|---|
| Balance au centigramme | 0.01 | Sous 0,01 g, la balance elle-même ne fait pas la différence |
| Thermomètre gradué au demi-degré | 0.5 | Deux lectures à 0,3 °C d'écart sont la même température |
| Montant en dollars | 0.005 | Un demi-cent : en dessous, c'est le même prix |
| Calcul pur, sans mesure | 0.000000001 (1e-9) | Assez petit pour absorber les arrondis, assez grand pour les attraper tous |
Le seuil s'écrit en majuscules (SEUIL) parce que c'est une constante : on la fixe une fois en haut du programme, et on ne la change plus.
L'outil tout fait : math.isclose()
Python fournit la comparaison à un seuil toute prête dans le module math :
Sans réglage, isclose tolère un écart relatif d'un milliardième (rel_tol=1e-9) : parfait pour absorber les arrondis. Pour une mesure, on donne plutôt un écart absolu avec abs_tol, exactement comme notre SEUIL.
Avec numpy, les équivalents s'appellent np.isclose(a, b) (élément par élément) et np.allclose(a, b) (vrai si tout le tableau concorde).
Démo : les deux verdicts côte à côte
Tape deux valeurs (un petit calcul est accepté, comme 0.1 + 0.2) et un seuil. La démo montre ce que répond == et ce que répond la comparaison au seuil.
Comparer
Trois pièges qui restent
1. Arrondir n'est pas une solution. On pourrait croire que round(a, 3) == round(b, 3) règle tout. Non : deux nombres à 0.0002 d'écart peuvent tomber de chaque côté d'une frontière d'arrondi et donner deux résultats différents. Et round(2.675, 2) donne 2.67, pas 2.68, à cause du même arrondi binaire. Le seuil, lui, ne dépend d'aucune frontière.
2. Comparer à zéro. Une tolérance relative (« un milliardième de la valeur ») ne veut rien dire quand la valeur est zéro : math.isclose(0.0, 0.000000000001) répond False. Près de zéro, on donne toujours un seuil absolu : math.isclose(0.0, 0.000000000001, abs_tol=1e-9) répond True.
3. Les entiers, eux, n'ont pas ce problème. 7 == 7, compteur == 10, len(liste) == 0 : avec des int, == reste le bon outil. Le réflexe du seuil s'applique aux float, c'est-à-dire aux résultats de mesures, de divisions et de calculs à virgule.
Compléments jumeaux : les erreurs d'arrondi (d'où vient l'écart) et les lois de De Morgan (retourner une condition sans se tromper).