En savoir plus

Comparer des nombres à virgule

Pourquoi == trahit les float, et le réflexe du seuil pour comparer des mesures sans se faire piéger.

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.

print(0.1 + 0.2 == 0.3) # False (!) print(0.1 + 0.2) # 0.30000000000000004

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écute

Mê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).

SEUIL = 0.000001 # la tolérance : en dessous, c'est « égal » masseTotale = 0.1 + 0.1 + 0.1 attendu = 0.3 if abs(masseTotale - attendu) < SEUIL: print("Les masses concordent") # c'est maintenant cette branche else: print("Les masses diffèrent")

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.

SituationSeuil raisonnablePourquoi
Balance au centigramme0.01Sous 0,01 g, la balance elle-même ne fait pas la différence
Thermomètre gradué au demi-degré0.5Deux lectures à 0,3 °C d'écart sont la même température
Montant en dollars0.005Un demi-cent : en dessous, c'est le même prix
Calcul pur, sans mesure0.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 :

import math print(math.isclose(0.1 + 0.2, 0.3)) # True print(math.isclose(3.3 / 1.1, 3.0)) # True print(math.isclose(100.0, 100.4, abs_tol=0.5)) # True (thermomètre au demi-degré)

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

Avec l'égalité stricte
a == b
-
Avec le seuil
abs(a - b) < SEUIL
-

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).