Por que o valor do ponto flutuante de 4 * 0,1 fica bonito no Python 3, mas 3 * 0.1 não?

158

Eu sei que a maioria dos decimais não tem uma representação exata de ponto flutuante (a matemática do ponto flutuante está quebrada? ).

Mas não vejo por que 4*0.1é bem impresso 0.4, mas 3*0.1não é, quando ambos os valores realmente têm representações decimais feias:

>>> 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')
Aivar
fonte
7
Porque alguns números podem ser representados exatamente, e outros não.
Morgan Thrapp
58
@MorganThrapp: não, não é. O OP está perguntando sobre a opção de formatação de aparência bastante arbitrária. Nem 0,3 nem 0,4 podem ser representados exatamente no ponto flutuante binário.
Bathsheba
42
@BartoszKP: Depois de ler o documento várias vezes, ele não explica por que Python está exibindo 0.3000000000000000444089209850062616169452667236328125como 0.30000000000000004e 0.40000000000000002220446049250313080847263336181640625como .4, embora eles parecem ter a mesma precisão, e, portanto, não responder à pergunta.
Mooing Duck
6
Veja também stackoverflow.com/questions/28935257/… - Estou um pouco irritado por ter sido fechado como duplicado, mas este não foi.
usar o seguinte comando
12
Reaberto, não feche isso, pois uma duplicata de "matemática de ponto flutuante está quebrada" .
Antti Haapala

Respostas:

301

A resposta simples é 3*0.1 != 0.3devido ao erro de quantização (arredondamento) (enquanto a 4*0.1 == 0.4multiplicação por uma potência de dois geralmente é uma operação "exata").

Você pode usar o .hexmétodo em Python para visualizar a representação interna de um número (basicamente, o valor exato do ponto flutuante binário, em vez da aproximação da base 10). Isso pode ajudar a explicar o que está acontecendo sob o capô.

