15. Arithmétique en nombres à virgule flottante : problèmes et limites
**********************************************************************

Les nombres à virgule flottante sont représentés, au niveau matériel,
en fractions de nombres binaires (base 2). Par exemple, la fraction
décimale

   0.125

a la valeur 1/10 + 2/100 + 5/1000 et, de la même manière, la fraction
binaire

   0.001

a la valeur 0/2 + 0/4 + 1/8. Ces deux fractions ont une valeur
identique, la seule différence est que la première est une fraction
décimale, la seconde est une fraction binaire.

Malheureusement, la plupart des fractions décimales ne peuvent pas
avoir de représentation exacte en fractions binaires. Par conséquent,
en général, les nombres à virgule flottante que vous donnez sont
seulement approximés en fractions binaires pour être stockés dans la
machine.

Le problème est plus simple à aborder en base 10. Prenons par exemple,
la fraction 1/3. Vous pouvez l'approximer en une fraction décimale :

   0.3

ou, mieux,

   0.33

ou, mieux,

   0.333

etc. Peu importe le nombre de décimales que vous écrivez, le résultat
ne vaut jamais exactement 1/3, mais c'est une estimation s'en
approchant toujours mieux.

De la même manière, peu importe combien de décimales en base 2 vous
utilisez, la valeur décimale 0.1 ne peut pas être représentée
exactement en fraction binaire. En base 2, 1/10 est le nombre
périodique suivant

   0.0001100110011001100110011001100110011001100110011...

En se limitant à une quantité finie de bits, on ne peut obtenir qu'une
approximation. Sur la majorité des machines aujourd'hui, les nombres à
virgule flottante sont approximés par une fraction binaire avec les 53
premiers bits comme numérateur et une puissance de deux au
dénominateur. Dans le cas de 1/10, la fraction binaire est
"3602879701896397 / 2 ** 55" qui est proche mais ne vaut pas
exactement 1/10.

Du fait de la manière dont les flottants sont affichés par
l'interpréteur, il est facile d'oublier que la valeur stockée est une
approximation de la fraction décimale d'origine. Python n'affiche
qu'une approximation décimale de la valeur stockée en binaire. Si
Python devait afficher la vraie valeur décimale de l'approximation
binaire stockée pour 0,1, il afficherait

   >>> 0.1
   0.1000000000000000055511151231257827021181583404541015625

C'est bien plus de décimales que ce qu'attendent la plupart des
utilisateurs, donc Python affiche une valeur arrondie afin d'améliorer
la lisibilité

   >>> 1 / 10
   0.1

Rappelez-vous simplement que, bien que la valeur affichée ressemble à
la valeur exacte de 1/10, la valeur stockée est la représentation la
plus proche en fraction binaire.

Il existe beaucoup de nombres décimaux qui partagent une même
approximation en fraction binaire. Par exemple, "0.1",
"0.10000000000000001" et
"0.1000000000000000055511151231257827021181583404541015625" ont tous
pour approximation "3602879701896397 / 2 ** 55". Puisque toutes ces
valeurs décimales partagent la même approximation, chacune peut être
affichée tout en respectant "eval(repr(x)) == x".

Historiquement, le mode interactif de Python et la primitive "repr()"
choisissaient la version avec 17 décimales significatives,
"0.10000000000000001". Python, depuis la version 3.1 (sur la majorité
des systèmes) est maintenant capable de choisir la plus courte
représentation et n'affiche que "0.1".

