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')
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.
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.1
ne 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')
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
etrepr
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, lorsques = "0.1")
.En même temps, il garantit que
float(repr(x)) == x
est valable pour chaque floatx
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).