Les Crash-Devs d'un Passionné

Comprendre la gestion des floats

/Catégorie/python

Temps de lecture : 6 minutes

On a appris à l'école que 1 + 1 = 2, donc on retrouve cela également en python

clipboard
Copier le code
1
2
>>> 1 + 1
2

Si on applique ce principe sur des flottants on obtient

clipboard
Copier le code
1
2
>>> 1.0 + 1.0
2.0

Maintenant prenons un flottant avec une décimale

clipboard
Copier le code
1
2
3
4
>>> 0.1 + 0.1
0.2
>>> 0.2 + 0.1
0.30000000000000004

Mais pourquoi cette différence dans le résultat ci-dessus?

Prenons l'example de 0.625, pour connaîte la valeur binaire de la partie décimal (625) il faut multiplier par 2 cette valeur et prendre la partie entière comme valeur binaire. On continue tant que la partie décimale n'est pas nulle

0.625 * 2 = 1.25

0.25 * 2 = 0.5

0.5 * 2 = 1.0

C'est ainsi que l'on obtient 101 (base 2) pour la valeur décimale 625 (base 10) du float 0.625

Mais il faut savoir qu'il est assez rare d'otenir une valeur base 2 qui vaut exactement la valeur de la base 10, c'est vrai dans l'exemple juste avant. Mais reprenons notre exemple de

clipboard
Copier le code
1
2
>>> 0.2 + 0.1
0.30000000000000004

En fait 0.2 (et également 0.1) ne vaut pas exactement cette valeur, pour vérifier tranformont 0.2 (base 10) en base 2 avec le même principe que 0.625 vu au dessus

0.2 * 2 = 0.4

0.4 * 2 = 0.8

0.8 * 2 = 1.6

0.6 * 2 = 1.2

0.2 * 2 = 0.4

....

Et on peut continuer un bon moment comme cela. Mais alors que vaut vraiment 0.2 ?

On peut utiliser une librairie

Decimal
qui ne fait pas d'approximation comme le fait python par défaut

clipboard
Copier le code
1
2
3
>>> from decimal import Decimal
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

Et donc pour 0.2 on obtient

clipboard
Copier le code
1
2
>>> Decimal(0.2)
Decimal('0.200000000000000011102230246251565404236316680908203125')

Et c'est ainsi que lorsque l'on additionne les 2 floattants on obtient une valeur proche de 3 : 0.30000000000000004

Et pourquoi autant de chiffres après la virgule ?

Reprenons l'exemple de

clipboard
Copier le code
1
2
>>> 0.2 + 0.1
0.30000000000000004

Si on compte les décimales on en trouve 17 car python utilise la fonction

repr()
pour afficher la valeur et il a été défini que l'approximation serait de cette nature (on verra ensuite pourquoi).

On peut également utiliser la fonction

format

clipboard
Copier le code
1
2
>>> format(0.1+0.2)
'0.30000000000000004'

à laquelle on lui indique la précision de 20 chiffres après la virgule

clipboard
Copier le code
1
2
>>> format(0.1+0.2, '.20f')
'0.30000000000000004441'

Et si on veut 80 chiffres après la virgule

clipboard
Copier le code
1
2
>>> format(0.1+0.2, '.80f')
'0.30000000000000004440892098500626161694526672363281250000000000000000000000000000'

On remarque qu'après le 53ème chiffre on obtient des zéro. Pourquoi ?

Python tourne sur un moteur CPython par défaut qui est écrit en C. Et dans ce langage l'équipe dev de python se repose sur un double, pour la gestion des

float
, selon la norme IEEE 754 binary64. Donc la précision d'un float est de 64 bits

Dans le 64 bits on retrouve :

  • 1 bit de signe
  • 11 bits d'exposant
  • 53 bits de mantisse

Tout est expliqué plus en détail https://en.wikipedia.org/wiki/Double-precision_floating-point_format

On peut retrouver ces informations dans le module

sys

clipboard
Copier le code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> sys.float_info
sys.float_info(
 max=1.7976931348623157e+308,
 max_exp=1024, max_10_exp=308,
 min=2.2250738585072014e-308,
 min_exp=-1021,
 min_10_exp=-307,
 dig=15, mant_dig=53,
 epsilon=2.220446049250313e-16,
 radix=2,
 rounds=1
)

Où :

  • dig=15 est le nombre de digit après la virgule
  • mant_dig=53 est la mantisse de 53 bits
  • max=1.7976931348623157e+308 nous indique le plus grand nombre possible

La valeur 0.3 à une précision de 53 chiffres (qui n'a rien à voir avec mant_dig) qui est bien inférieure à ce que permet un double. On vient de voir que l'on peut avoir jusque 308 chiffres (propriété

max
de
sys.float_info
) ...

Maintenant que l'on connait un peu mieux la précision des floats, regardons comment faire des tests conditionnels

clipboard
Copier le code
1
2
>>> 0.3 == 0.1 + 0.2
False

Nous venons d'en faire la démonstation juste au dessus, 0.3 est une approximation (il faut réellement 0.3000000000000000444089209850062616169452667236328125) donc il parait pertinent que le résultat vaut

False

Mais comment tester l'égalité d'un flottant ?

Nous allons passé par la lib

Fraction
, car un flottant peut être converti sous cette forme :

clipboard
Copier le code
1
2
3
4
5
>>> from fractions import Fraction
>>> Fraction(0.625)
Fraction(5, 8)
>>> Fraction(0.1+0.2)
Fraction(1351079888211149, 4503599627370496)

La fraction peut également être obtenue avec cette fonction

as_integer_ratio()

clipboard
Copier le code
1
2
3
4
>>> 0.625.as_integer_ratio()
(5, 8)
>>> (0.1 + 0.2).as_integer_ratio()
(1351079888211149, 4503599627370496)

Et donc on peut faire les tests

clipboard
Copier le code
1
2
3
4
5
6
7
8
9
>>> 0.625 == 5/8
True
>>> 0.625 == Fraction(0.625)
True

>>> 0.1 + 0.2 == 1351079888211149 / 4503599627370496
True
>>> 0.1 + 0.2 == Fraction(0.1 + 0.2)
True

On peut également passer par la lib

Decimal
que nous avons vu en début d'article

clipboard
Copier le code
1
2
3
4
>>> 0.1 + 0.2 == Decimal(0.1 + 0.2)
True
>>> 0.625 == Decimal(0.625)
True

Et bien évidemment comme il s'agit d'approximation les tests suivants échouent logiquement

clipboard
Copier le code
1
2
3
4
5
6
7
8
>>> 0.1 + 0.2 == 0.3
False
>>> 0.1 + 0.2 == Decimal(0.1) + Decimal(0.2)
False
>>> 0.3 == Decimal(0.1) + Decimal(0.2)
False
>>> 0.3 == Decimal(0.1 + 0.2)
False

J'espère que ces explications vont vous permettre de mieux appréhender la manipulation des floats

Synthèse

Si vous n'avez pas besoin de précision dans les manipulations de

float
laissez
python
le faire à votre place. Sinon utilisez la librairie
Decimal
lorsque vous avez besoin de manipuler ce type et pour l'affichage la fonction
format
vous permettra d'afficher le nombre de digit voulu