Retour à l'accueil

Python et IEEE 754

Publié le

Voici quelques commandes à reproduire sous l’interpréteur Python 3 permettant de mieux comprendre les limitations de la représentation IEEE 754 qui est le standard de représentation des nombres décimaux sur ordinateur. Pour ce faire, nous pouvons utiliser le module Decimal de Python 3.

Module « Decimal » de Python

Nous allons d’abord importer la librairie dans le langage Python par la commande suivante.

>>> from decimal import *

Il est possible de consulter la précision du « float » représentée dans le langage Python :

>>> getcontext().prec
28

Les « float » et les « Decimal » utilisent la notation standard IEEE 754 sur 64 bits. On peut augmenter la précision d’affichage. Cela ne change pas la précision interne. Cela change que l’affichage.

>>> getcontext().prec = 55

Nous utiliserons une représentation plus exacte du type « float » à l’aide du langage Python via la librairie « Decimal ».

Consultons le représentation interne du nombre 0,1 à l’aide de la fonction Decimal. On constate un minime résidu sur un affichage de précision de 55 nombres significatifs après la virgule.

>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

Si nous utilisons la représentation standard de Python, la précision disponible est cachée par l’interpréteur Python contrairement à la librairie « Decimal ». Voici ce que Python nous fournit normalement :

>>> 0.1
0.1

En général, les erreurs de précision, on ne veut pas les voir. C’est la même chose pour beaucoup de nombres réels. Voici un autre exemple de comparaison entre l’affichage standard Python et avec la fonction Decimal.

>>> 0.8
0.8
>>> Decimal(0.8)
Decimal('0.8000000000000000444089209850062616169452667236328125')

On constate que l’ordinateur nous cache bien les détails de la représentation des nombres réels. On peut comparer les nombres réels en Python et leur représentation Decimal à l’aide de l’opérateur ==. Voir l’exemple suivant :

>>> Decimal(0.1) == 0.1
True
>>> Decimal(0.8) == 0.8
True

La fonction Decimal nous révèle bien que la représentation n’est pas tout à faire la même. Python les considère quand même identiques en comparaison (==).

« 0.1 * 3 » est différent de « 0.3 ». Pourquoi?

Mais il y a des cas de comparaisons qui ne fonctionnent pas. C’est le cas de 0,3 et 0,1 * 3. On peut le constater dans presque tous les langages de programmation et en particulier, en Python. Examinons cela :

>>> 0.1 * 3 == 0.3
False

Faites cette comparaison dans d’autres langages de programmation et vous obtiendrez toujours faux.

Pourtant, on peut constater que « 0.1 * 2 » est égale à « 0.2 ».

>>> 0.1 * 2 == 0.2
True

Si on utilise la librairie « Decimal », on peut mieux comprendre pourquoi la comparaison pour 0,3 est fausse mais pas pour 0,2.

>>> Decimal(0.1) * 3
Decimal('0.3000000000000000166533453693773481063544750213623046875')
>>> Decimal(0.1) * 2
Decimal('0.2000000000000000111022302462515654042363166809082031250')
>>> Decimal(0.2)
Decimal('0.200000000000000011102230246251565404236316680908203125')
>>> Decimal(0.1) * 3
Decimal('0.3000000000000000166533453693773481063544750213623046875')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(0.1) * 3 - Decimal(0.3)
Decimal('2.77555756156289135105907917022705078125E-17')
>>> Decimal(0.1) * 2 - Decimal(0.2)
Decimal('0E-55')

La dernière soustraction indique une différente pratiquement nulle de 0E-55 dans le cas 0,2 mais plus significative de 0E-17 dans le cas de 0,3. Python considère qu’une différence de 0E-55 est suffisamment petite pour considérer les nombres identiques mais une différence de 0E-17 est trop grande et refuse de les considérer égaux.

« 9.99 + 5.0 + 3.0 » est différent de « 17.99 »

Voici un cas de figure où la représentation IEEE 754 ne permet pas d’obtenir une comparaison correcte.

>>> 9.99 + 5.0 + 3.0 == 17.99
False

Pourtant, une sommation partielle donne le bon résultat attendu…

>>> 9.99 + 5.0 == 14.99
True
>>> 9.99 + 3.0 == 12.99
True
>>> 5.0 + 3.0 == 8.0
True

Calculons-le avec la librairie « Decimal » :

