Saiba porquê não usar floats para representar Reais, Dólares, etc

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!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *