pt
Moulé à la louche depuis 1999
Les trucs qui m'énervent... et je vais pas prendre de pincettes
Internet, informatique, logiciel libre, économie, politique, vie courante et tout le reste...

1,7 est différent de 1,7 ?

Vendredi 27 janvier 2012

Je reprend ici un petit article technique que j'avais écrit en anglais pour ma page de snippets Python.

Il sert juste à illustrer le fait que certaines notions en informatique ne sont pas du tout intuitives, et qu'il est utile d'apprendre tout cela. Le domaine du développement logiciel est plein de petits chausses-trappes de ce genre, et voir des développeurs se prendre les pieds dans le tapis me fait sourire (pas méchamment, je vous l'assure).


Je vais ici parler d'opérations élémentaires: Additionner et comparer des nombres. Élémentaires, mais pas évidentes pour autant. Cela a l'air très simple, mais rien qu'à ce niveau, il y a de quoi rendre perplexe le développeur débutant. J'ai choisi ici d'illustrer avec des exemples en langage Python, mais ce problème se reproduit dans la plupart les langages.


Commençons par créer deux variables (en Python):

a = 1.7
b = 0.9 + 0.8

En principe, a et b contiennent la même valeur, on est bien d'accord ? Affichons cela pour vérifier:

print a
print b
Ce qui affiche:
1.7
1.7

Bien. Maintenant comparons a et b:

if a == b:
    print "a et b sont égaux."
else:
    print "a et b sont différents !"

En principe, il doit afficher "a et b sont égaux", n'est-ce pas ? Faux !

a et b sont différents !

Comment est-ce possible ? Comment 1,7 peut-il être différent de 1,7 ?


Il y a deux choses qui sont à l'origine de cette incompréhension et qu'il fondamentale de connaître:
  • L'ordinateur stock ses données en binaire.
  • Il ne faut pas confondre une donnée et sa représentation textuelle.

A l'instant même où vous faites a=1.7, la variable a ne contient pas 1,7, mais une approximation binaire de la valeur décimale 1,7, c'est à dire quelque chose du genre: 1.10110011001100110011001100110011... (Avec les bits précédents, il s'agit de la valeur 1.699999999953434)

En binaire, on peut facilement stocker les fractions dont le diviseur est une puissance de deux. Quelques exemples:
  • 0,25 (1/4) en décimal vaut 0,01 en binaire.
  • 0,125 (1/8) en décimal vaut 0,001 en binaire.
  • 3/4 + 5/8 (0,75+0,625) = 1,375 en décimal vaut 1,011 en binaire.
Mais dans tous les autres cas, l'ordinateur est bien en mal de stocker la valeur. Vous pouvez tester en ligne avec ce petit convertisseur. L'ordinateur fait donc deux approximations:
  • Une approximation quand vous stockez la valeur 1,7 dans a: Il arrondit le décimale au binaire le plus proche.
  • Une seconde approximation quand vous affichez la valeur avec un print: Il arrondit le binaire au décimal le plus proche.

De la même manière, quand vous affichez une date:

print datetime.datetime.now()
2012-01-27 21:25:20.904000

Il faut bien garder à l'esprit que "2012-01-27 21:25:20.904000" n'est pas la date: C'est une représentation textuelle d'une donnée codée en binaire dans la mémoire de l'ordinateur. Ce que vous voyez à l'écran n'est pas forcément exactement ce qu'il y a dans la mémoire de l'ordinateur.

Bien sûr, il existe des bibliothèques mathématiques et des codages binaires qui permettent de stocker correctement des nombres décimaux, mais cela fait du travail en plus pour le processeur.

Alors comment comparer nos variables a et b précédentes sans recourir à ces codages spéciaux ? Soit en considérant que les valeurs sont égales si elles sont très proches:

if abs(a-b) < 0.00001:
    print "a et b sont égaux."
else:
    print "a et b sont différents !"

ou même en les convertissant en chaînes de caractères:

if str(a) == str(b):
    print "a et b sont égaux."
else:
    print "a et b sont différents !"


Et oui, l'ordinateur a ses limites, et même quand on développe dans un langage "haut niveau", il est bon de les connaître. ;-)


EDIT: Quelques précisions (signalées par Benjamin et Kévin):
  • Pour éviter ce problème de représentation binaire, certains langages ont adopté des codages différents. (EDIT: Voir ci-dessous). Le codage à virgule flottante le plus répandu est l'IEEE-754, supporté par une majorité de langages et processeurs.
  • A défaut d'utiliser ces codages, vous pouvez - pour les langages le supportant - surcharger l'opérateur == pour effectuer un test plus poussé en cas d'inégalité.

EDIT: Jean-Francois me fait remarquer que je me suis un peu raté sur l'IEEE-754: C'est également un standard à virgule flottante, donc soumis aux mêmes problèmes d'imprécision. La différence est que l'IEEE-754 définit clairement des règles (comme les méthodes d'arrondi à respecter), ce qui permet d'avoir un standard stable et d'avoir les mêmes comportements partout. Pour éviter ces problèmes d'imprécision, il faut recourir à d'autres encodages de nombres, généralement sous forme de bibliothèque (comme GMP ou CLN).

Voir tous les billets