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


158

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

Mais je ne vois pas pourquoi 4*0.1est bien imprimé comme 0.4, mais 3*0.1ne l'est pas, alors que les deux valeurs ont en fait des représentations décimales laides:

>>> 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
Parce que certains nombres peuvent être représentés exactement, d'autres non.
Morgan Thrapp

58
@MorganThrapp: non, ce n'est pas le cas. L'OP pose des questions sur le choix de formatage plutôt arbitraire. Ni 0,3 ni 0,4 ne peuvent être représentés exactement en virgule flottante binaire.
Bathsheba

42
@BartoszKP: Après avoir lu le document plusieurs fois, cela n'explique pas pourquoi Python s'affiche au 0.3000000000000000444089209850062616169452667236328125fur 0.30000000000000004et à 0.40000000000000002220446049250313080847263336181640625mesure .4qu'ils semblent avoir la même précision, et ne répond donc pas à la question.
Mooing Duck

6
Voir aussi stackoverflow.com/questions/28935257/… - Je suis un peu irrité qu'il ait été fermé en tant que duplicata, mais celui-ci ne l'a pas fait.
Random832

12
Rouvert, veuillez ne pas le fermer car un double de "est un calcul en virgule flottante cassé" .
Antti Haapala

Réponses:


301

La réponse simple est que, en 3*0.1 != 0.3raison d'une erreur de quantification (arrondi) (alors que4*0.1 == 0.4 que multiplier par une puissance de deux est généralement une opération "exacte").

Vous pouvez utiliser la .hexméthode en Python pour afficher la représentation interne d'un nombre (en gros, la valeur exacte en virgule flottante binaire, 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 est très légèrement plus grand que la valeur "exacte" de 0,1 (car le 0x0.99 final est arrondi à 0x0.a). Lorsque vous multipliez cela par 4, une puissance de deux, l'exposant se décale (de 2 ^ -4 à 2 ^ -2) mais le nombre reste inchangé par ailleurs, donc4*0.1 == 0.4 .

Cependant, lorsque vous multipliez par 3, la petite différence minuscule entre 0x0.99 et 0x0.a0 (0x0.07) s'agrandit en une erreur 0x0.15, qui apparaît comme une erreur à un chiffre à la dernière position. Cela fait que 0,1 * 3 est très légèrement supérieur à la valeur arrondie de 0,3.

Le flotteur de Python 3 represt conçu pour être allers-retours , c'est -à-dire que la valeur affichée doit être exactement convertible en valeur d'origine. Par conséquent, il ne peut pas afficher 0.3et 0.1*3exactement de la même manière, ou les deux nombres différents finiraient de la même manière après un aller-retour. Par conséquent, le reprmoteur de Python 3 choisit d'en afficher un avec une légère erreur apparente.


25
C'est une réponse incroyablement complète, merci. (En particulier, merci d'avoir montré .hex(); je ne savais pas que cela existait.)
NPE

21
@supercat: Python essaie de trouver la chaîne la plus courte qui arrondirait à la valeur souhaitée , quelle qu'elle soit. Évidemment, la valeur évaluée doit être inférieure à 0,5 ulp (ou elle arrondirait à autre chose), mais elle peut nécessiter plus de chiffres dans des cas ambigus. Le code est très gnarly, mais si vous voulez jeter un coup d'œil: hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
nneonneo

2
@supercat: toujours la chaîne la plus courte à 0,5 ulp. ( Strictement à l' intérieur si nous regardons un flottant avec un LSB impair; c'est-à-dire la chaîne la plus courte qui le fait fonctionner avec des liens ronds à pair). Toute exception à cette règle est un bogue et doit être signalée.
Mark Dickinson

7
@MarkRansom Ils ont sûrement utilisé autre chose que eparce que c'est déjà un chiffre hexadécimal. Peut-être ppour la puissance au lieu de l' exposant .
Bergi

11
@Bergi: L'utilisation de pdans ce contexte remonte (au moins) à C99, et apparaît également dans IEEE 754 et dans divers autres langages (y compris Java). Quand float.hexet float.fromhexont été implémentés (par moi :-), Python copiait simplement ce qui était alors une pratique établie. Je ne sais pas si l'intention était «p» pour «Power», mais cela semble être une bonne façon d'y penser.
Mark Dickinson

