Nombres flottants et virgule flottante
Introduction
Les entiers, c'est bien — mais comment représente-t-on ou ? Les nombres réels posent un problème fondamental : il en existe une infinité entre deux entiers consécutifs, alors qu'une machine ne dispose que d'un nombre fini de bits. Il faudra donc approximer. C'est ce que fait la représentation en virgule flottante, normalisée par le standard IEEE 754 et utilisée par tous les processeurs modernes.
Cette approximation a un effet déroutant : en Python, 0.1 + 0.2 ne vaut
pas exactement 0.3. Comprendre pourquoi vous évitera bien des bugs.
L'analogie avec la notation scientifique
En mathématiques, on note souvent un grand ou un petit nombre en notation scientifique :
Trois éléments structurent cette écriture :
- une mantisse : ici ou ,
- une base : ici 10,
- un exposant : ici ou .
La virgule flottante reprend exactement cette idée, en base 2. Un flottant s'écrit alors comme :
où est le bit de signe, la mantisse (en binaire, entre 1 et 2 environ), et l'exposant (un entier signé).
La norme IEEE 754, vue simplifiée
La norme IEEE 754 fixe la répartition des bits. En simple précision (32 bits) :
| Champ | Bits | Rôle |
|---|---|---|
| Signe | 1 | 0 = positif, 1 = négatif |
| Exposant | 8 | Codé avec un biais de 127 |
| Mantisse | 23 | Partie fractionnaire après le « 1, » implicite |
En double précision (64 bits, le float Python par défaut) :
| Champ | Bits | Rôle |
|---|---|---|
| Signe | 1 | 0 = positif, 1 = négatif |
| Exposant | 11 | Biais de 1023 |
| Mantisse | 52 | Partie fractionnaire |
Pourquoi pose problème
En base 10, s'écrit simplement 0,1. Mais en base 2,
a un développement infini périodique :
Comme la mantisse a une taille bornée (52 bits en double précision), la machine tronque ce développement infini. Le résultat est une approximation très proche de , mais pas exactement .
La preuve par Python
print(0.1 + 0.2) # 0.30000000000000004
print(0.1 + 0.2 == 0.3) # False
L'écart est minuscule — de l'ordre de — mais il est non nul, et il suffit à faire échouer un test d'égalité strict. Ce comportement n'est pas un bug de Python : c'est la conséquence directe de la représentation binaire des flottants.
Sortie attendue (extrait) :
0.1000000000000000055511151231257827021181583404541015625
0.200000000000000011102230246251565404236316680908203125
0.3000000000000000166533453694
Les deux premières lignes affichent la valeur binaire exacte des floats
0.1 et 0.2 (le constructeur Decimal(float) n'arrondit pas). La 3ᵉ ligne
correspond à l'addition : Python repasse alors par le contexte Decimal
de précision par défaut, qui arrondit à 28 chiffres significatifs. La
valeur reste différente de 0.3 exact — ce qui confirme l'erreur de
représentation binaire.
Que renvoie en Python l'expression 0.1+0.2==0.30.1 + 0.2 == 0.30.1+0.2==0.3 ?
Comment comparer deux flottants
La règle d'or est simple : on ne teste jamais l'égalité stricte de deux flottants. À la place, on vérifie qu'ils sont suffisamment proches, à une tolérance près.
def proche(a, b, eps=1e-9):
return abs(a - b) < eps
print(proche(0.1 + 0.2, 0.3)) # True
Python fournit aussi math.isclose pour cette comparaison, plus robuste :
import math
print(math.isclose(0.1 + 0.2, 0.3)) # True
isclose ou
une tolérance explicite quand des flottants sont en jeu.Valeurs spéciales — inf, -inf, nan
IEEE 754 prévoit quelques valeurs particulières :
inf(infini positif) : résultat d'un dépassement ou d'une division par zéro positive.-inf(infini négatif).nan(Not a Number) : résultat d'une opération invalide, comme ou .
import math
print(math.inf) # inf — constante du module math
print(1 / 0.0) # ZeroDivisionError en Python pur (et non inf)
print(float('nan') == float('nan')) # False — un nan n'est jamais égal à lui-même !
1 / 0.0lève une ZeroDivisionError — l'infini ne sort
pas naturellement de la division. Pour obtenir inf, on passe par la
constante math.inf (ou par des bibliothèques comme NumPy, où 1 / 0.0
renvoie effectivement inf).Combien de bits utilise un float en double précision ?
Pièges courants
- Ne jamais tester
a == bpour des flottants. Utilisermath.isclose. - Additionner beaucoup de petits flottants peut perdre en précision (les petits termes deviennent négligeables face au cumul).
- Convertir une
strenfloat(float("0.1")) crée la même approximation : il n'y a pas de chemin pour obtenir un exact en flottant binaire.
Pour aller plus loin
Pour des calculs financiers ou scientifiques exigeants, Python fournit deux alternatives :
- Le module
decimal(calcul en base 10, à précision arbitraire) — bien adapté aux montants monétaires. - Le module
fractions(rationnels exacts ) — utile pour les calculs symboliques où les approximations sont interdites.
from fractions import Fraction
print(Fraction(1, 10) + Fraction(2, 10)) # 3/10
Ces alternatives sont plus lentes, mais elles fournissent le résultat exact.