Você está construindo um sistema que calcula impostos ou um app para finanças pessoais ou qualquer outro sistema que faça cálculos usando valores monetários, como calcular o valor total multiplicando pela quantidade de produtos na cesta de compras. Para essa tarefa, a escolha mais óbvia para um iniciante é representar o dinheiro como um float, pois um inteiro não serviria para representar os centavos de um certo valor em reais.
O problema
Vou usar Python nesses exemplos, mas a maioria das linguagens de programação produziria resultados semelhantes.
Vamos calcular o preço total de 3 salgados que custam R$1,10 cada:
preco_salgado = 1.1
quantidade = 3
total = preco_salgado * quantidade
# Esperava 3.3, mas retorna 3.3000000000000003 :(
Mas o que esta acontecendo aqui? Este resultado ficaria horrível em um carrinho de compras!
Mesmo uma simples adição leva a um resultado bizarro:
base_bonus = 0.2
bonus_adicional = 0.1
bonus_total = bonus_base + bonus_adicional
# Esperava 0.3, mas retorna 0.30000000000000004 :(
Você pode estar pensando que a solução é realmente simples, basta cortar após dois dígitos!
bonus_total = 0.1 + 0.2
# 0.30000000000000004
total_bonus_corrigido = round(bonus_total, 2)
# 0.3
Isso pode funcionar para uma conta realmente simples, mas se você continuar fazendo esses cálculos com floats e arredondando repetidamente, pequenos erros de arredondamento se acumularão e levarão a um grande erro. Isso não será aceitável para um aplicativo que precisa de precisão e o arredondamento pode levar a erros como este:
minimo_aprovacao = 5.0
resultado_partido = 4.97 # o partido estaria fora do parlamento
resultado_partido = round(resultado_partido, 1)
# resultado_partido agora é 5.0
if resultado_partido >= minimo_aprovacao:
print('Você pode participar do Parlamento')
else:
print('Você não pode mais se sentar no Parlamento')
# Retorna: Você pode participar do Parlamento
Nas eleições parlamentares alemãs, um partido com menos de 5,0% dos votos não pode participar do parlamento. O partido Verde parecia ter conseguido 5,0%, até que se descobriu (após o anúncio dos resultados) que, na verdade, eles tinham apenas 4,97%. A impressão do resultado tinha um limite de dois dígitos e a porcentagem foi arredondada para 5,0%.
Desastres já aconteceram devido à perda de precisão ao converter inteiros para floats:
Em 25 de fevereiro de 1991, durante a Guerra do Golfo, um sistema de defesa antimísseis americano Patriot acabou deixando passar um míssel iraquiano Scud. Ele atingiu uma base americana, matando 28 pessoas. O problema estava na perda de precisão na conversão de segundos em inteiro para float, que foi o suficiente para que o sistema falhasse em atingir um míssel viajando em alta velocidade. Mais detalhes aqui.
Você pode até falhar em um teste automatizado simples e passar horas tentando descobrir o motivo:
assert 0.1 + 0.2 == 0.3
# Gera AssertionError!
Por que isso acontece?
Os computadores usam zeros e uns para representar números. Um bit pode ser um zero ou um. Computadores modernos costumam usar até 64 bits para representar informações, o que significa que você pode ter até 15 ou 17 dígitos após a vírgula, o que é muito, mas ainda há um limite para os valores que é possível representar.
Como humanos, podemos entender facilmente o conceito de 0,1. É um décimo de alguma coisa ou 10% de alguma coisa. Mas os computadores que usam zeros e uns representarão 0,1 como:
0.0001100110011...
Em que a parte 0011
estará se repetindo infinitamente.
Mas um computador de 64 bits não consegue armazenar infinitos decimais, de modo que terá apenas 15 ou 17 dígitos após o ponto decimal. Nesse processo de arredondamento de um número infinito para os dígitos de precisão permitidos, ele perde a precisão. Portanto, 0,1 que foi ajustado em dados de 64 bits será uma aproximação, mas não será exatamente 0,1.
Se revelarmos os 20 dígitos que seguem o sinal decimal em um float:
decimal_um = 0.1
print(f'{decimal_one:.20f}') # Na verdade é 0.10000000000000000555
decimal_dois = 0.2
print(f"{decimal_two:.20f}") # Na verdade é 0.20000000000000001110
Podemos ver que, de fato, estamos obtendo apenas o valor aproximado de um número decimal, não o exato. E é por isso que você pode ver esses resultados inesperados usando floats.
Solução 1: Use o módulo Decimal
Python tem um módulo chamado Decimal, que se comporta como um humano faria um cálculo matemático:
from decimal import Decimal
preco_salgado = Decimal('1.1')
quantidade = 3
total = preco_salgado * quantidade # Decimal('3.3')
print(total)
# 3.3
Observe que é possível fazer operações matemáticas com esses objetos Decimais:
Decimal('0.1') + Decimal('0.2')
# decimal('0.3')
Ao criar objetos Decimais, o valor float deve ser passado como uma string, caso contrário, você sofreria novamente com problemas de precisão que queríamos evitar:
Decimal(0.1) # Decimal('0.1000000000000000055511151231257827021181583404541015625')
Decimal('0.1') # Decimal('0.1')
Ao criar objetos Decimais, é possível customizar o nível de precisão, regras de arredondamento, etc.
Solução 2: use números inteiros
É possível usar números inteiros para armazenar os valores e fazer os cálculos. Somente quando houver a necessidade de mostrar os resultados para um humano que ele será dividido por 100:
preco_salgado = 110
quantidade = 3
total = preco_salgado * quantidade # 330
print(total/100)
# 3.3
Dessa forma, nos manteremos na segurança dos números inteiros.
A desvantagem dessa solução é na hora de calcular um número com um grande número de dígitos. Float é representado como notação científica, por isso é muito rápido fazer cálculos com um enorme número de dígitos, como o tamanho do universo ou átomos que compõem o Sol, mas ao usar números inteiros podemos esperar que o desempenho nos cálculos fique muito mais lento ou até impossível devido ao número de dígitos não ser comportado por um inteiro.
Espero que você tenha gostado do artigo e boa sorte nos cálculos com decimais!