Question Pourquoi la valeur à virgule flottante de 4 * 0.1 semble-t-elle intéressante dans Python 3 mais pas dans 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 brisées).

Mais je ne vois pas pourquoi 4*0.1 est bien imprimé comme 0.4, mais 3*0.1 n'est pas, quand les deux valeurs ont en réalité des représentations décimales très 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')

152
2017-09-21 14:07


origine


Réponses:


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

Vous pouvez utiliser le .hex méthode en Python pour voir la représentation interne d'un nombre (fondamentalement, le exact valeur à virgule flottante binaire, plutôt que l'approximation 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 grande que la valeur "exacte" de 0.1 (car la version finale 0x0.99 est arrondie à 0x0.a). Lorsque vous multipliez cela par 4, une puissance de deux, l'exposant décale 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 petite différence entre 0x0.99 et 0x0.a0 (0x0.07) grossit en une erreur 0x0.15, qui apparaît sous la forme d'une erreur à un chiffre dans la dernière position. Cela provoque 0.1 * 3 à être très légèrement supérieure à la valeur arrondie de 0,3.

Le flotteur de Python 3 repr est conçu pour être ronde-trippablec'est-à-dire que la valeur affichée doit être exactement convertible en valeur d'origine. Par conséquent, il ne peut pas afficher 0.3 et 0.1*3 exactement la même manière, ou les deux différent les chiffres finiraient par être les mêmes après un tour de table. Par conséquent, Python 3 repr Le moteur choisit d'afficher un avec une légère erreur apparente.


296
2017-09-21 14:30



repr (et str dans Python 3) affichera autant de chiffres que nécessaire pour rendre la valeur sans ambiguïté. 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), il s'agit en fait d'un LSB plus haut (0x1.3333333333334p-2) et nécessite donc plus de chiffres pour le distinguer de 0.3.

Par contre, la multiplication 4*0.1  Est-ce que obtenir 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 compacte et montre la différence de bit entre les deux valeurs. Vous pouvez le faire vous-même en utilisant p. Ex. (3*0.1).hex(). Si vous préférez les voir dans toute leur gloire décimale, voilà:

>>> 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
2017-09-21 14:26



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

Si vous cochez 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, les Python str et repr utiliser un système d'arrondissement complexe, qui préfère   décimales jolies si possible, mais utilise plus de chiffres où   nécessaire pour garantir une cartographie bijective (one-to-one) entre les flottants   et leurs représentations de chaîne.

Ce schéma garantit cette valeur de repr(float(s)) semble bien pour simple   décimales, même si elles ne peuvent pas être   représenté exactement comme des flotteurs (par ex. quand s = "0.1").

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


21
2017-09-21 17:42



Pas vraiment spécifique à l'implémentation de Python mais devrait s'appliquer à n'importe quelle fonction de 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 tout nombre qui a un facteur de nombre premier qui n'est pas partagé avec la base entraînera toujours une représentation ponctuelle 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 est 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 exactement représenté par un nombre fini de bits après le point.

Puisque 0.1 n'a pas de représentation exacte, une fonction qui convertit l'approximation en chaîne décimale essaiera généralement d'approximer certaines valeurs afin qu'elles n'obtiennent pas de résultats non intuitifs comme 0.1000000000004121.

Puisque le 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, ainsi que 1.00101010e11 * 100 = 1.00101010e101 en notation binaire. Si je multiplie par un non-pouvoir 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 nombres décimaux courants en se basant uniquement sur les chiffres significatifs. 0.1 et 0.4 ont 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 décimal 1,6, alors il fonctionnera sur 0,1, 0,2, 0,4, 0,8, etc. Il peut également avoir des schémas de sigfig magiques pour d'autres combinaisons, telles que le flottant 3 divisé par un flottant 10 et d'autres schémas magiques statistiquement susceptibles d'être formés par division de 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, empêchant 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 la plus proche. Par exemple, les nombres 0.1 et 0.10000000000000001 et 0.1000000000000000055511151231257827021181583404541015625 sont tous approximés par 3602879701896397/2 **. ) == 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).


5
2017-09-22 08:25