2.39. Trabalhar com números de vírgula flutuante¶
Os números de vírgula flutuante parecem decimais comuns, mas alguns dos seus comportamentos surpreendem quem os encontra pela primeira vez – e um desses comportamentos é mais acentuado no MicroPython do que no Python para desktop. Esta página explica o que esperar e como escrever código com números de vírgula flutuante que não se comporte incorretamente de forma silenciosa.
2.39.1. Precisão¶
A float do Python é um número de vírgula flutuante binária IEEE 754. Na maioria das compilações do MicroPython é de precisão simples (32 bits), enquanto o CPython para desktop usa precisão dupla (64 bits). A precisão simples tem cerca de sete dígitos decimais de exatidão; a dupla tem cerca de quinze.
>>> 0.1 + 0.2
0.3000000
>>> 1.0 / 3.0
0.3333333
>>> 1e30 * 1e30
inf
O intervalo representável é também mais estreito em ambas as extremidades: números com magnitude superior a cerca de 3.4e38 transbordam para inf, e números com magnitude inferior a cerca de 1.2e-38 arredondam para zero.
2.39.2. Comparar números de vírgula flutuante¶
A armadilha mais comum é testar a igualdade com ==:
>>> 0.1 + 0.2 == 0.3
False
Ambas as expressões parecem dever ser iguais, mas o resultado de 0.1 + 0.2 é o valor representável mais próximo, que não é exatamente 0.3. Use antes uma verificação de tolerância – pergunte se dois números de vírgula flutuante são suficientemente próximos em vez de idênticos:
if abs(a - b) < 1e-6:
# close enough
...
A escolha da tolerância depende da escala dos valores. Um 1e-6 fixo funciona bem quando os números estão na ordem de grandeza de 1; uma tolerância relativa é melhor quando os valores variam em ordens de grandeza.
math.isclose() trata de ambas de uma vez:
from math import isclose
isclose(0.1 + 0.2, 0.3) # True
isclose(1.0e6 + 1, 1.0e6) # True (within default tolerance)
Os dois argumentos de palavra-chave controlam que tipo de «proximidade» conta:
rel_tol– tolerância relativa, predefinição1e-9. Dois valores correspondem se a sua diferença estiver dentro desta fração do maior. Adequado para comparações gerais em qualquer escala.abs_tol– tolerância absoluta, predefinição0. Dois valores correspondem se a sua diferença estiver dentro deste valor fixo.
math.isclose() devolve True se qualquer uma das tolerâncias for satisfeita. Os valores predefinidos são adequados para a maioria dos pares de números não nulos; a armadilha ocorre quando um dos valores pode ser exatamente zero. A verificação de tolerância relativa resulta em «diferença ≤ rel_tol × maior valor», e o maior valor é zero, pelo que a verificação falha sempre:
>>> isclose(0.0, 1e-12)
False
A verificação de tolerância absoluta não tem esse problema – passe um abs_tol sempre que zero seja um valor contra o qual pode estar a comparar:
>>> isclose(0.0, 1e-12, abs_tol=1e-9)
True
2.39.3. Desvio por acumulação¶
Somas longas de números de vírgula flutuante perdem precisão mais depressa no MicroPython do que no CPython, porque cada resultado intermédio é arredondado de volta para precisão de 32 bits:
total = 0.0
for _ in range(1000000):
total += 0.1
print(total) # noticeably off from 100000.0
Para adições repetidas onde a exatidão é importante, dois padrões ajudam:
Acumular em inteiros sempre que os valores puderem ser escalados para inteiros – trabalhe em milissegundos em vez de segundos, ou milivolts em vez de volts, e depois converta uma única vez no final.
Calcular em lotes mais pequenos e somar os resultados dos lotes, de modo a que cada adição seja entre valores de magnitude semelhante.
O lado dos inteiros não tem tal limitação – os inteiros do MicroPython têm precisão arbitrária, tal como os do CPython. Quando tiver a escolha, prefira aritmética inteira para tudo aquilo em que a perda de precisão se poderia acumular.