75

repr(et stren Python 3) affichera autant de chiffres que nécessaire pour rendre la valeur sans ambiguïté. Dans ce cas, le résultat de la multiplication3*0.1 n'est pas la valeur la plus proche de 0,3 (0x1.3333333333333p-2 en hexadécimal), c'est en fait un LSB plus élevé (0x1.3333333333334p-2), il a donc besoin de plus de chiffres pour le distinguer de 0,3.

D'autre part, la multiplication n'obtenir la valeur la plus proche de 0,4 (0x1.999999999999ap-2 dans l' hexagone), donc il n'a pas besoin de chiffres supplémentaires.4*0.1

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, allez-y:

>>> 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
(+1) Bonne réponse, merci. Pensez-vous qu'il pourrait être utile d'illustrer le point «pas la valeur la plus proche» en incluant le résultat de 3*0.1 == 0.3et 4*0.1 == 0.4?
NPE du

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

Je me demande s'il vaudrait la peine de noter les valeurs décimales précises des «doubles» les plus proches à 0,1, 0,3 et 0,4, car beaucoup de gens ne peuvent pas lire l'hexagone à virgule flottante.
supercat le

@supercat vous faites un bon point. Mettre ces super grands doubles dans le texte serait distrayant, mais j'ai pensé à un moyen de les ajouter.
Mark Ransom

25

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

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

À partir de la version 3.2, Python str etrepr utiliser un système d'arrondi complexe, qui préfère décimaux belle apparence si possible, mais il utilise plus de chiffres si nécessaire pour garantir biunivoque (un à un) correspondance entre les flotteurs et leurs représentations à cordes.

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

En même temps, il garantit qu'il float(repr(x)) == xtient pour chaque flotteurx


2
Votre réponse est exacte pour les versions Python> = 3.2, où stret reprsont identiques pour les flottants. Pour Python 2.7, repra les propriétés que vous identifiez, mais strc'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 repret strsont basés sur un nombre fixe de chiffres significatifs (17 pour repr, 12 pour str). (Et personne ne se soucie de Python 3.0 ou Python 3.1 :-)
Mark Dickinson

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

2
Notez que l'arrondi de shell vient reprdonc le comportement de Python 2.7 serait identique ...
Antti Haapala

5

Pas vraiment spécifique à l'implémentation de Python mais devrait s'appliquer à toutes les fonctions de chaîne flottante à décimale.

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 qui a un facteur de nombre premier qui n'est pas partagé avec la base se traduira toujours par une représentation de point de point récurrente. 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 chaîne de virgule décimale essaiera généralement d'approximer certaines valeurs afin qu'elles n'obtiennent pas de résultats non intuitifs tels que 0.1000000000004121.

Puisque la virgule flottante est en notation scientifique, toute multiplication par une puissance de la base n'affecte que la partie exposante du nombre. Par exemple 1.231e + 2 * 100 = 1.231e + 4 pour la 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 les mêmes chiffres significatifs en binaire, car leurs flotteurs sont essentiellement des troncatures de (8/5) (2 ^ -4) et (8/5) (2 ^ -6) respectivement. Si l'algorithme identifie le motif 8/5 sigfig comme le décimal 1,6, alors il fonctionnera sur 0,1, 0,2, 0,4, 0,8, etc. et d'autres modèles magiques statistiquement susceptibles d'être formés par division par 10.

Dans le cas de 3 * 0,1, les derniers chiffres significatifs seront probablement différents de la division d'un flotteur 3 par un flotteur 10, ce qui empêchera l'algorithme de reconnaître le nombre magique de la constante 0,3 en fonction de sa tolérance à la perte de précision.

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

Fait intéressant, il existe de nombreux nombres décimaux différents qui partagent la même fraction binaire approximative la plus proche. Par exemple, les nombres 0.1 et 0.10000000000000001 et 0.1000000000000000055511151231257827021181583404541015625 sont tous approximés par 3602879701896397/2 ** 55. Étant donné que 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'évaluation invariante (repr (x) ) == x.

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


4
Cela n'ajoute pas vraiment grand-chose aux réponses existantes.
Antti Haapala

1
"En fonction de 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 fait réellement Python .
Mark Dickinson
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.