Pourquoi la valeur en virgule flottante de 4*0.1 est-elle belle en Python 3 mais pas 3*0.1?

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

mais je ne vois pas pourquoi 4*0.1 est joliment imprimé comme 0.4 , mais 3*0.1 n'est pas, quand 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')
152
demandé sur Community 2016-09-21 17:07:21

4 réponses

la réponse simple est que 3*0.1 != 0.3 est dû à une erreur de quantification (arrondie) (alors que 4*0.1 == 0.4 parce que la multiplication par une puissance de deux est habituellement une opération" exacte").

vous pouvez utiliser la méthode .hex en Python pour visualiser la représentation interne d'un nombre (essentiellement, la valeur exacte binaire à virgule flottante, plutôt que l'approximation de 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 point flottant binaire est très légèrement plus grand que la valeur" exacte " de 0,1 (parce que le final 0x0.99 est arrondi à 0x0.un.) Lorsque vous multipliez ceci par 4, une puissance de deux, l'exposant augmente (de 2^-4 à 2^-2) mais le nombre reste inchangé, donc 4*0.1 == 0.4 .

cependant, lorsque vous multipliez par 3, la petite différence minuscule entre 0x0.99 et 0x0.a0 (0x0.07) grossit en 0x0.15 erreur, qui apparaît comme une erreur à un chiffre dans la dernière position. Cela fait 0,1 * 3 pour être très légèrement plus grand que la valeur arrondie de 0,3.

Python 3 float repr est conçu pour être rond-trippable , qui est, la valeur affichée doit être exactement convertible dans la valeur d'origine. Par conséquent, il ne peut pas afficher 0.3 et 0.1*3 exactement de la même façon, ou les deux numéros différents finiraient par le même après le coup de pied rond. Par conséquent, le moteur repr de Python 3 choisit d'en afficher un avec une légère erreur apparente.

296
répondu nneonneo 2016-09-21 19:24:28

repr (et str en Python 3) affichera 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 in hex), il est en fait un LSB plus élevé (0x1.3333333333334p-2) il faut donc plus de chiffres pour le distinguer de 0,3.

, d'autre part, la multiplication des 4*0.1 ne obtenir la valeur la plus proche à 0.4 (0x1.999999999999ap-2 in hex), donc il n'a 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 hex ci-dessus parce que c'est agréable et compact 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, voici:

>>> 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')
75
répondu Mark Ransom 2016-09-21 15:39:27

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

si vous vérifiez un flotteur sur la ligne de commande de Python ou si vous l'Imprimez, il passe par la fonction repr qui crée sa représentation de chaîne.

à partir de la version 3.2, str et repr de Python utilisent un schéma d'arrondissement complexe, qui préfère joli décimales si possible, mais utilise plus de chiffres où nécessaire pour garantir bijectif (un à un) cartographie entre les flotteurs et leurs représentations de ficelles.

ce schéma garantit que la valeur de repr(float(s)) semble agréable pour simple décimales, même si elles ne peuvent pas être représenté précisément comme flotteurs (par exemple: quand s = "0.1") .

en même temps il garantit que float(repr(x)) == x tient pour chaque flotteur x

21
répondu Aivar 2016-09-22 07:30:20

N'est pas vraiment spécifique à L'implémentation de Python mais devrait s'appliquer à n'importe quelle fonction float à chaîne 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 n'importe quel nombre qui a un facteur de nombre premier qui n'est pas partagé avec la base résultera toujours en une représentation de point récurrente. Par exemple 1/7 a un facteur premier, 7, qui est non partagé avec 10, et donc a une représentation décimale récurrente, et la même chose est vraie 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 exactement représenté par un nombre fini de bits après le point de point.

puisque 0.1 n'a pas de représentation exacte, une fonction qui convertit l'approximation à une chaîne de points décimaux essaiera généralement d'approcher certaines valeurs de sorte qu'ils ne reçoivent pas de résultats unintuitifs comme 0.1000000000004121.

puisque le point flottant est en notation scientifique, toute multiplication par une puissance de la base n'affecte que la partie exposant du nombre. Par exemple, 1.231 e+2 * 100 = 1.231 e + 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.151910920"

Selon l'algorithme utilisé, il peut essayer de deviner commune de décimales basé sur les chiffres significatifs. Les deux 0,1 et 0,4 ont les mêmes chiffres significatifs en binaire, parce que leurs flotteurs sont essentiellement des troncations de (8/5) (2^-4) et (8/5) (2^-6) respectivement. Si l'algorithme identifie le 8/5 sigfig pattern comme la décimale 1.6, alors il fonctionnera sur 0.1, 0.2, 0.4, 0.8, etc. Il peut également avoir des motifs Magic sigfig pour d'autres combinaisons, comme le float 3 divisé par float 10 et d'autres magic tendances statistiquement susceptibles d'être formées par division d'ici 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 fait que l'algorithme ne parvient pas à reconnaître le nombre magique pour la constante de 0,3 en fonction de sa tolérance pour la perte de précision.

Edit: 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 numéros de 0,1 et 0.10000000000000001 et 0.1000000000000000055511151231257827021181583404541015625 sont tous approximée par 3602879701896397 / 2 ** 55. Puisque toutes ces valeurs décimales partagent la même approximation, n'importe laquelle pourrait être affichée tout en conservant l'invariant eval(repr(x)) == X.

il n'y a pas 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).

5
répondu AkariAkaori 2016-09-25 05:08:49