163 votes

Pourquoi la valeur en virgule flottante de 4*0,1 est-elle belle dans Python 3 mais pas 3*0,1 ?

Je sais que la plupart des décimales n'ont pas une représentation exacte en virgule flottante ( Les mathématiques à virgule flottante sont-elles cassées ? ).

Mais je ne vois pas pourquoi 4*0.1 s'imprime joliment comme 0.4 mais 3*0.1 ne l'est pas, alors que les deux valeurs ont en fait de vilaines représentations décimales :

>>> 3*0.1
0.30000000000000004
>>> 4*0.1
0.4
>>> from decimal import Decimal
>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')

7 votes

Parce que certains nombres peuvent être représentés exactement, et d'autres non.

59 votes

@MorganThrapp : non, ce n'est pas le cas. Le PO pose une question sur le choix de formatage qui semble plutôt arbitraire. Ni 0,3 ni 0,4 ne peuvent être représentés exactement en virgule flottante binaire.

3 votes

Ce n'est pas du tout arbitraire, ça montre tous les chiffres significatifs.

310voto

nneonneo Points 56821

La réponse est simple : parce que 3*0.1 != 0.3 due à l'erreur de quantification (arrondi) (alors que 4*0.1 == 0.4 car la multiplication par une puissance de deux est généralement une opération "exacte"). Python essaie de trouver la chaîne la plus courte qui serait arrondie à la valeur désirée pour qu'il puisse afficher 4*0.1 comme 0.4 car ils sont égaux, mais il ne peut pas afficher 3*0.1 comme 0.3 car ils ne sont pas égaux.

Vous pouvez utiliser le .hex en Python pour afficher la représentation interne d'un nombre (en gros, la méthode exact valeur binaire à virgule flottante, plutôt que l'approximation en base 10). Cela peut aider à expliquer ce qui se passe sous le capot.

>>> (0.1).hex()
'0x1.999999999999ap-4'
>>> (0.3).hex()
'0x1.3333333333333p-2'
>>> (0.1*3).hex()
'0x1.3333333333334p-2'
>>> (0.4).hex()
'0x1.999999999999ap-2'
>>> (0.1*4).hex()
'0x1.999999999999ap-2'

0.1 est 0x1.999999999999a fois 2^-4. Le "a" à la fin signifie le chiffre 10 - en d'autres termes, 0,1 en virgule flottante binaire correspond à très légèrement plus grande que la valeur "exacte" de 0,1 (parce que la valeur finale de 0x0,99 est arrondie à 0x0,a). Lorsque vous multipliez cette valeur par 4, une puissance de deux, l'exposant se déplace vers le haut (de 2^-4 à 2^-2) mais le nombre reste inchangé, donc 4*0.1 == 0.4 .

Cependant, lorsque vous multipliez par 3, la toute petite différence entre 0x0.99 et 0x0.a0 (0x0.07) se transforme en une erreur de 0x0.15, qui se traduit par une erreur d'un chiffre en dernière position. Cela fait que 0,1*3 est très légèrement plus grande que la valeur arrondie de 0,3.

Le flotteur de Python 3 repr est conçu pour être rond-trippable c'est-à-dire que la valeur indiquée doit être exactement convertible en valeur d'origine ( float(repr(f)) == f pour tous les flotteurs f ). Par conséquent, il ne peut pas afficher 0.3 et 0.1*3 exactement de la même manière, ou les deux différents les numéros seraient les mêmes après un aller-retour. Par conséquent, la méthode de calcul de Python 3 repr Le moteur choisit d'en afficher un avec une légère erreur apparente.

25 votes

