web-dev-qa-db-fra.com

Pourquoi la valeur à virgule flottante de 4 * 0.1 a-t-elle une belle apparence en Python 3 mais 3 * 0.1 ne l’est pas?

Je sais que la plupart des décimales n'ont pas une représentation exacte en virgule flottante ( Est-ce que les maths en virgule flottante sont cassés? ).

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 réellement 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')
152
Aivar

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

Vous pouvez utiliser le .hex méthode in Python pour afficher la représentation interne d’un nombre (en gros, la valeur exacte binaire en virgule flottante, plutôt que l’approximation en base 10). 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 dernier 0x0.99 est arrondi vers le haut) à 0x0.a). Lorsque vous multipliez ce nombre 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.

Toutefois, lorsque vous multipliez par 3, la toute petite différence entre 0x0.99 et 0x0.a0 (0x0.07) s'agrandit 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 très légèrement plus grand que la valeur arrondie de 0,3.

Le float de Python 3 repr est conçu pour être round-trippable, 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.3 et 0.1*3 exactement de la même manière, ou les deux nombres différents finiraient par être identiques après un aller-retour. Par conséquent, le moteur de repr de Python) de 3 choisit d'en afficher un avec une légère erreur apparente.

297
nneonneo

repr (et str in 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 en hexadécimal), mais en réalité, un LSB est supérieur (0x1.3333333333334p-2), de sorte qu'il faut plus de chiffres pour le distinguer de 0,3.

Par contre, la multiplication 4*0.1ne obtient la valeur la plus proche de 0,4 (0x1.999999999999ap-2 en hex), ainsi, 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 hexadécimale ci-dessus parce que c'est joli et compact et montre la différence de bit 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, la 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
Mark Ransom

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

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

À partir de la version 3.2, str et repr de Python utilisent un schéma d'arrondi complexe, qui privilégie si possible les décimales d'apparence agréable, mais qui utilise davantage de chiffres si nécessaire pour garantir une bijective (un à un). mappage entre les flottants et leurs représentations de chaîne.

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

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

22
Aivar

Pas vraiment spécifique à l'implémentation de Python mais devrait s'appliquer à n'importe quelle fonction de chaîne décimale ou décimale.

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

L'inverse de tout nombre ayant un facteur de nombre premier qui n'est pas partagé avec la base donnera toujours une représentation récurrente de points. 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. 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.

Comme 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’approcher certaines valeurs afin d’éviter des résultats non intuitifs tels que 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 pour la notation décimale et 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

En fonction de 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 valeurs significatives 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 de sigfig 8/5 comme étant le nombre décimal 1,6, il travaillera sur 0.1, 0.2, 0.4, 0.8, etc. Il peut également avoir des motifs de sigfig magiques pour d'autres combinaisons, telles que le flottant 3 divisé par le flottant 10 et d’autres schémas magiques susceptibles de se former statistiquement par 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 le flottant 10, ce qui obligera l'algorithme à ne pas reconnaître le nombre magique pour la constante de 0,3 en fonction de sa tolérance à 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 nombres 0.1 et 0.10000000000000001 et 0.10000000000000000000055511151231257827021181583404541015625 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 pourrait être affichée tout en conservant la valeur 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).

5
AkariAkaori