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.625" has
value 6/10 + 2/100 + 5/1000, and in the same way the **binary**
fraction "0.101" has value 1/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:

   >>> 0.1 + 0.1 + 0.1 == 0.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(0.1, 1) + round(0.1, 1) + round(0.1, 1) == round(0.3, 1)
   False

Though the numbers cannot be made closer to their intended exact
values, the "math.isclose()" function can be useful for comparing
inexact values:

   >>> math.isclose(0.1 + 0.1 + 0.1, 0.3)
   True

Alternatively, the "round()" function can be used to compare rough
approximations:

   >>> round(math.pi, ndigits=2) == round(22 / 7, ndigits=2)
   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 provides tools that may help on those rare occasions when you
really *do* want to know the exact value of a float.  The
"float.as_integer_ratio()" method expresses the value of a float as a
fraction:

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

Since the ratio is exact, it can be used to losslessly recreate the
original value:

   >>> x == 3537115888337719 / 1125899906842624
   True

The "float.hex()" method expresses a float in hexadecimal (base 16),
again giving the exact value stored by your computer:

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

This precise hexadecimal representation can be used to reconstruct the
float value exactly:

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

Another helpful tool is the "sum()" function which helps mitigate
loss-of-precision during summation.  It uses extended precision for
intermediate rounding steps as values are added onto a running total.
That can make a difference in overall accuracy so that the errors do
not accumulate to the point where they affect the final total:

   >>> 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 == 1.0
   False
   >>> sum([0.1] * 10) == 1.0
   True

The "math.fsum()" goes further and tracks all of the "lost digits" as
values are added onto a running total so that the result has only a
single rounding.  This is slower than "sum()" but will be more
accurate in uncommon cases where large magnitude inputs mostly cancel
each other out leaving a final sum near zero:

   >>> arr = [-0.10430216751806065, -266310978.67179024, 143401161448607.16,
   ...        -143401161400469.7, 266262841.31058735, -0.003244936839808227]
   >>> float(sum(map(Fraction, arr)))   # Exact summation with single rounding
   8.042173697819788e-13
   >>> math.fsum(arr)                   # Single rounding
   8.042173697819788e-13
   >>> sum(arr)                         # Multiple roundings in extended precision
   8.042178034628478e-13
   >>> total = 0.0
   >>> for x in arr:
   ...     total += x                   # Multiple roundings in standard precision
   ...
   >>> total                            # Straight addition has no correct digits!
   -0.0051575902860057365


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

and recalling that *J* has exactly 53 bits (is ">= 2**52" but "<
2**53"), the best value for *N* is 56:

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

That is, 56 is the only value for *N* that leaves *J* with exactly 53
bits.  The best possible value for *J* is then that quotient rounded:

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

Since the remainder is more than half of 10, the best approximation is
obtained by rounding up:

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

If we multiply that fraction by 10**55, we can see the value out to 55
decimal digits:

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

meaning that the exact number stored in the computer is equal to the
decimal value
0.1000000000000000055511151231257827021181583404541015625. Instead of
displaying the full decimal value, many languages (including older
versions of Python), round the result to 17 significant digits:

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

The "fractions" and "decimal" modules make these calculations easy:

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