Aritmética de Ponto Flutuante: Problemas e Limitações

Prévio Próximo

Python Logo14. Aritmética de Ponto Flutuante: Problemas e Limitações

Números de ponto flutuante são representados em hardware de computador como frações de base 2 (binário). Por exemplo, a fração decimal
0.125
tem um valor de 1/10 + 2/100 + 5/1000, e da mesma forma a fração binária
0.001
tem valor de 0/2 + 0/4 + 1/8. Estas duas fracções têm valores idênticos, a única diferença real é que o primeiro está escrito na notação fracionária base 10 , e a segunda, em base 2.
Infelizmente, a maioria das frações decimais não podem ser representadas exatamente como frações binárias. A consequência é, em geral, os números decimais de ponto flutuante que você digita apenas são aproximados pelos números binários de ponto flutuante realmente armazenados na máquina.
O problema é mais fácil de entender em primeiro lugar na base 10. Considere a fração 1/3. Voce pode aproximá-la como uma fração de base 10:
0.3
ou, melhor,
0.33
ou, melhor
0.333

e assim por diante. Não importa quantos dígitos você está disposto a escrever para baixo, o resultado nunca será exatamente 1/3, mas será cada vez mais uma melhor aproximação de 1/3.

Da mesma forma, não importa quantas base de 2 dígitos que está disposto a usar, o valor decimal 0,1 não pode ser representado exatamente como uma fração base 2. Na base 2, 1/10 é a fração repetindo infinitamente

0.0001100110011001100110011001100110011001100110011...

Pare em qualquer número finito de bits, e você terá uma aproximação.

Em uma máquina típica executando Python, há 53 bits de precisão disponíveis para um float Python, de modo que o valor armazenado internamente quando você digita o número decimal de 0,1 é a fração binária

0.00011001100110011001100110011001100110011001100110011010

que fica perto, mas não exatamente igual, 1/10.

É fácil esquecer que o valor armazenado é uma aproximação à fração decimal original, por causa da maneira que floats são exibidos no prompt do interpretador. Python imprime apenas uma aproximação decimal para o verdadeiro valor decimal da aproximação binária armazenada pela máquina. Se Python tivesse que imprimir o verdadeiro valor decimal da aproximação binária armazenado para 0,1, teria que exibir

>>> 0.10.1000000000000000055511151231257827021181583404541015625
Isso é mais dígitos do que a maioria das pessoas acham útil, então Python em vez disso, mantém o número de dígitos gerenciáveis, exibindo um valor arredondado
>>> 0.10.1
É importante perceber que este é, em um sentido real, uma ilusão: o valor na máquina não é exatamente 1/10, you’are simplesmente arredondamento a exibição do valor da máquina verdade. Este fato torna-se evidente logo que você tentar fazer aritmética com esses valores
>>> 0.1 + 0.20.30000000000000004

Note-se que isto está na própria natureza do binário de ponto flutuante: este não é um bug do Python, e não é um bug em seu código também. Você vai ver o mesmo tipo de coisa em todas as línguas que suportam o seu hardware aritmética de ponto flutuante (embora algumas línguas podem não exibir a diferença por padrão, ou em todos os modos de saída).

Outras surpresas seguem. Por exemplo, se você tentar arredondar o valor 2.675 para duas casas decimais, você receber isso
>>> round(2.675, 2)2.67
A documentação para a função built-in round() diz que arredonda para o valor mais próximo, arredondando laços para longe de zero. Desde que a fração decimal 2.675 é exatamente a meio caminho entre 2,67 e 2,68, você pode esperar o resultado aqui para ser (a aproximação binário) 2.68. Não é, pois, quando a seqüência decimal 2.675 é convertido em um número binário de ponto flutuante, ele é novamente substituído por uma aproximação binária, cujo valor exato é
2.67499999999999982236431605997495353221893310546875

Uma vez que esta aproximação é um pouco mais perto do que 2,67 para 2,68, é arredondado para baixo.

Se você estiver em uma situação em que você se importa de que maneira seus meio-casos decimais sejam arredondados, você deve considerar o uso do módulo decimal. Aliás, o módulo decimal também fornece uma boa maneira de “ver” o valor exato que está armazenado em qualquer especial float Python