Ce comportement est inhérent à la nature même de la représentation des
nombres à virgule flottante dans la machine : ce n'est pas un bogue
dans Python et ce n'est pas non plus un bogue dans votre code. Vous
pouvez observer le même type de comportement dans tous les autres
langages utilisant le support matériel pour le calcul des nombres à
virgule flottante (bien que certains langages ne rendent pas visible
la différence par défaut, ou pas dans tous les modes d'affichage).

Pour obtenir un affichage plus plaisant, les fonctions de formatage de
chaînes de caractères peuvent limiter le nombre de décimales
significatives affichées :

   >>> format(math.pi, '.12g')  # give 12 significant digits
   '3.14159265359'

   >>> format(math.pi, '.2f')   # give 2 digits after the point
   '3.14'

   >>> repr(math.pi)
   '3.141592653589793'

Il est important de comprendre que tout cela n'est, au sens propre,
qu'une illusion : vous demandez simplement à Python d'arrondir la
valeur stockée réellement dans la machine à *l'affichage*.

Une autre conséquence du fait que 0,1 n'est pas exactement stocké 1/10
est que la somme de trois valeurs de 0,1 ne donne pas 0,3 non plus :

   >>> .1 + .1 + .1 == .3
   False

Aussi, puisque 0,1 ne peut pas être stocké avec une représentation
plus proche de sa valeur exacte 1/10, comme 0,3 qui ne peut pas être
plus proche de sa valeur exacte 3/10, arrondir au préalable avec la
fonction "round()" n'aide en rien :

   >>> round(.1, 1) + round(.1, 1) + round(.1, 1) == round(.3, 1)
   False

Bien que les nombres ne peuvent se rapprocher plus de la valeur qu’on
attend qu’ils aient, la fonction "round()" peut être utile à
postériori pour arrondir deux valeurs inexactes et pouvoir les
comparer :

   >>> round(.1 + .1 + .1, 10) == round(.3, 10)
   True

L'arithmétique des nombres binaires à virgule flottante réserve
beaucoup de surprises de ce genre. Le problème avec « 0.1 » est
expliqué en détails ci-dessous, dans la section « Erreurs de
représentation ». Voir The Perils of Floating Point pour une liste
plus complète de ce genre de surprises.

Même s'il est vrai qu'il n'existe pas de réponse simple, ce n'est pas
la peine de vous méfier outre mesure des nombres à virgule flottante !
Les erreurs, en Python, dans les opérations de nombres à virgule
flottante sont dues au matériel sous-jacent et, sur la plupart des
machines, sont de l'ordre de 1 sur 2**53 par opération. C'est plus que
suffisant pour la plupart des tâches, mais vous devez garder à
l'esprit que ce ne sont pas des opérations décimales et que chaque
opération sur des nombres à virgule flottante peut souffrir d'une
nouvelle erreur.

Bien que des cas pathologiques existent, pour la plupart des cas
d'utilisations courants vous obtiendrez le résultat attendu à la fin
en arrondissant simplement au nombre de décimales désirées à
l'affichage avec "str()". Pour un contrôle fin sur la manière dont les
décimales sont affichées, consultez dans Syntaxe de formatage de
chaîne les spécifications de formatage de la méthode "str.format()".

Pour les cas requérant une représentation décimale exacte, le module
"decimal" peut être utile : il implémente l'arithmétique décimale et
peut donc être un choix adapté pour des applications nécessitant une
grande précision.

Une autre forme d'arithmétique exacte est implémentée dans le module
"fractions" qui se base sur les nombres rationnels (donc 1/3 peut y
être représenté exactement).

Si vous êtes un utilisateur intensif des opérations sur les nombres à
virgule flottante, nous vous conseillons de considérer le paquet
*Numerical Python* ainsi que les paquets pour les opérations
statistiques et mathématiques fournis par le projet SciPy. Consultez
<https://scipy.org>.

Python fournit des outils qui peuvent être utiles dans les rares
occasions où vous voulez réellement connaître la valeur exacte d'un
nombre à virgule flottante. La méthode "float.as_integer_ratio()"
donne la valeur du nombre sous forme de fraction :

   >>> x = 3.14159
   >>> x.as_integer_ratio()
   (3537115888337719, 1125899906842624)

Puisque le ratio est exact, il peut être utilisé pour recréer la
valeur originale sans perte :

   >>> x == 3537115888337719 / 1125899906842624
   True

La méthode "float.hex()" donne le nombre en hexadécimal (base 16),
donnant ici aussi la valeur exacte stockée par la machine :

   >>> x.hex()
   '0x1.921f9f01b866ep+1'

Cette représentation hexadécimale petit être utilisée pour
reconstruire, sans approximation, le *float* :

   >>> x == float.fromhex('0x1.921f9f01b866ep+1')
   True

Puisque cette représentation est exacte, elle est pratique pour
échanger des valeurs entre différentes versions de Python
(indépendamment de la machine) ou d'autres langages qui comprennent ce
format (tels que Java et C99).

Une autre fonction utile est "math.fsum()", elle aide à diminuer les
pertes de précision lors des additions. Elle surveille les *décimales
perdues* au fur et à mesure que les valeurs sont ajoutées au total.
Cela peut faire une différence au niveau de la précision globale en
empêchant les erreurs de s'accumuler jusqu'à affecter le résultat
final :

>>> sum([0.1] * 10) == 1.0
False
>>> math.fsum([0.1] * 10) == 1.0
True


15.1. Erreurs de représentation
===============================

Cette section explique en détail l'exemple du « 0.1 » et montre
comment vous pouvez effectuer une analyse exacte de ce type de cas par
vous-même. Nous supposons que la représentation binaire des nombres
flottants vous est familière.

Le terme *Erreur de représentation* (*representation error* en
anglais) signifie que la plupart des fractions décimales ne peuvent
être représentées exactement en binaire. C'est la principale raison
pour laquelle Python (ou Perl, C, C++, Java, Fortran et beaucoup
d'autres) n'affiche habituellement pas le résultat exact en décimal.

Pourquoi ? 1/10 n'est pas représentable de manière exacte en fraction
binaire. Cependant, toutes les machines d'aujourd'hui (novembre 2000)
suivent la norme IEEE-754 en ce qui concerne l'arithmétique des
nombres à virgule flottante et la plupart des plateformes utilisent un
« IEEE-754 double précision » pour représenter les *floats* de Python.
Les « IEEE-754 double précision » utilisent 53 bits de précision donc,
à la lecture, l'ordinateur essaie de convertir 0,1 dans la fraction la
plus proche possible de la forme *J*/2***N* avec *J* un nombre entier
d'exactement 53 bits. Pour réécrire

   1 / 10 ~= J / (2**N)

en

   J ~= 2**N / 10

en se rappelant que *J* fait exactement 53 bits (donc ">= 2**52" mais
"< 2**53"), la meilleure valeur possible pour *N* est 56 :

   >>> 2**52 <=  2**56 // 10  < 2**53
   True

Donc 56 est la seule valeur possible pour *N* qui laisse exactement 53
bits pour *J*. La meilleure valeur possible pour *J* est donc ce
quotient, arrondi :

   >>> q, r = divmod(2**56, 10)
   >>> r
   6

Puisque la retenue est plus grande que la moitié de 10, la meilleure
approximation est obtenue en arrondissant par le haut :

   >>> q+1
   7205759403792794

Par conséquent la meilleure approximation possible pour 1/10 en «
IEEE-754 double précision » est celle au-dessus de 2**56, soit :

   7205759403792794 / 2 ** 56

Diviser le numérateur et le dénominateur par deux réduit la fraction à
:

   3602879701896397 / 2 ** 55

Notez que puisque l'arrondi a été fait vers le haut, le résultat est
en réalité légèrement plus grand que 1/10 ; si nous n'avions pas
arrondi par le haut, le quotient aurait été légèrement plus petit que
1/10. Mais dans aucun cas il ne vaut *exactement* 1/10 !

Donc l'ordinateur ne « voit » jamais 1/10 : ce qu'il voit est la
fraction exacte donnée ci-dessus, la meilleure approximation utilisant
les nombres à virgule flottante double précision de l'« IEEE-754 » :

   >>> 0.1 * 2 ** 55
   3602879701896397.0

Si nous multiplions cette fraction par 10**30, nous pouvons observer
les valeurs de ses 55 décimales de poids fort :

   >>> 3602879701896397 * 10 ** 55 // 2 ** 55
   1000000000000000055511151231257827021181583404541015625

La valeur stockée dans l'ordinateur est donc égale à
0,1000000000000000055511151231257827021181583404541015625. Au lieu
d'afficher toutes les décimales, beaucoup de langages (dont les
vieilles versions de Python) arrondissent le résultat à la 17^e
décimale significative :

   >>> format(0.1, '.17f')
   '0.10000000000000001'

Les modules "fractions" et "decimal" rendent simples ces calculs :

   >>> from decimal import Decimal
   >>> from fractions import Fraction

   >>> Fraction.from_float(0.1)
   Fraction(3602879701896397, 36028797018963968)

   >>> (0.1).as_integer_ratio()
   (3602879701896397, 36028797018963968)

   >>> Decimal.from_float(0.1)
   Decimal('0.1000000000000000055511151231257827021181583404541015625')

   >>> format(Decimal.from_float(0.1), '.17')
   '0.10000000000000001'
