Calculadora com dinheiro

Trabalhando com precisão em números decimais no Python

(Last Updated On: 3 de janeiro de 2019)

Estou trabalhando em uma aplicação em Python para controle de gastos e ganhos da empresa onde trabalho. A princípio, é simples, só guardo os valores em variáveis para depois passá-los para um banco de dados próprio. No mês de julho, tivemos 5 vendas de R$ 99.91 e compramos 3 equipamentos de R$ 110.10:


ganhos_julho = 99.91 * 5
gastos_julho = 110.1 * 3

armazena_no_banco(ganhos_julho, gastos_julho)

A partir disso, os especialistas em finanças da empresa fazem análises para tentar melhorar nossos resultados.

Meu código é bem simples, mas tem uma lógica clara que deveria funcionar bem. Apesar disso, no final do mês a chefia acabou me dando uma bronca. Isso porque alguns cálculos não bateram com os resultados reais que tivemos. Fiquei confuso, e fui testar meu código para conferir se tudo está como deveria:


ganhos_julho = 99.91 * 5
print(ganhos_julho)

gastos_julho = 110.1 * 3
print(gastos_julho)

Olha os resultados que obtive:


499.54999999999995
330.29999999999995

Espera… o quê? Fazendo as contas manualmente, ou na calculadora, os resultados são outros: 499.55 e 330.3. Por que o Python me entregou esses números compridos e, principalmente, imprecisos?

O problema do float

A lógica das nossas contas está correta, o problema, como vimos, está nos próprios resultados que o Python nos dá. O Python não sabe fazer conta direito, é isso?

Bem, a resposta pode ser sim… e não! Em primeiro lugar, essa questão não é exclusiva do Python, mas sim da computação e de como ela lida com números de ponto flutuante (nosso querido float). Além disso, não é exatamente um problema; vamos entender!

No mais baixo nível, computadores funcionam com a diferença de dois estados elétricos – baixa e alta voltagem, ligado e desligado, verdadeiro e falso. Daí temos o binário, o famoso 0 e 1, e é por conta desse sistema que os computadores conseguem ser tão rápidos com algumas coisas.

Entretanto, utilizando o formato binário para os números de ponto flutuante, os computadores não conseguem representar com precisão exata algumas frações (como 0.98 e 0.1). Desse modo, esses números são automaticamente arredondados para o mais próximo que se encaixe na possibilidade do binário, o que resulta em um pequeno erro de precisão.

No geral, esse erro é muito pequeno para ser considerado relevante, mas há situações em que não podemos desconsiderá-lo, como agora!

No nosso caso, como estamos lidando com dinheiro, precisamos de uma precisão maior. Já vimos como arredondar e formatar valores monetários antes, mas mesmo essa forma pode resultar em alguns pequenos erros de arredondamento que queremos evitar. E agora?

Trabalhando com inteiros

Como estamos trabalhando com dinheiro, podemos rapidamente pensar numa alternativa que evitaria o problema dos números de ponto flutuante – trabalhar apenas com números inteiros. Mas como? Sabemos que 1 real equivale a 100 centavos, então podemos, simplesmente, trabalhar com essa subunidade e evitar números quebrados.

No nosso caso, tínhamos R$ 188.98 e R$ 13.10 que também podem ser representados, respectivamente, por 18898¢ e 1310¢ – dois números inteiros!

Por estarmos trabalhando com o Python, ainda evitamos (a princípio) o problema de overflow de número inteiro padrão, que é muito limitado em algumas linguagens (como em Java, que só chega a 2.147.483.647).

Como o banco de dados está guardando inteiros, agora, podemos simplesmente dividir por 100 no momento de imprimir os valores, o que resolveria os problemas. Olha:


ganhos_julho, gastos_julho = pega_valores_no_banco()

print(ganhos_julho / 100)
print(gastos_julho / 100)

E a resposta:


188.98
13.1

