05 - Réels
Le problème fondamental
Nous avons vu comment représenter les entiers en binaire. Mais comment un ordinateur peut-il stocker un nombre comme \(3,14\) ou \(0,1\) ? Le problème est plus subtil qu’il n’y paraît.
Essayons dans Python :
>>> 0.1 + 0.2
0.30000000000000004
>>> 0.1 + 0.2 == 0.3
False
Le résultat n’est pas \(0,3\) mais \(0,30000000000000004\). Ce n’est pas un bug de Python : c’est une conséquence directe de la façon dont les nombres à virgule sont stockés en mémoire. Pour comprendre d’où vient cette erreur, il faut d’abord comprendre comment on représente les parties décimales en binaire.
Partie décimale en binaire
Rappel : la notation positionnelle
En base 10, l’écriture \(6,625\) signifie :
\[6 \times 10^0 + 6 \times 10^{-1} + 2 \times 10^{-2} + 5 \times 10^{-3}\]
Le même principe s’applique en base 2 : les chiffres après la virgule correspondent aux puissances négatives de 2.
| Position | \(2^{-1}\) | \(2^{-2}\) | \(2^{-3}\) | \(2^{-4}\) |
|---|---|---|---|---|
| Valeur | 0,5 | 0,25 | 0,125 | 0,0625 |
Conversion binaire vers décimal
On additionne les puissances de 2 correspondant aux bits à 1.
Exemple. \(110,101_2\) :
\[1 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 + 1 \times 2^{-1} + 0 \times 2^{-2} + 1 \times 2^{-3} = 4 + 2 + 0,5 + 0,125 = 6,625_{10}\]
Conversion décimal vers binaire
Pour la partie entière, on procède par divisions successives par 2 (comme dans le chapitre sur les bases). Pour la partie décimale, on utilise des multiplications successives par 2 :
- on multiplie la partie décimale par 2 ;
- la partie entière du résultat donne le bit suivant ;
- on recommence avec la nouvelle partie décimale.
Exemple. Convertissons \(0,6875_{10}\) en binaire :
0,6875 × 2 = 1,375 → bit : 1
0,375 × 2 = 0,75 → bit : 0
0,75 × 2 = 1,5 → bit : 1
0,5 × 2 = 1,0 → bit : 1
Donc \(0,6875_{10} = 0,1011_2\).
Vérification. \(0,5 + 0,125 + 0,0625 = 0,6875\). C’est correct.
Le piège : des développements infinis
Essayons maintenant avec \(0,1_{10}\) :
0,1 × 2 = 0,2 → bit : 0
0,2 × 2 = 0,4 → bit : 0
0,4 × 2 = 0,8 → bit : 0
0,8 × 2 = 1,6 → bit : 1
0,6 × 2 = 1,2 → bit : 1
0,2 × 2 = 0,4 → bit : 0 ← la séquence recommence !
0,4 × 2 = 0,8 → bit : 0
...
Le calcul ne s’arrête jamais : \(0,1_{10} = 0,0001100110011\ldots{}_2\) (la séquence \(0011\) se répète indéfiniment). Comme l’ordinateur ne dispose que d’un nombre fini de bits, il est obligé de tronquer cette suite : il ne stocke qu’une approximation de \(0,1\).
C’est exactement ce qui explique le résultat surprenant de 0.1 + 0.2 en Python.
Anecdote. Le 25 février 1991, pendant la guerre du Golfe, une batterie américaine de missiles Patriot a échoué à intercepter un missile Scud irakien, causant la mort de 28 soldats. La cause : l’horloge interne comptait le temps en dixièmes de seconde, et \(0,1\) n’a pas d’écriture finie en binaire. Après 100 heures de fonctionnement, l’erreur d’arrondi accumulée atteignait 0,34 seconde, pendant laquelle un Scud parcourt environ 500 mètres.
La norme IEEE 754
Pour que tous les ordinateurs représentent les nombres à virgule de la même façon, la norme IEEE 754 (1985) définit un format standardisé. L’idée est d’utiliser la notation scientifique binaire : tout nombre non nul peut s’écrire sous la forme :
\[(-1)^S \times 1,F \times 2^e\]
où \(S\) est le bit de signe, \(F\) la partie fractionnaire (appelée mantisse) et \(e\) l’exposant.
Structure d’un flottant en simple précision (32 bits)