Cette réponse est incroyablement complète, merci. (En particulier, merci d'avoir montré .hex() ; je ne savais pas que ça existait.)

2 votes

@NPE alors vous pourriez être intéressé par float.fromhex() aussi, il fait l'inverse.

0 votes

Par curiosité, est-ce que Python essaie toujours d'utiliser la chaîne la plus courte qui se trouve à 0,50 ulp près de la valeur donnée, ou est-ce qu'il utilise la chaîne la plus courte qui se trouve par exemple à 0,47 ulp près de la valeur donnée ? Certaines bibliothèques à virgule flottante, si elles reçoivent une chaîne décimale qui se situe presque exactement à mi-chemin entre deux valeurs pouvant être représentées par "double", ne renvoient pas toujours la valeur la plus proche de la valeur exacte représentée par la chaîne, mais l'impression d'un chiffre décimal supplémentaire résoudrait ce problème.

77voto

Mark Ransom Points 132545

repr (et str en Python 3) émettra autant de chiffres que nécessaire pour rendre la valeur non ambiguë. Dans ce cas, le résultat de la multiplication 3*0.1 n'est pas la valeur la plus proche de 0,3 (0x1,3333333333333p-2 en hexadécimal), elle est en fait supérieure d'un LSB (0x1,3333333333334p-2) et nécessite donc plus de chiffres pour la distinguer de 0,3.

D'autre part, la multiplication 4*0.1 fait obtient la valeur la plus proche de 0,4 (0x1.999999999999ap-2 en hexadécimal), il n'a donc pas besoin de chiffres supplémentaires.

Vous pouvez le vérifier assez facilement :

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

J'ai utilisé la notation hexadécimale ci-dessus car elle est agréable et compacte et montre la différence de bits entre les deux valeurs. Vous pouvez le faire vous-même en utilisant par exemple (3*0.1).hex() . Si vous préférez les voir dans toute leur gloire décimale, c'est ici :

>>> Decimal(3*0.1)
Decimal('0.3000000000000000444089209850062616169452667236328125')
>>> Decimal(0.3)
Decimal('0.299999999999999988897769753748434595763683319091796875')
>>> Decimal(4*0.1)
Decimal('0.40000000000000002220446049250313080847263336181640625')
>>> Decimal(0.4)
Decimal('0.40000000000000002220446049250313080847263336181640625')

2 votes

(+1) Bonne réponse, merci. Pensez-vous qu'il serait utile d'illustrer le point "pas la valeur la plus proche" en incluant le résultat de 3*0.1 == 0.3 et 4*0.1 == 0.4 ?

0 votes

@NPE J'aurais dû le faire dès le départ, merci pour la suggestion.

0 votes

Je me demande si cela vaudrait la peine de noter les valeurs décimales précises des "doubles" les plus proches de 0,1, 0,3 et 0,4, puisque beaucoup de gens ne savent pas lire les hexagones à virgule flottante.

26voto

Aivar Points 1184

Voici une conclusion simplifiée à partir d'autres réponses.

Si vous vérifiez un flottant sur la ligne de commande de Python ou l'imprimez, il passe par la fonction repr qui crée sa représentation sous forme de chaîne.

À partir de la version 3.2, l'outil Python str et repr utiliser un schéma d'arrondi complexe, qui préfère les décimales si possible, mais utilise plus de chiffres lorsque cela est nécessaires pour garantir une correspondance bijective (un à un) entre les flottants et leurs représentations en chaîne.

Ce schéma garantit que la valeur de repr(float(s)) est agréable pour les simples décimales simples, même si elles ne peuvent pas être être représentées précisément comme des flottants (par ex. lorsque s = "0.1") .

En même temps, il garantit que float(repr(x)) == x est valable pour tout flotteur x

3 votes

Votre réponse est exacte pour les versions de Python >= 3.2, où str et repr sont identiques pour les flottants. Pour Python 2.7, repr a les propriétés que vous identifiez, mais str est beaucoup plus simple - il calcule simplement 12 chiffres significatifs et produit une chaîne de sortie basée sur ceux-ci. Pour Python <= 2.6, les deux fonctions repr et str sont basées sur un nombre fixe de chiffres significatifs (17 pour l'UE). repr 12 pour str ). (Et personne ne se soucie de Python 3.0 ou Python 3.1 :-)

0 votes

Merci @MarkDickinson ! J'ai inclus votre commentaire dans la réponse.

2 votes

Notez que l'arrondi de la coquille provient de repr Ainsi, le comportement de Python 2.7 serait identique...

5voto

AkariAkaori Points 319

Pas vraiment spécifique à l'implémentation de Python mais devrait s'appliquer à toutes les fonctions de chaîne de caractères de float à decimal.

Un nombre à virgule flottante est essentiellement un nombre binaire, mais en notation scientifique avec une limite fixe de chiffres significatifs.

L'inverse de tout nombre dont le facteur est un nombre premier qui n'est pas partagé avec la base donnera toujours une représentation en points récurrents. Par exemple, 1/7 a un facteur premier, 7, qui n'est pas partagé avec 10, et a donc une représentation décimale récurrente, et il en va de même pour 1/10 avec les facteurs premiers 2 et 5, ce dernier n'étant pas partagé avec 2 ; cela signifie que 0,1 ne peut pas être représenté exactement par un nombre fini de bits après le point.

Étant donné que 0,1 n'a pas de représentation exacte, une fonction qui convertit l'approximation en une chaîne de caractères décimaux essaiera généralement d'approximer certaines valeurs afin de ne pas obtenir des résultats peu intuitifs comme 0,1000000000004121.

Comme la virgule flottante est en notation scientifique, toute multiplication par une puissance de la base n'affecte que la partie exposant du nombre. Par exemple, 1.231e+2 * 100 = 1.231e+4 en notation décimale, et de même, 1.00101010e11 * 100 = 1.00101010e101 en notation binaire. Si je multiplie par une non puissance de la base, les chiffres significatifs seront également affectés. Par exemple 1,2e1 * 3 = 3,6e1

Selon l'algorithme utilisé, il peut essayer de deviner les décimales communes en se basant uniquement sur les chiffres significatifs. 0,1 et 0,4 ont tous deux les mêmes chiffres significatifs en binaire, car leurs flottants sont essentiellement des troncatures de (8/5). (2^-4) et (8/5) (2^-6) respectivement. Si l'algorithme identifie le motif sigfig 8/5 comme étant le décimal 1,6, alors il fonctionnera sur 0,1, 0,2, 0,4, 0,8, etc. Il peut également avoir des motifs sigfig magiques pour d'autres combinaisons, comme le flottant 3 divisé par le flottant 10 et d'autres motifs magiques statistiquement susceptibles d'être formés par la division par 10.

Dans le cas de 3*0,1, les derniers chiffres significatifs seront probablement différents de la division d'un flottant 3 par un flottant 10, ce qui fera que l'algorithme ne reconnaîtra pas le nombre magique pour la constante 0,3 en fonction de sa tolérance à la perte de précision.

Edit : https://docs.python.org/3.1/tutorial/floatingpoint.html

Il est intéressant de noter qu'il existe de nombreux nombres décimaux différents qui partagent la même fraction binaire la plus proche. Par exemple, les nombres 0,1, 0,10000000000000001 et 0,1000000000000000055511151231257827021181583404541015625 sont tous approximés par 3602879701896397 / 2 ** 55. Comme toutes ces valeurs décimales partagent la même approximation, n'importe laquelle d'entre elles peut être affichée tout en préservant l'invariant eval(repr(x)) == x.

Il n'y a aucune tolérance pour la perte de précision, si le float x (0.3) n'est pas exactement égal au float y (0.1*3), alors repr(x) n'est pas exactement égal à repr(y).

4 votes

Cela n'ajoute pas grand-chose aux réponses existantes.

1 votes

"Selon l'algorithme utilisé, il peut essayer de deviner les décimales communes en se basant uniquement sur les chiffres significatifs." <- Cela semble être de la pure spéculation. D'autres réponses ont décrit ce que Python en fait fait.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X