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

Floating-point numbers are represented in computer hardware as base 2 (binary) fractions. For example, the decimal fraction 0.125 has value 1/10 + 2/100 + 5/1000, and in the same way the binary fraction 0.001 has value 0/2 + 0/4 + 1/8. These two fractions have identical values, the only real difference being that the first is written in base 10 fractional notation, and the second in base 2.

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.

Many users are not aware of the approximation because of the way values are displayed. Python only prints a decimal approximation to the true decimal value of the binary approximation stored by the machine. On most machines, if Python were to print the true decimal value of the binary approximation stored for 0.1, it would have to display

>>> 0.1
0.1000000000000000055511151231257827021181583404541015625

That is more digits than most people find useful, so Python keeps the number of digits manageable by displaying a rounded value instead

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

For more pleasant output, you may wish to use string formatting to produce a limited number of significant digits:

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

One illusion may beget another. For example, since 0.1 is not exactly 1/10, summing three values of 0.1 may not yield exactly 0.3, either:

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

Also, since the 0.1 cannot get any closer to the exact value of 1/10 and 0.3 cannot get any closer to the exact value of 3/10, then pre-rounding with round() function cannot help:

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

Though the numbers cannot be made closer to their intended exact values, the round() function can be useful for post-rounding so that results with inexact values become comparable to one another:

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

Binary floating-point arithmetic holds many surprises like this. The problem with "0.1" is explained in precise detail below, in the "Representation Error" section. See Examples of Floating Point Problems for a pleasant summary of how binary floating-point works and the kinds of problems commonly encountered in practice. Also see The Perils of Floating Point for a more complete account of other common 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).

If you are a heavy user of floating-point operations you should take a look at the NumPy package and many other packages for mathematical and statistical operations supplied by the SciPy project. See <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.

Why is that? 1/10 is not exactly representable as a binary fraction. Since at least 2000, almost all machines use IEEE 754 binary floating-point arithmetic, and almost all platforms map Python floats to IEEE 754 binary64 "double precision" values. IEEE 754 binary64 values contain 53 bits of precision, so on input the computer strives to convert 0.1 to the closest fraction it can of the form J/2**N where J is an integer containing exactly 53 bits. Rewriting

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

Therefore the best possible approximation to 1/10 in IEEE 754 double precision is:

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 !

So the computer never "sees" 1/10: what it sees is the exact fraction given above, the best IEEE 754 double approximation it can get:

>>> 0.1 * 2 ** 55
3602879701896397.0

Si nous multiplions cette fraction par 10**55, 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 17e 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'