>>> (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 é 0x1,999999999999a vezes 2 ^ -4. O "a" no final significa o dígito 10 - em outras palavras, 0,1 no ponto flutuante binário é muito ligeiramente maior que o valor "exato" de 0,1 (porque o 0x0,99 final é arredondado para 0x0.a). Quando você multiplica isso por 4, uma potência de dois, o expoente muda (de 2 ^ -4 para 2 ^ -2), mas o número permanece inalterado 4*0.1 == 0.4.

No entanto, quando você multiplica por 3, a pequena diferença entre 0x0,99 e 0x0.a0 (0x0,07) aumenta para um erro 0x0.15, que aparece como um erro de um dígito na última posição. Isso faz com que 0,1 * 3 seja muito ligeiramente maior que o valor arredondado de 0,3.

O float do Python 3 reprfoi projetado para ser trip de ida e volta , ou seja, o valor mostrado deve ser exatamente conversível no valor original. Portanto, ele não pode exibir 0.3e 0.1*3exatamente da mesma maneira, ou os dois diferentes números iria acabar o mesmo depois de round-trip. Conseqüentemente, o reprmecanismo do Python 3 escolhe exibir um com um pequeno erro aparente.

nneonneo
fonte
25
Esta é uma resposta incrivelmente abrangente, obrigado. (Em particular, Obrigado por mostrar .hex(), eu não sabia que existia.)
NPE
21
@ supercat: Python tenta encontrar a string mais curta que arredondaria para o valor desejado , o que quer que seja. Obviamente, o valor avaliado deve estar dentro de 0,5ulp (ou seria arredondado para outra coisa), mas pode exigir mais dígitos em casos ambíguos. O código é muito gnarly, mas se você quiser dar uma olhada: hg.python.org/cpython/file/03f2c8fc24ea/Python/dtoa.c#l2345
nneonneo
2
@ supercat: sempre a corda mais curta que está dentro de 0,5 ulp. ( Estritamente dentro, se estivermos olhando para um flutuador com LSB ímpar; ou seja, a corda mais curta que o faz funcionar com laços redondos para pares). Qualquer exceção a isso é um erro e deve ser relatada.
Mark Dickinson
7
@ MarkRansom Certamente eles usaram outra coisa senão eporque isso já é um dígito hexadecimal. Talvez ppor poder em vez de expoente .
Bergi 22/09/16
11
@ Bergi: O uso de pneste contexto remonta (pelo menos) a C99 e também aparece no IEEE 754 e em várias outras linguagens (incluindo Java). Quando float.hexe float.fromhexforam implementados (por mim :-), o Python estava apenas copiando o que era então uma prática estabelecida. Não sei se a intenção era 'p' para "Power", mas parece uma boa maneira de pensar sobre isso.
Mark Dickinson
75

repr(e strno Python 3) exibirá quantos dígitos forem necessários para tornar o valor inequívoco. Nesse caso, o resultado da multiplicação 3*0.1não é o valor mais próximo de 0,3 (0x1.3333333333333p-2 em hexadecimal); na verdade, é um LSB maior (0x1.333333333333334p-2), portanto, é necessário mais dígitos para diferenciá-lo de 0,3.

Por outro lado, a multiplicação 4*0.1 faz obter o valor mais próximo a 0,4 (0x1.999999999999ap-2 em hexadecimal), por isso não precisa de dígitos adicionais.

Você pode verificar isso facilmente:

>>> 3*0.1 == 0.3
False
>>> 4*0.1 == 0.4
True

Usei a notação hexadecimal acima porque é agradável e compacta e mostra a diferença de bits entre os dois valores. Você pode fazer isso sozinho usando, por exemplo (3*0.1).hex(). Se você preferir vê-los em toda a sua glória decimal, aqui está:

>>> 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')
Mark Ransom
fonte
2
(+1) Boa resposta, obrigado. Você acha que vale a pena ilustrar o ponto "não é o valor mais próximo" incluindo o resultado de 3*0.1 == 0.3e 4*0.1 == 0.4?
NPE
@ NPE Eu deveria ter feito isso logo de cara, obrigado pela sugestão.
Mark Ransom
Eu me pergunto se valeria a pena observar os valores decimais precisos dos "dobros" mais próximos para 0,1, 0,3 e 0,4, já que muitas pessoas não conseguem ler hexadecimais de ponto flutuante.
Supercat 21/09
@ supercat você faz um bom argumento. Colocar essas duplas super grandes no texto seria uma distração, mas pensei em uma maneira de adicioná-las.
Mark Ransom
25

Aqui está uma conclusão simplificada de outras respostas.

Se você verificar um float na linha de comando do Python ou imprimi-lo, ele passará pela função reprque cria sua representação de string.

A partir da versão 3.2, o Python stre reprusa um esquema de arredondamento complexo, que prefere decimais de boa aparência, se possível, mas usa mais dígitos quando necessário para garantir o mapeamento bijetivo (um a um) entre flutuadores e suas representações de string.

Esse esquema garante que o valor de seja repr(float(s))bonito para decimais simples, mesmo que eles não possam ser representados com precisão como flutuadores (por exemplo, quando s = "0.1").

Ao mesmo tempo, garante que float(repr(x)) == xé válido para todos os carros alegóricosx

Aivar
fonte
2
Sua resposta é precisa para as versões do Python> = 3.2, onde stre reprsão idênticas para os carros alegóricos. Para o Python 2.7, reprpossui as propriedades que você identifica, mas stré muito mais simples - simplesmente calcula 12 dígitos significativos e produz uma string de saída com base neles. Para Python <= 2.6, ambos repre strsão baseados em um número fixo de dígitos significativos (17 para repr, 12 para str). (E ninguém se preocupa com Python 3.0 ou Python 3.1 :-)
Mark Dickinson
Obrigado @MarkDickinson! Eu incluí o seu comentário na resposta.
Aivar
2
Note-se que o arredondamento de shell vem repr, assim, o comportamento Python 2.7 seria idêntico ...
Antti Haapala
5

Não é realmente específico para a implementação do Python, mas deve se aplicar a qualquer função float para string decimal.

Um número de ponto flutuante é essencialmente um número binário, mas em notação científica com um limite fixo de números significativos.

O inverso de qualquer número que possua um fator de número primo que não seja compartilhado com a base sempre resultará em uma representação de ponto pontual recorrente. Por exemplo, 1/7 tem um fator primo, 7, que não é compartilhado com 10 e, portanto, tem uma representação decimal recorrente, e o mesmo vale para 1/10 com os fatores primos 2 e 5, o último não sendo compartilhado com 2 ; isso significa que 0,1 não pode ser representado exatamente por um número finito de bits após o ponto.

Como 0.1 não tem representação exata, uma função que converte a aproximação em uma sequência de pontos decimais geralmente tentará aproximar certos valores para que eles não obtenham resultados não intuitivos como 0.1000000000004121.

Como o ponto flutuante está em notação científica, qualquer multiplicação por uma potência da base afeta apenas a parte expoente do número. Por exemplo, 1.231e + 2 * 100 = 1.231e + 4 para notação decimal e, da mesma forma, 1.00101010e11 * 100 = 1.00101010e101 em notação binária. Se eu multiplicar por uma não potência da base, os dígitos significativos também serão afetados. Por exemplo 1.2e1 * 3 = 3.6e1

Dependendo do algoritmo usado, ele pode tentar adivinhar decimais comuns com base apenas nos números significativos. Tanto 0,1 como 0,4 têm os mesmos números significativos em binário, porque seus flutuadores são essencialmente truncamentos de (8/5) (2 ^ -4) e (8/5) (2 ^ -6), respectivamente. Se o algoritmo identificar o padrão 8/5 sigfig como o decimal 1.6, ele funcionará em 0.1, 0.2, 0.4, 0.8, etc. Ele também poderá ter padrões mágicos sigfig para outras combinações, como o float 3 dividido pelo float 10 e outros padrões mágicos estatisticamente prováveis ​​de serem formados pela divisão por 10.

No caso de 3 * 0,1, os últimos números significativos provavelmente serão diferentes da divisão de um flutuador 3 por flutuador 10, fazendo com que o algoritmo não reconheça o número mágico da constante 0,3, dependendo de sua tolerância à perda de precisão.

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

Curiosamente, existem muitos números decimais diferentes que compartilham a mesma fração binária aproximada mais próxima. Por exemplo, os números 0.1 e 0.10000000000000001 e 0.1000000000000000055511151231257827021181583404541015625 são todos aproximados por 360287970189696397/2 ** 55. Como todos esses valores decimais compartilham a mesma aproximação, qualquer um deles pode ser exibido enquanto ainda preserva a avaliação invariante (repr (x) ) == x.

Não há tolerância para a perda de precisão, se float x (0,3) não for exatamente igual a float y (0,1 * 3), então repr (x) não será exatamente igual a repr (y).

AkariAkaori
fonte
4
Isso realmente não adiciona muito às respostas existentes.
Antti Haapala
1
"Dependendo do algoritmo usado, ele pode tentar adivinhar decimais comuns com base apenas nos números significativos". <- Isso parece pura especulação. Outras respostas descreveram o que o Python realmente faz.
Mark Dickinson