>>> Decimal(9.99)
Decimal('9.9900000000000002131628207280300557613372802734375')
>>> Decimal(9.99) + Decimal(5.0) + Decimal(3.0)
Decimal('17.9900000000000002131628207280300557613372802734375')
>>> Decimal(17.99)
Decimal('17.989999999999998436805981327779591083526611328125')
>>> (Decimal(9.99) + Decimal(5.0) + Decimal(3.0)) - Decimal(17.99)
Decimal('1.7763568394002504646778106689453125E-15')
>>> (Decimal(9.99) + Decimal(5.0)) - Decimal(14.99)
Decimal('0E-49')
>>> (Decimal(9.99) + Decimal(3.0)) - Decimal(12.99)
Decimal('0E-49')

On constate que la différence entre 17,99 et la somme de 9,99, 5,0 et 3,0 donne une valeur de 0E-15 tandis que les différences des sommes partielles donne des valeur de 0E-49.

Précision exacte ou non?

Il arrive que des nombres réels se représentent exactement dans leur format IEEE 854. La représentation IEEE 854 convient très bien pour ces nombres réels. On peut mieux comprendre cela quand on utilise la librairie « Decimal ». Si le format IEEE 854 convient bien à un nombre réel, on obtient le même résultat. Voici des exemples :

>>> Decimal(3.25)
Decimal('3.25')
>>> Decimal(0.5)
Decimal('0.5')

Pour comprendre ce phénomène, nous allons convertir le nombre réel en sa représentation binaire sur 64 bits comme elle se retrouve dans le registre du processeur au moment d’une manipulation de l’ordinateur.

Conversion en représentation IEEE 754

Voici un programme Python qui convertit un nombre réel en sa représentation binaire sur 64 bits. Copiez le code du programme Python suivant:

# Code venant de http://www.technical-recipes.com/2012/converting-between-binary-and-decimal-representations-of-ieee-754-floating-point-numbers-in-c/

import struct

getBin = lambda x: x > 0 and str(bin(x))[2:] or "-" + str(bin(x))[3:]

def floatToBinary64(value):
    val = struct.unpack('Q', struct.pack('d', value))[0]
    return getBin(val)

def binaryToFloat(value):
    hx = hex(int(value, 2))
    return struct.unpack("d", struct.pack("q", int(hx, 16)))[0]

# floats are represented by IEEE 754 floating-point format which are
# 64 bits long (not 32 bits)

# float to binary
#binstr = floatToBinary64(19.5)
#print('Binary equivalent of 19.5:')
#print(binstr + '\n')

# binary to float
#fl = binaryToFloat(binstr)
#print('Decimal equivalent of ' + binstr)
#print(fl)

Nommez-le ieee754.py. Mettez-le dans le même dossier où vous exécutez Python. Faites les commandes suivantes :

>>> from ieee754 import *
>>> from decimal import *
>>> floatToBinary64(3.25)
'100000000001010000000000000000000000000000000000000000000000000'

On constate que la représentation binaire du nombre 3.25 ne contient que des zéros vers la fin. Il n’y a pas de “perte” de valeurs significatives en quelque sorte. Cela explique aussi pourquoi la fonction Decimal fournit un nombre identique comme suit:

>>> Decimal(3.25)
Decimal('3.25')

Essayons avec le nombre 303.3

>>> floatToBinary64(303.3)
'100000001110010111101001100110011001100110011001100110011001101'
>>> Decimal(303.3)
Decimal('303.30000000000001136868377216160297393798828125')

On constate que si la librairie « Decimal » ne fournit pas un nombre exact, la représentation IEEE 754 contient une période dans sa représentation binaire. Dans le cas de 303,3, la période est 1100.

Voici un autre exemple avec 0,1

>>> floatToBinary64(0.1)
'11111110111001100110011001100110011001100110011001100110011010'
>>> Decimal(0.1)
Decimal('0.1000000000000000055511151231257827021181583404541015625')

Il y a bien une période dans la représentation binaire du nombre 0,1 qui est de 1100.

En résumé, si la librairie « Decimal » ne fournit pas un nombre exact, la représentation IEEE 754 contient une période dans sa représentation binaire. Cela explique pourquoi la fonction Decimal ne peut pas donner une valeur identique à la représentation standard de Python. Si elle fournit un nombre exact, on constate qu’il n’y a pas de période dans la représentation binaire IEEE 754 (puisqu’elle est de taille limitée).