Ao longo desta série, discutimos como converter, operar e manipular números inteiros em diferentes bases. Vimos que, para inteiros, a conversão é sempre exata: um número inteiro em uma base sempre tem um correspondente exato em outra.
No entanto, o mundo real não é feito apenas de inteiros. Medidas, probabilidades e constantes físicas dependem de números fracionários (reais). A questão é: como representamos \(0.1\) ou \(3.14159\) usando apenas zeros e uns?
Neste artigo, expandiremos nossos métodos para tratar a parte fracionária. Você verá que, embora a lógica seja similar, as frações trazem um novo desafio: a precisão. Diferente dos inteiros, nem toda fração finita no decimal possui uma representação finita em binário, o que dá origem aos famosos erros de arredondamento na computação.
A regra geral continua a mesma: usamos a base decimal como ponte.
- Decimal \(\to\) Base \(r\): Usamos Multiplicações Sucessivas.
- Base \(s\) \(\to\) Decimal: Usamos Expansão Posicional (com expoentes negativos).
De base decimal para qualquer base (Multiplicações Sucessivas) #
Para converter a parte fracionária do decimal para binário, octal ou hexadecimal, usamos o método das multiplicações sucessivas. Diferente da parte inteira (onde dividíamos), aqui multiplicamos pela base de destino para “empurrar” os dígitos para a parte inteira.
O Algoritmo:
- Multiplique a parte fracionária do número pela base de destino \(r\).
- A parte inteira do resultado torna-se o próximo dígito da nova base.
- Pegue apenas a parte fracionária restante e repita o processo.
- Pare quando o resultado for zero (exato) ou quando atingir a precisão desejada (no caso de dízimas).
Exemplo: Converter \(0.625_{10}\) para binário (\(2\)) com 3 casas decimais
- \(0.625 \times 2 = \mathbf{1}.25\) → guardamos 1, sobra \(0.25\)
- \(0.25 \times 2 = \mathbf{0}.50\) → guardamos 0, sobra \(0.50\)
- \(0.50 \times 2 = \mathbf{1}.00\) → guardamos 1, sobra \(0.00\)
Como a fração chegou a zero, a conversão é exata. Lemos de cima para baixo:
$$ 0.625_{10} = 0.101_2 $$O grande desafio aqui são as frações que parecem simples em decimal, mas são infinitas (periódicas) em binário. Veja o caso do \(0.3\):
Exemplo: Converter \(0.3_{10}\) para binário (\(2\)) com 5 casas decimais
- \(0.3 \times 2 = \mathbf{0}.6\) → guardamos 0
- \(0.6 \times 2 = \mathbf{1}.2\) → guardamos 1
- \(0.2 \times 2 = \mathbf{0}.4\) → guardamos 0
- \(0.4 \times 2 = \mathbf{0}.8\) → guardamos 0
- \(0.8 \times 2 = \mathbf{1}.6\) → guardamos 1
Note que sobrou \(0.6\), o que nos levaria de volta ao passo 2. O resultado é uma dízima periódica (\(0.010011001...\)). Para obter as 5 casas solicitadas no enunciado do exemplo, truncamos:
$$ 0.3_{10} \approx 0.01001_2 $$Esse “voltar ao passo 2” é o coração do fenômeno, e fica mais evidente desenhado. Na figura, os cinco passos aparecem em sequência, cada um com o bit que produz e o resto que sobra. A seta vermelha marca o momento em que o resto se repete e o processo entra em loop:

Repare que o ciclo tem quatro passos, e não cinco: o passo 1 acontece uma única
vez, e o que se repete é o bloco formado pelos passos 2 a 5. Por isso o
resultado se escreve como \(0.0\) seguido de 1001 repetido para sempre.
E quando temos parte Inteira e Fracionária juntas? #
A estratégia é o clássico “dividir para conquistar”. Você não precisa de um método novo; basta separar o número em dois problemas menores:
- Converta a parte inteira usando Divisões Sucessivas (como visto em artigo anterior).
- Converta a parte fracionária usando Multiplicações Sucessivas.
- Junte os dois resultados com o ponto (ou vírgula) separador.
Exemplo Misto: Converter \(6.512_{10}\) para binário (\(2\)) com 5 casas de precisão
Parte 1: O Inteiro (\(6\)) Usamos divisões:
$$ 6 \div 2 = 3 \quad (\text{resto } \mathbf{0}) \\ 3 \div 2 = 1 \quad (\text{resto } \mathbf{1}) \\ 1 \div 2 = 0 \quad (\text{resto } \mathbf{1}) $$Resultado Inteiro: \(110_2\)
Parte 2: A Fração (\(0.512\)) Usamos multiplicações:
- \(0.512 \times 2 = \mathbf{1}.024\) → guardamos 1
- \(0.024 \times 2 = \mathbf{0}.048\) → guardamos 0
- \(0.048 \times 2 = \mathbf{0}.096\) → guardamos 0
- \(0.096 \times 2 = \mathbf{0}.192\) → guardamos 0
- \(0.192 \times 2 = \mathbf{0}.384\) → guardamos 0
Resultado Fracionário: \(0.10000..._2\)
Parte 3: União
$$ 6.512_{10} \approx 110.10000_2 $$Repare no sinal de “aproximadamente”. Pare e faça o caminho de volta: \(110.10000_2\) vale exatamente \(4 + 2 + 0{,}5 = 6{,}5\). Ou seja, parar em 5 casas binárias perdeu \(0{,}012\) pelo caminho. Não houve erro de conta — foi o número de casas que acabou antes da fração. É a primeira vez neste artigo que você vê o erro de truncamento acontecendo, e é exatamente esse mecanismo que está por trás de todas as esquisitices de ponto flutuante do final do texto.
Visualizando no Python (O Raio-X da Memória) #
Vamos usar o Python para ver como números reais são armazenados na memória do computador. Lembre-se que, internamente, os números reais são representados em ponto flutuante, seguindo o padrão IEEE 754.
O padrão IEEE 754 será detalhado em artigos futuros. Mas o básico para você precisar saber agora é que um número real em ponto flutuante é dividido em três campos principais:
- Sinal (1 bit): Indica se o número é positivo (0) ou negativo (1).
- Expoente (8 bits para float de 32 bits): Armazena o expoente “ajustado” (com viés). O viés para floats de 32 bits é 127.
- Mantissa (23 bits para float de 32 bits): Armazena a parte fracionária do número, com um “bit escondido” implícito.
Aqui estamos usando 32 bits apenas como exemplo. Para outros tamanhos de ponto flutuante (como double de 64 bits), os tamanhos dos campos são diferentes, mas o conceito é o mesmo. Novamente, isso será aprofundado em artigos futuros. Agora, vamos focar em como visualizar esses bits no Python.
Talvez sua primeira reação seja tentar usar a função nativa bin(), que usamos
exaustivamente nos artigos sobre números inteiros. Mas se você tentar rodar
bin(0.625), o Python lhe devolverá um erro:
TypeError: 'float' object cannot be interpreted as an integer
Isso acontece porque a função bin() foi feita para mostrar a representação
exata de inteiros. Para ver como um número real (float) está realmente
gravado nos 32 bits da memória — enxergando os campos de Sinal, Expoente e
Mantissa — precisamos usar uma abordagem diferente.
Vamos usar a biblioteca struct para pegar o valor float, empacotá-lo em bytes
brutos e depois ler esses mesmos bytes como se fossem um inteiro. Isso nos dá um
“Raio-X” do hardware. A biblioteca struct é utilizada para converter entre
representações Python e C, permitindo manipular dados binários diretamente.
O código a seguir usa o formato 'f' do struct, que empacota o número em
32 bits (o float da linguagem C). Lembre-se que o Python armazena seus
floats com 64 bits ('d'), usando 11 bits de expoente e 52 de mantissa. A
estrutura de sinal, expoente e mantissa é idêntica; apenas os tamanhos dos
campos diferem.
Os formatos 'f' e 'I' sem prefixo usam a ordem de bytes nativa da
máquina. Como as duas pontas usam a mesma ordem, o padrão de bits que sai é o
mesmo em qualquer plataforma — o código abaixo é portátil como está.
Ainda assim, num artigo sobre representação binária vale conhecer a forma
explícita: struct.unpack('<I', struct.pack('<f', numero)) fixa
little-endian (e '!' fixaria a ordem de rede). A vantagem não é corrigir
nada, é documentar a intenção — e proteger de quem, um dia, mude só uma das
duas strings de formato. Misturar as ordens ('<I' com '>f') é o único jeito
de obter bits errados aqui.
import struct
def ver_bits_reais(numero):
# 'f' = float (32 bits padrão IEEE 754)
# 'I' = unsigned int (inteiro sem sinal de 32 bits)
# pack: converte o número float para bytes brutos
# unpack: lê esses bytes brutos como se fossem um inteiro
bits_inteiros = struct.unpack('I', struct.pack('f', numero))[0]
# Formata como uma string de 32 zeros e uns
bits_string = f"{bits_inteiros:032b}"
# Vamos imprimir de forma didática, separando os campos
sinal = bits_string[0]
expoente = bits_string[1:9]
mantissa = bits_string[9:]
print(f"\nAnalisando o número: {numero}")
print(f"Bits Brutos: {bits_string}")
print(f"Divisão: [S:{sinal}] [Exp:{expoente}] [Mant:{mantissa}]")
# Teste 1: Número apenas fracionário (Exemplo anterior)
ver_bits_reais(0.625)
# Teste 2: Número com parte inteira e fracionária
ver_bits_reais(12.5)
# Teste 3: Número periódico (0.1)
ver_bits_reais(0.1)
# Teste 4: Número negativo (-3.75)
ver_bits_reais(-3.75)Resultado da execução:
Analisando o número: 0.625
Bits Brutos: 00111111001000000000000000000000
Divisão: [S:0] [Exp:01111110] [Mant:01000000000000000000000]
Analisando o número: 12.5
Bits Brutos: 01000001010010000000000000000000
Divisão: [S:0] [Exp:10000010] [Mant:10010000000000000000000]
Analisando o número: 0.1
Bits Brutos: 00111101110011001100110011001101
Divisão: [S:0] [Exp:01111011] [Mant:10011001100110011001101]
Analisando o número: -3.75
Bits Brutos: 11000000011100000000000000000000
Divisão: [S:1] [Exp:10000000] [Mant:11100000000000000000000]O que estamos vendo?
Para entender as contas a seguir, considere que multiplicar por \(2^e\) é o mesmo que mover a vírgula \(e\) casas para a direita (se \(e\) for positivo) ou para a esquerda (se \(e\) for negativo).
- No caso do 0.625:
- O expoente é
01111110(126 decimal). Como o viés é 127, o expoente real é \(126 - 127 = -1\). - A mantissa guardada é
01000.... Lembre-se do “Bit Escondido”: o computador assume um “1.” na frente. Então a mantissa real é \(1.01\). - Conta final: \(1.01_2 \times 2^{-1} = 0.101_2 = 0.625\). Bateu!
- No caso do 12.5:
- O expoente é
10000010(130 decimal). Exponente real: \(130 - 127 = 3\). - A mantissa guardada é
1001000.... Colocando o “1.” implícito, temos \(1.1001\). - Conta final: \(1.1001_2 \times 2^3\). Mover a vírgula 3 casas para a direita nos dá \(1100.1_2\), que é exatamente \(12.5\) decimal.
- No caso do 0.1 (O Revelador):
- O expoente é
01111011(123 decimal). Expoente real: \(123 - 127 = -4\). - Olhe para a mantissa:
100110011001.... Você percebe o padrão repetitivo? Aqui está a prova visual de que 0.1 é uma dízima periódica em binário. - Note o último bit
1. O computador precisou arredondar o valor porque a sequência era infinita. É essa pequena “sujeira” no final que causa erros como \(0.1 + 0.2 \neq 0.3\). - Conta final: \(1.100110011001..._2 \times 2^{-4}\). Mover a vírgula 4 casas para a esquerda nos dá \(0.0001100110011..._2\), que é a representação binária aproximada de \(0.1\).
- No caso do -3.75:
- Sinal:
1(Negativo). - O expoente é
10000000(128 decimal). Expoente real: \(128 - 127 = 1\). Isso significa multiplicar por \(2^1\). - A mantissa guardada é
111000.... Adicionando o “1.” implícito, temos \(1.111\). - Conta final: \(-1 \times 1.111_2 \times 2^1\). Mover a vírgula 1 casa para a direita resulta em \(-11.11_2\).
- Traduzindo: \(11_2\) é 3 e \(0.11_2\) é \(0.5 + 0.25 = 0.75\). Resultado: \(-3.75\).
Caso tenha tido dificuldade, não se preocupe. A representação em ponto flutuante é um tema complexo e profundo. Por isso mesmo, dedicamos o próximo artigo desta série exclusivamente ao padrão IEEE 754, onde faremos um mergulho detalhado em cada campo — sinal, expoente e mantissa — e explicaremos os casos especiais como NaN, Infinito e números Subnormais.
De qualquer base para Decimal (Expansão Posicional) #
Para saber o valor decimal de uma fração em outra base, usamos a expansão posicional. A diferença é que, após a vírgula, as posições têm expoentes negativos: \(b^{-1}, b^{-2}, b^{-3}\), e assim por diante.
$$ 0.d_1 d_2 d_3 \dots _b = d_1 \cdot b^{-1} + d_2 \cdot b^{-2} + d_3 \cdot b^{-3} + \dots $$
Exemplo: Converter \(0.101_2\) para decimal (\(10\))
A conversão é exata, pois a soma é finita.
O Caso Geral: Usando a Ponte Decimal #
Se precisamos converter entre duas bases quaisquer entre si (ex: Base 6 para Octal), o caminho mais seguro é:
- Converter a fração de origem para Decimal (Expansão).
- Converter do Decimal para Destino (Multiplicação).
Exemplo: Converter \(0.32_4\) para binário (\(2\)) com 3 casas decimais
-
Converter Base 4 para Decimal:
$$ \begin{align*} 0.32_4 & = 3 \times 4^{-1} + 2 \times 4^{-2} \\ & = 3 \times 0.25 + 2 \times 0.0625 = 0.75 + 0.125 = 0.875_{10} \end{align*} $$ -
Converter Decimal (\(0.875\)) para Binário:
- \(0.875 \times 2 = \mathbf{1}.75\) → dígito 1
- \(0.75 \times 2 = \mathbf{1}.50\) → dígito 1
- \(0.50 \times 2 = \mathbf{1}.00\) → dígito 1
Veja que a fração chegou a zero, então a conversão é exata.
Resultado final:
$$ 0.32_4 = 0.111_2 $$
Exemplo: Converter \(0.52_6\) para octal (\(8\)) com 3 casas decimais
-
Converter Base 6 para Decimal:
$$ \begin{align*} 0.52_6 & = 5 \times 6^{-1} + 2 \times 6^{-2} \\ & = 5 \times 0.1666... + 2 \times 0.0277... \approx 0.8888_{10} \end{align*} $$(Nota: Para precisão máxima, mantenha as frações: \(5/6 + 2/36 = 32/36 = 8/9\))
-
Converter Decimal (\(8/9 \approx 0.888...\)) para Octal:
- \(0.8888... \times 8 = 7.1111...\) → dígito 7
- \(0.1111... \times 8 = 0.8888...\) → dígito 0
- \(0.8888... \times 8 = 7.1111...\) → dígito 7
Observe que o padrão se repete, indicando uma dízima periódica em octal.
Resultado aproximado:
$$ 0.52_6 \approx 0.707_8 $$Bases Relacionadas por Potência (Atalhos) #
Assim como nos inteiros, se as bases são potências uma da outra (como 2, 8 e 16), podemos converter agrupando ou expandindo bits.
Para números mistos (que têm parte inteira e fracionária), a regra de ouro é: comece sempre pela vírgula e vá para as extremidades.
Imagine a vírgula como um espelho. O agrupamento deve “se afastar” dela nas duas direções:
- Parte inteira (à esquerda da vírgula): agrupe da direita para a esquerda \(\leftarrow\). Se o último grupo ficar incompleto, complete com zeros à esquerda.
- Parte fracionária (à direita da vírgula): agrupe da esquerda para a direita \(\rightarrow\). Se o último grupo ficar incompleto, complete com zeros à direita.
Exemplo: Converter \(0.A7_{16}\) para binário (\(2\))
Expandimos cada dígito hexadecimal da parte fracionária em 4 bits:
$$ A_{16} = 1010_2, \quad 7_{16} = 0111_2 $$Resultado:
$$ 0.A7_{16} = 0.10100111_2 $$Agora, veja um exemplo onde o preenchimento de zeros é necessário.
Exemplo: Converter \(11011.11_2\) para Hexadecimal (\(16\))
O ponto de partida é a vírgula. Vamos agrupar 4 bits em cada direção.
$$ \leftarrow \mathbf{11011} \quad . \quad \mathbf{11} \rightarrow $$Lado Esquerdo (Inteiro):
- Grupo 1 (perto da vírgula): \(1011\) \(\to B\)
- Grupo 2 (ponta esquerda): Sobrou \(1\). Completamos com zeros à esquerda: \(0001\) \(\to 1\)
Lado Direito (Fração):
- Grupo 1 (perto da vírgula): Temos apenas \(11\). Como precisamos de 4 bits, completamos com zeros à direita: \(1100\) \(\to C\)
Resultado Final:
$$ 1B.C_{16} $$Mais um exemplo para reforçar o conceito:
Novo Exemplo: Converter \(0.11011_2\) para Hexadecimal (\(16\))
Precisamos agrupar os bits em blocos de 4, partindo da vírgula para a direita.
Número original:
$$ 0.11011_2 $$Grupo 1: Os primeiros 4 bits:
$$ 1101_2 = D_{16} $$Grupo 2: Sobrou apenas o bit 1. Como precisamos de um bloco de 4, completamos com zeros à direita:
(Nota: Se completássemos à esquerda, teríamos \(0001=1\), o que estaria errado, pois o bit original tem peso \(2^{-5}\), não \(2^{-8}\))
Resultado final:
$$ 0.11011_2 = 0.D8_{16} $$Ponto Flutuante em Python: Curiosidades e Limitações #
Além do que vimos anteriormente na seção “Visualizando no Python (O Raio-X da Memória)”, há algumas outras curiosidades interessantes sobre como o Python lida com números reais e suas bases.
Diferente dos números inteiros, onde o Python nos dá prefixos fáceis como 0b e
0x, lidar com bases não-decimais para números fracionários exige ferramentas
diferentes.
Não existe “Literal Binário Fracionário” #
Você não pode escrever algo como x = 0b1.101 em Python. A linguagem não
suporta essa sintaxe nativamente.
Se você precisar converter uma string binária fracionária (como “10.11”) para float, terá que fazer isso manualmente ou usar bibliotecas. A lógica interna do computador é binária, mas o Python abstrai isso para o programador.
Floats Hexadecimais #
Pouca gente sabe, mas o Python permite converter floats para hexadecimal e vice-versa de forma nativa. Isso é extremamente útil para programadores que precisam saber exatamente o que está salvo na memória, sem os arredondamentos da visualização decimal.
O formato usa a notação 0x...p..., onde p indica uma potência de 2
(semelhante à notação científica, mas binária).
# Convertendo float para sua representação exata em Hex
x = 10.5
print(x.hex())
# Saída: '0x1.5000000000000p+3'
# Significa: 1.5 (hexa) * 2^3
# Convertendo de volta (Hex para Float)
y = float.fromhex('0x0.3p10') # 0.1875 * 1024
print(y)
# Saída: 192.0Se você achou essa saída confusa, não se preocupe. Essa notação (float.hex)
parece uma “salada de bases” porque ela mistura Base 16 com potências de
Base 2.
Vamos detalhar mais a notação. Ela segue o formato: 0x[Mantissa Hexadecimal]p[Expoente Decimal]
A regra de ouro para decifrar isso é:
$$ \text{Valor} = (\text{Número Hexadecimal}) \times 2^{(\text{Expoente após o p})} $$Vamos dissecar os dois exemplos acima para entender a matemática por trás:
1. Analisando 0x1.5p+3 (que resultou em 10.5)
- A Mantissa (
1.5): Está em Hexadecimal. O1vale 1, mas o5depois do ponto não é “5 décimos”. Em base 16, cada casa decimal divide por 16.- Logo,
.5hex significa \(5/16\) ou \(0.3125\) em decimal. - Valor da Mantissa: \(1 + 0.3125 = \mathbf{1.3125}\).
- Logo,
- O Expoente (
p+3): Opindica uma potência de 2.- Valor do Expoente: \(2^3 = \mathbf{8}\).
- Conta Final: \(1.3125 \times 8 = \mathbf{10.5}\).
2. Analisando 0x0.3p10 (que resultou em 192.0)
- A Mantissa (
0.3): Novamente, em Hexadecimal.- O
.3hex significa \(3/16\) ou \(0.1875\) em decimal.
- O
- O Expoente (
p10):- Valor do Expoente: \(2^{10} = \mathbf{1024}\).
- Conta Final: \(0.1875 \times 1024 = \mathbf{192.0}\).
Por que usar isso? Essa notação é útil para programadores de sistemas porque ela define o número exatamente como ele é armazenado no hardware, sem sofrer os erros de arredondamento típicos da conversão Decimal → Binário. É a forma mais precisa de transportar números float entre diferentes máquinas.
A Prova Real do Erro de Precisão #
Lembra que discutimos como \(0.3_{10}\) vira uma dízima periódica em binário? No Python, isso gera o famoso “erro” de aritmética de ponto flutuante.
Como o computador trunca a dízima binária, a soma não bate exata:
val1 = 0.1
val2 = 0.2
soma = val1 + val2
print(f"Resultado: {soma}")
# Saída: 0.30000000000000004
print(soma == 0.3)
# Saída: FalseVale parar aqui e entender onde esse erro mora. A raiz de tudo é que a reta numérica do computador não é contínua: ela é feita de marcas espaçadas, e a maioria dos números reais cai entre duas marcas. A figura abaixo mostra os dois lados disso — em cima, como as marcas se distribuem (densas perto do zero, esparsas longe dele); embaixo, um zoom em torno de \(0.1\), onde dá para ver que o valor exato simplesmente não tem marca:

O 0.1 que você digita, portanto, nunca chega inteiro à memória: o que fica
guardado é a marca mais próxima. Some duas dessas aproximações e o resultado
escapa da marca que corresponderia a 0.3 — é literalmente isso que a saída
acima está mostrando.
Para aplicações críticas (como sistemas financeiros) onde esse erro é
inaceitável, o Python oferece o módulo decimal, que faz aritmética em base 10 e
é exato para frações decimais, ao custo de desempenho, evitando a conversão
para binário. Note o recorte: exato para frações decimais, não para toda
divisão — Decimal(1) / Decimal(3) continua sendo uma dízima, e continua sendo
truncada.
from decimal import Decimal
d1 = Decimal('0.1')
d2 = Decimal('0.2')
print(d1 + d2)
# Saída: 0.3Esse comportamento não é bug nem defeito de implementação do Python: é consequência direta da conversão de base que fizemos à mão no começo do artigo. A documentação oficial tem uma página dedicada só a isso, Floating Point Arithmetic: Issues and Limitations, e vale a leitura.
Em 25 de fevereiro de 1991, durante a Guerra do Golfo, uma bateria antimíssil Patriot em Dhahran, na Arábia Saudita, falhou em interceptar um míssil Scud. O ataque matou 28 militares norte-americanos.
A causa foi exatamente o que este artigo descreve. O relógio interno do sistema contava décimos de segundo, e o valor \(0{,}1\) era armazenado num registrador de 24 bits — truncado, porque \(0{,}1\) é dízima em binário. O erro de cada conversão era de cerca de \(9{,}5 \times 10^{-8}\) segundo, desprezível. Só que a bateria estava ligada havia cerca de 100 horas, e o erro se acumulou até \(0{,}34\) segundo. Para um Scud a 1.676 m/s, 0,34 segundo significa 687 metros de erro na posição prevista — o suficiente para o míssil não estar onde o sistema procurava.
O caso está documentado em relatório oficial do GAO (GAO/IMTEC-92-26) e é analisado em detalhe na página do professor Douglas Arnold, da Universidade de Minnesota.
Estratégias para Contornar o Erro #
Além de usar o módulo Decimal, existem duas abordagens algorítmicas muito
comuns para lidar com essa limitação, dependendo do seu domínio de problema:
A. Trabalhar com Inteiros (Abordagem Financeira)
Em sistemas bancários ou de e-commerce, muitas vezes evitamos floats
completamente. A ideia é trabalhar com a menor unidade monetária (centavos) como
inteiro.
Ao invés de guardar R$ 10.50, guardamos o inteiro 1050. Como a matemática de inteiros é exata, o erro de arredondamento desaparece. Só dividimos por 100 na hora de exibir o valor na tela.
# Abordagem com Floats (Perigosa para dinheiro)
preco_a = 0.10
preco_b = 0.20
print(preco_a + preco_b) # 0.30000000000000004
# Abordagem com Inteiros (Segura)
centavos_a = 10
centavos_b = 20
total = centavos_a + centavos_b
print(f"R$ {total / 100:.2f}") # R$ 0.30B. Comparação por Tolerância (Abordagem Científica)
Em contextos científicos ou de engenharia, raramente verificamos se dois números
reais são estritamente iguais (==). Ao invés disso, verificamos se eles
estão “perto o suficiente” um do outro, dentro de uma margem de erro aceitável
chamada de tolerância, usualmente denotada por \(\epsilon\) (letra grega
épsilon).
Em Python, você pode fazer isso manualmente ou usar a função math.isclose:
import math
val1 = 0.1 + 0.2
esperado = 0.3
# O jeito errado
if val1 == esperado:
print("Iguais") # Nunca entra aqui
# O jeito manual (definindo uma tolerância, ex: 0.00001)
tolerancia = 1e-5
if abs(val1 - esperado) < tolerancia:
print("Iguais (dentro da margem)")
# O jeito Pythonico (recomendado)
# Aqui passamos sua variável para o 'abs_tol' (Absolute Tolerance)
if math.isclose(val1, esperado, abs_tol=tolerancia):
print(f"Iguais (usando math.isclose com tolerância de {tolerancia})")A função math.isclose é útil porque permite definir o que significa
“igual” de duas formas diferentes. Entender a diferença entre elas é vital para
evitar bugs silenciosos:
1. Tolerância Relativa (rel_tol) - O Padrão
- O que é: É uma comparação baseada em porcentagem ou magnitude. O padrão é
1e-9(0.000000001), o que significa que os números precisam concordar até o 9º algarismo significativo — que é coisa diferente da 9ª casa decimal: para valores da ordem de milhões, o 9º algarismo significativo cai bem antes da vírgula. - Como funciona: a diferença é comparada com
rel_tolmultiplicado por \(\max(|a|, |b|)\) — ou seja, contra o maior dos dois em módulo, e não contra o segundo argumento. - Quando usar: Na maioria dos casos gerais.
- A Lógica: Um erro de 1 metro é inaceitável se você está medindo uma mesa (erro relativo alto), mas é irrelevante se você está medindo a distância da Terra à Lua (erro relativo baixo). O
rel_tolse adapta à escala dos números.
2. Tolerância Absoluta (abs_tol) - O Valor Fixo
- O que é: É uma margem de erro fixa, como uma régua. Se você definir
abs_tol=0.01, qualquer diferença menor que 0.01 é aceita, não importa se os números são gigantes ou minúsculos. O padrão é0.0. - Quando usar: Principalmente ao comparar com Zero.
- A Lógica: Se você tentar verificar
math.isclose(0.0, 0.0000001), a tolerância relativa falhará (pois qualquer coisa comparada a zero é infinitamente maior percentualmente). Nesse caso, você precisa definir umabs_tolpara dizer: “considero zero qualquer coisa menor que \(10^{-7}\)”.
import math
# Exemplo de Tolerância Relativa (Padrão)
# 100 e 101 são "próximos"? Com 5% de tolerância, sim.
print(math.isclose(100, 101, rel_tol=0.05)) # True
# Exemplo de Tolerância Absoluta (Zero)
# Comparar com zero exige abs_tol
a = 0.0
b = 1e-10 # 0.0000000001
print(math.isclose(a, b)) # False (O padrão falha com zero)
print(math.isclose(a, b, abs_tol=1e-9)) # True (Diferença é menor que a margem fixa)A biblioteca Numpy, amplamente usada em contextos científicos, também oferece
funções para comparação com tolerância, como numpy.isclose e numpy.allclose,
que são otimizadas para arrays grandes.
Tabela-Resumo #
| Ideia | Regra | Exemplo do artigo |
|---|---|---|
| Notação posicional fracionária | dígitos à direita da vírgula têm peso \(b^{-1}, b^{-2}, \dots\) | \(0.101_2 = 0{,}5 + 0{,}125 = 0{,}625\) |
| Decimal \(\to\) outra base | multiplicações sucessivas; leia as partes inteiras de cima para baixo | \(0{,}625 \to 0.101_2\) |
| Quando o processo não termina | a fração vira dízima na base de destino | \(0{,}3 \to 0.01001..._2\), com ciclo |
| Número misto | converta as duas partes por métodos diferentes e junte | \(6{,}512 \approx 110.10000_2\) |
| Preço de truncar | parar cedo custa precisão | \(110.10000_2\) vale \(6{,}5\): perderam-se \(0{,}012\) |
| Agrupamento por potência | comece na vírgula e vá para as pontas | \(0.\mathtt{A7}_{16} = 0.10100111_2\) |
| Padding | zeros à esquerda na parte inteira, à direita na fracionária | \(0.11011_2 = 0.\mathtt{D8}_{16}\) |
| Ver os bits de um float | struct.unpack('I', struct.pack('f', x)) |
0.625 \(\to\) 00111111001000000000000000000000 |
| Ver o valor exato guardado | Decimal(x), sem aspas |
Decimal(0.3) tem 54 dígitos |
| Float em hexadecimal | float.hex() e float.fromhex() |
(10.5).hex() \(\to\) '0x1.5000000000000p+3' |
| Dinheiro | use inteiros na menor unidade | 1050 centavos, não 10.50 |
| Comparação científica | math.isclose, com rel_tol (escala) ou abs_tol (zero) |
isclose(0.0, 1e-10, abs_tol=1e-9) é True |
Considerações Finais: O Problema da Precisão #
Ao longo deste artigo, você deve ter notado que conversões como \(0.3_{10} \to \text{Binário}\) resultam em dízimas infinitas. Como a memória do computador é finita, ele não pode armazenar “infinitos bits”. Ele precisa cortar (truncar) ou arredondar esse número em algum momento.
Isso significa que o computador quase nunca armazena exatamente o número
\(0.3\), mas sim o valor representável mais próximo dele. E não precisamos
adivinhar qual é: o módulo decimal mostra o número guardado, dígito por
dígito.
from decimal import Decimal
print(Decimal(0.3)) # sem aspas: converte o float que já está na memória
# Saída: 0.299999999999999988897769753748434595763683319091796875
print(Decimal(0.1))
# Saída: 0.1000000000000000055511151231257827021181583404541015625Repare que Decimal('0.3'), com aspas, é o número decimal exato que você
escreveu; Decimal(0.3), sem aspas, é a tradução do que o hardware realmente
guardou. São 54 dígitos para representar algo que você digitou com um único
dígito depois da vírgula.
É esse pequeno erro, inerente à conversão de bases fracionárias, que causa os
comportamentos “estranhos” em linguagens de programação, como
0.1 + 0.2 != 0.3.
No próximo artigo, entraremos na anatomia do padrão IEEE 754, o sistema universal que define como esses bits (sinal, expoente e mantissa) são organizados para minimizar esses erros e permitir cálculos científicos.
Se quiser se aprofundar, a referência clássica sobre o assunto é o artigo “What Every Computer Scientist Should Know About Floating-Point Arithmetic”, de David Goldberg (1991) — denso, mas é o texto que todo mundo cita quando o assunto é erro de ponto flutuante.
Até lá!