>>> from decimal import Decimal
>>> Decimal(2.675)
Decimal('2.67499999999999982236431605997495353221893310546875')
Outra consequência é que uma vez que 0,1 não seja exatamente 1/10, somando os valores de 0,1 não pode produzir exatamente 1,0, tambem:
>>> sum = 0.0
>>> for i in range(10):
...     sum += 0.1
...
>>> sum
0.9999999999999999

Aritmética binária de ponto flutuante tem muitas surpresas como esta. O problema com “0.1” é explicado em pormenor a seguir, na secção “Representação de Erro“. Veja Os Perigos de ponto flutuante para um relato mais completo de outras surpresas comuns.

Como que diz perto do fim, “não há respostas fáceis.” Ainda assim, não ser excessivamente cauteloso em ponto flutuante! Os erros nas operações flutuador Python são herdadas do hardware de ponto flutuante, e na maioria das máquinas são da ordem de não mais do que 1 parte em 2 ** 53 por operação. Isso é mais do que suficiente para a maioria das tarefas, mas você precisa ter em mente que não é aritmética decimal, e que toda a operação de flutuação pode sofrer um novo erro de arredondamento.
Enquanto existem casos patológicos, para a maioria do uso casual de aritmética de ponto flutuante, você verá o resultado que você espera no final, se você simplesmente voltar da exibição dos seus resultados finais para o número de dígitos decimais que você espera. Para um bom controle sobre como um float é exibido ver especificadores de formato do método str.format() em Sintaxe de Formato de String.

14.1. Representation Error

Esta seção explica o “0.1” exemplo em detalhe, e mostra como é possível realizar voce mesmo uma análise exata de casos como este. Familiaridade básica com representação binária de ponto flutuante é assumida.

Representação de erro refere-se ao fato de que algumas (a maioria, na verdade) frações decimais não podem ser representadas exatamente como frações binárias (base 2). Esta é a razão principal pela qual Python (ou Perl, C, C ++, Java, Fortran, e muitos outros) muitas vezes não vãoi exibir o número decimal exato que você espera:

>>> 0.1 + 0.20.30000000000000004
Que é isso ? 1/10 e 2/10 não são exatamente representáveis como uma fração binária. Quase todas as máquinas de hoje (julho de 2010) usam aritmética de ponto flutuante IEEE-754, e quase todas as plataformas mapeam  floats Python para  “precisão dupla” IEEE-754. 754 duplos contêm 53 bits de precisão, de modo que na entrada o computador se esforça para converter 0,1 para a fracção mais próxima que pode, de forma a J/**2N, na qual J é um número inteiro contendo exactamente 53 bits. Reescrevendo
1 / 10 ~= J / (2**N)
como
J ~= 2**N / 10
e lembrando que J tem exatamente 53 bits (é >= 2**52 mas <2**53), o melhor valor para N é de 56:
>>> 2**52
4503599627370496
>>> 2**53
9007199254740992
>>> 2**56/10
7205759403792793
Ou seja, 56 é o único valor para N que deixa J com exatamente 53 bits. O melhor valor possível para o J é, então, aquele quociente arredondado:
>>> q, r = divmod(2**56, 10)
>>> r
6
Uma vez que o restante é mais do que metade de 10, a melhor aproximação é obtido por arredondamento para cima:
>>> q+1
7205759403792794
Portanto, a melhor aproximação possível a 1/10 em 754 de precisão dupla é mais de 2 ** 56, ou
7205759403792794 / 72057594037927936

Note-se que desde que nós arredondamos para cima, isto é, na verdade, um pouco maior do que 1/10; se não tivéssemos arredondado para cima, o quociente teria sido um pouco menor do que 1/10. Mas em nenhum caso, pode ser exatamente 1/10!

Assim, o computador nunca “vê” 1/10: o que ele vê é a exata  ração dada acima, a melhor aproximação dupla que 754 pode obter:

>>> .1 * 2**56
7205759403792794.0
Se multiplicarmos essa fração de 10**30, podemos ver o valor (truncado) dos seus 30 dígitos decimais mais significativos:
>>> 7205759403792794 * 10**30 // 2**56
100000000000000005551115123125L
o que significa que o número exato armazenado no computador é aproximadamente igual ao valor decimal,100000000000000005551115123125. Em versões anteriores para Python 2.7 e Python 3.1, Python arredondava este valor para 17 dígitos significativos, dando ‘0,10000000000000001’. Nas versões atuais, Python apresenta um valor com base na fração decimal menor que arredonda corretamente para o verdadeiro valor binário, o que resulta simplesmente em ‘0,1‘.
Prévio Próximo