Legal! Com o código arrumado, fui compartilhar com as filiais internacionais de minha empresa. Entretanto, rapidamente recebi diversas reclamações mostrando erros nos cálculos. Mas por quê?

A primeira reclamação veio direto de nossa filial na Tunísia. O programa estava apresentando cálculos claramente errados para eles – o banco de dados armazenava 10000 milim e o valor impresso era de 100 dinares tunisianos, quando deveria ser 10. Isso é porque, na Tunísia (e em diversos outros países) a subunidade de moeda principal não vem do centésimo (1/100), mas do milésimo (1/1000) – 1 dinar tunisiano equivale a 1000 milim.

Desse jeito, temos um grande problema em potencial com a internacionalização do código. Na verdade, mesmo que trabalhemos com apenas um local, o perigo continua. No mundo financeiro, subunidades mudam com o tempo, devido à inflação e deflação.

Se armazenarmos números inteiros, teremos que migrar os valores armazenados toda vez que houver uma mudança, o que pode atrapalhar bastante a manutenção de todo o sistema.

O ideal ainda seria conseguir trabalhar com a unidade principal da moeda, com números quebrados, mas com exatidão. Será que tem como?

Trabalhando com precisão com o tipo Decimal

Por conta dessa necessidade recorrente de lidarmos com números não-inteiros exatos, a maioria das linguagens de programação nos disponibiliza tipos específicos para lidar com isso.

No caso do Java, por exemplo, temos o BigDecimal. No Python, temos todo o módulo decimal e, mais especificamente, o tipo Decimal. Importando-o, seu uso é direto:


from decimal import Decimal

ganhos_julho = Decimal('99.91') * 5
print(ganhos_julho)

gastos_julho = Decimal('110.1') * 3
print(gastos_julho)

Dessa vez, olha o resultado:


499.55
330.3

Exatamente como na calculadora! Os analistas da empresa não terão mais nenhum problema com os cálculos.

Usando a precisão exata quando precisamos

Começamos com um problema – o tipo float, do Python, não conseguia nos devolver um resultado exato de um cálculo. Logo entendemos que o problema não estava no Python, em si, mas no float e em como o computador lida com ele.

Como estávamos lidando com dinheiro, a precisão nos cálculos era fundamental. Assim, precisávamos de alguma solução. Demos uma olhada em como podemos transformar tudo em números inteiros (por exemplo, transformando o valor monetário de Real para centavos), o que, algumas vezes, pode ser uma boa saída.

Tratar todos os números como inteiros, entretanto, tem suas consequências negativas, como um possível overflow e problemas de manutenção de código. Queríamos uma solução melhor e… conseguimos!

Aprendemos que a maioria das linguagens de programação tem algum tipo numérico exato para evitar esse problema. No caso do Python, esse tipo é o Decimal, com precisão arbitrária. Com ele, nossos cálculos ganharam a precisão necessária e acabamos com todo o problema inicial.

Uma intuição natural depois de se conhecer os tipos numéricos exatos nas linguagens de programação, como o Decimal, é querer usá-los para tudo. Apesar disso, é importante sempre analisarmos se vale a pena – tipos exatos demandam mais tempo de processamento.

Normalmente, um pequeno erro na 10ª casa decimal de um número é irrelevante, e a precisão exata desnecessária. Por isso, temos sempre que analisar o contexto de nosso próprio programa antes de aplicar uma decisão.

Já conhecia o tipo Decimal antes? E toda essa confusão com o float? Escreva um comentário com sua opinião sobre o post e, se se interessar mais por Python, não deixe de dar uma olhada em nossos cursos na Alura sobre a linguagem!

FIQUE POR DENTRO

Desenvolvedor Python apaixonado por educação e segurança.
Explicit is better than implicit.
Twitter: @yanorestes
Email: yan.orestes@alura.com.br

Próximo ArtigoComeçando com o desenvolvimento Java