| Champ | Nombre de bits | Rôle |
|---|---|---|
| Signe (S) | 1 | 0 = positif, 1 = négatif |
| Exposant (E) | 8 | Exposant biaisé (voir ci-dessous) |
| Mantisse (F) | 23 | Chiffres après la virgule du \(1,\ldots\) |
Pourquoi un exposant « biaisé » ?
L’exposant \(e\) peut être positif ou négatif (pour représenter des nombres très petits comme \(0,001\)). Plutôt que d’utiliser un bit de signe séparé pour l’exposant, la norme ajoute un biais de 127 : on stocke \(E = e + 127\).
Ainsi, un exposant effectif de \(-3\) est stocké comme \(E = -3 + 127 = 124\), et un exposant de \(+5\) comme \(E = 5 + 127 = 132\). Les valeurs stockées vont de 1 à 254 (les valeurs 0 et 255 sont réservées pour les cas spéciaux).
La formule complète est :
\[\text{valeur} = (-1)^S \times (1 + F) \times 2^{E - 127}\]
Exemple détaillé : coder \(-6,625\)
Suivons les cinq étapes de la conversion :
1. Signe. Le nombre est négatif, donc \(S = 1\).
2. Valeur absolue en binaire. \(6,625_{10} = 110,101_2\) (on a \(6 = 110_2\) et \(0,625 = 0,101_2\)).
3. Notation normalisée. On déplace la virgule pour obtenir \(1,\ldots \times 2^e\) :
\[110,101_2 = 1,10101_2 \times 2^2\]
La virgule a été décalée de 2 positions vers la gauche, donc \(e = 2\).
4. Exposant biaisé. \(E = 2 + 127 = 129_{10} = 10000001_2\).
5. Mantisse. On prend les chiffres après le \(1,\) et on complète à 23 bits :
\[10101\underbrace{000000000000000000}_{18 \text{ zéros}}\]
Résultat final :
S Exposant Mantisse
1 10000001 10101000000000000000000

Deuxième exemple : coder \(0,15625\)
1. Signe. Positif, donc \(S = 0\).
2. Valeur absolue en binaire. \(0,15625 = 0,00101_2\) (car \(0,125 + 0,03125 = 0,15625\), soit \(2^{-3} + 2^{-5}\)).
3. Notation normalisée. \(0,00101_2 = 1,01_2 \times 2^{-3}\).
4. Exposant biaisé. \(E = -3 + 127 = 124_{10} = 01111100_2\).
5. Mantisse. \(01\underbrace{000000000000000000000}_{21 \text{ zéros}}\)
Résultat :
S Exposant Mantisse
0 01111100 01000000000000000000000
Cet exemple illustre un cas où l’exposant effectif est négatif : le biais permet de le stocker comme un nombre positif (124).
Simple et double précision
La norme définit deux formats courants :
| Format | Bits | Signe | Exposant | Mantisse | Précision | Intervalle approx. |
|---|---|---|---|---|---|---|
| Simple | 32 | 1 | 8 (biais 127) | 23 | ~7 chiffres | \(\pm 3,4 \times 10^{38}\) |
| Double | 64 | 1 | 11 (biais 1023) | 52 | ~15 chiffres | \(\pm 1,8 \times 10^{308}\) |
En Python, le type float utilise toujours la double précision (64 bits).
Conséquences pratiques en Python
Ne jamais comparer des flottants avec ==
Le problème fondamental vu en introduction se manifeste chaque fois que l’on compare des résultats de calculs flottants :
>>> 0.1 + 0.2 == 0.3
False
Solutions
Arrondir avec round() :
>>> round(0.1 + 0.2, 10) == 0.3
True
Comparer avec une tolérance grâce à math.isclose() :
import math
>>> math.isclose(0.1 + 0.2, 0.3)
True
Utiliser le module decimal pour des calculs exacts :
from decimal import Decimal
>>> Decimal('0.1') + Decimal('0.2') == Decimal('0.3')
True
Accumulation d’erreurs
Les erreurs d’arrondi s’accumulent au fil des calculs. Plus on enchaîne d’opérations, plus le résultat peut s’éloigner de la valeur exacte :
i = 0.0
while i < 1000:
i += 0.01
print(i)
| Résultat attendu | Résultat obtenu |
|---|---|
| 1000.0 | 1000.01 |
Après 100 000 additions de \(0,01\), l’erreur accumulée est visible.
Pour aller plus loin : valeurs spéciales
La norme IEEE 754 réserve les exposants extrêmes (tout à 0 ou tout à 1) pour des valeurs spéciales :
- l’infini (
float('inf')etfloat('-inf')) représente un dépassement de capacité ; - NaN (Not a Number,
float('nan')) signale un résultat indéfini (comme \(0/0\) ou \(\sqrt{-1}\)). Propriété surprenante :NaN != NaNrenvoieTrue; - le zéro signé : \(+0\) et \(-0\) existent mais sont considérés comme égaux par
==.
Résumé
| Concept | Ce qu’il faut retenir |
|---|---|
| Problème fondamental | Un nombre décimal fini en base 10 peut être infini en base 2 |
| Norme IEEE 754 | Format standardisé : signe + exposant biaisé + mantisse |
| Simple précision | 32 bits (1 + 8 + 23), ~7 chiffres significatifs |
| Double précision | 64 bits (1 + 11 + 52), ~15 chiffres, utilisée par Python |
| Exposant biaisé | On ajoute 127 (simple) ou 1023 (double) à l’exposant effectif |
| Notation normalisée | Tout nombre s’écrit \(1,\ldots \times 2^e\) (le 1 est implicite) |
| Comparaison de flottants | Ne jamais utiliser == ; préférer math.isclose() |
| Accumulation d’erreurs | Les erreurs d’arrondi s’amplifient au fil des calculs |