ints e floatsNem todos os números são representados da mesma forma dentro da
máquina. Nesta lição veremos as diferenças da representação de números
inteiros, abordados pelo tipo int em Python, e de números
reais, abordados pelo tipo float.
Já dissemos que ao se deparar com um literal inteiro no texto do
programa, Python representará o número internamente como um
int. Por exemplo, o literal 19 deve ser
interpretado como o número inteiro 19 e representado internamente como
um int correspondente. A questão é como é essa
representação interna? A resposta é simples. A máquina representa todo
número inteiro (até certo limite) de forma binária. Isto é, o número
será representado por uma sequência finita de bits (0s e 1s).
Na prática, raramente é necessário converter representações decimais em binário. Contudo, saber fazê-lo é conhecimento fundamental para um engenheiro de software ou um cientista da computação. Portanto, vejamos como podemos fazê-lo.
A conversão de um inteiro decimal para binário se dá por divisões inteiras sucessivas. A ideia é simples: parte-se do número original, dividimos o valor por 2 e tomamos o resto da divisão como o bit menos significativo. Em seguida, repetimos o processo a partir do quociente, até que seja igual a 0.
Retomemos nosso exemplo. Na sequência que se segue, faremos as
divisões necessárias para obter a representação binária do número 19.
Partiremos da divisão de 19 por 2. O resultado de cada divisão é
representado na mesma linha pelos dois valores inteiros entre
parênteses: o primeir número é o quociente e o segundo é o resto. A cada
linha, repetimos o processo com o quociente da linha anterior. E fazemos
isso até que o quociente obtido seja 0. A representação binária do
número original será a sequência de restos em ordem inversa. Isto é, o
primeiro resto será o bit menos significativo e o último resto o bit
mais significativo. Em nosso exemplo, o valor 19 é, portanto,
representado por 10011 na base 2.
19 / 2 = (9, 1)
9 / 2 = (4, 1)
4 / 2 = (2, 0)
2 / 2 = (1, 0)
1 / 2 = (0, 1)
Como o número de bits reservado para cada inteiro é fixo, os demais
bits menos significativos são zerados. Se assumirmos que a
representação interna tem 16 bits (2 bytes), a representação final de
19 internamente será algo do tipo:
0000000000010011
Um segundo exemplo. Vejamos como converter o número 1641 para binário. Vejamos a sequência de divisões ao estilo do que fizemos acima.
1641 / 2 = (820, 1)
820 / 2 = (410, 0)
410 / 2 = (205, 0)
205 / 2 = (102, 1)
102 / 2 = (51, 0)
51 / 2 = (25, 1)
25 / 2 = (12, 1)
12 / 2 = (6, 0)
6 / 2 = (3, 0)
3 / 2 = (1, 1)
1 / 2 = (0, 1)
Logo, a representação de 1641 em binário é
11001101001.
Observação: a representação exata de cada valor na memória varia não apenas entre linguagens, mas até mesmo entre diferentes versões de interpretadores da mesma linguagem. De fato, em Python, todo valor com que a linguagem lida é um objeto e consiste em vários campos de dados. Um inteiro Python, portanto, consiste em mais dados do que a simples sequência de bits acima. A representação a que nos referimos acima é o campo que contém a informação central do inteiro. E por isso nos concentraremos nela.
Um dos problemas da representação direta apresentada acima é que ela só é conveniente para números positivos. Como na máquina não dispomos de “sinais” para adicionar aos inteiros, precisamos de uma outra abordagem para representar números negativos.
Há várias formas de representar números negativos. Mas uma das mais populares nos dias atuais é a conhecida representação por complemento de dois (de fato, é raro encontrar máquinas modernas que não usem complemento de dois).
A explicação dos motivos que levaram à escolha por essa representação vai além do que trataremos aqui. Mas certamente isso será tema de outras disciplinas do curso.
Na representação por complemento de dois, as combinações de palavras que podemos formar com os bits são divididas ao meio. Metade é usada para representar números positivos e a outra metade para representar números negativos. Assuma, por simplicidade, que tivéssemos apenas 4 bits pra representar inteiros. As combinações possíveis dos quatro bits são:
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
1010
1011
1100
1101
1110
1111
Se usássemos a representação direta isso permitiria representar os números de 0 a 15. Não muito conveniente se precisarmos lidar com números negativos. Assim, a ideia é associarmos metade desses números a valores positivos e a outra metade a negativos. Esta é a ordem em que a chamada representação por complemento de dois associa os valores: a primeira metade representa valores positivos (e o zero) e a segunda metade representa valores negativos.
0000 0
0001 1
0010 2
0011 3
0100 4
0101 5
0110 6
0111 7
1000 -8
1001 -7
1010 -6
1011 -5
1100 -4
1101 -3
1110 -2
1111 -1
Dessa forma, podemos representar, com 4 bits, os valores de -8 a +7. Bem mais conveniente que ter apenas valores positivos.
Para se converter um número negativo qualquer para a representação em complemento de dois, pode-se usar o seguinte procedimento. Passo 1) subtraímos 1 do valor absoluto do número a representar. Passo 2) convertemos o valor encontrado em binário, usando a representação direta. Passo 3) invertemos todos os bits.
Por exemplo, vejamos como representar -3, passo a passo:
|-3| - 1 = 200101101.E, de fato, 1101 é a representação de -3 em complemento
de dois com 4 bits, como se pode confirmar pela listagem acima.
Vejamos um segundo exemplo. Vamos converter agora o valor -6, também passo a passo:
|-6| - 1 = 5.01011010.Exemplo com 8 bits O mesmo procedimento pode ser usado para representações com qualquer número de bits. Vejamos, por exemplo, como o número -19 pode ser representado em 8 bits. Façamos passo a passo, da mesma forma que os exemplos acima.
|-19| - 1 = 18.0001001011101101.Logo, a representação em complemento de dois de -19 em 8 bits será
11101101.
Exercício 1 Determine as representações internas que
teriam os literais 6, 2, 4,
-55, -73, se assumirmos que inteiros são
armazenados 8 bits, com complemento de dois.
Python, assim como outras linguagens, permite usar literais numéricos
em base 8 (octais), base 16 (hexadecimal) e em base 2 (binários). Para
isso, os literais seguem regras específicas. Literais iniciados por
0o são tratados como octais. Literais iniciados por
0x são tratados como hexadecimais e literais iniciados por
0b são considerados binários.
Atenção: em Python 2, octais podiam ser iniciados com um simples 0. Assim,
025, em Python 2, tem o valor 21 (= 2 x 8 + 5). Como isso tendia a produzir bastante confusão, em Python 3 literais iniciados apenas por 0 são considerados um erro sintático (estritamente, é um erro léxico, já que o token não existe mais na linguagem).
Exercício Analise o repl abaixo. Verifique
qual o valor da variável soma e entenda o motivo disso.
Literais que contêm um ponto decimal entre os digitos, tal como
0.5 não são considerados e tratados como inteiros.
Estritamente, esse literal não se encaixa na regra léxica de inteiros
que exige uma sequência ininterrupta de digitos. Alguns poderiam
imaginar que se trata de dois inteiros 0 e 5.
Felizmente, Python reconhece o ponto decimal como um caractere válido no
meio de digitos, para o literal de números
de ponto flutuante.
Observe que Python e a maioria das linguagens de programação usa o ponto (
.) como o separador da parte fracionária dos números e não a vírgula (,), como estamos acostumados em português. Se você usar uma vírgula, verá que Python não entenderá o que você digitou como um literal, mas como par ordenado de números.
Números de ponto flutuante são uma forma de representação aproximada de números reais usada por computadores (racionais seria mais apropriado). Trata-se de uma forma bastante adequada para representar e manipular um certo tipo de número dentro da máquina. Infelizmente, a representação implica em convivermos com erros de aproximação, para os quais precisamos nos manter sempre atentos. Assim, entender em detalhes o que é o tipo de ponto flutuante é outro conhecimento fundamental para compreender e utilizar qualquer linguagem de programação.
A representação de números de ponto flutuante é baseada na conhecida notação científica. Com uma particularidade. Por ser projetada para funcionar em máquinas digitais, em que tudo é representado na forma de bits, números de ponto flutuante são uma notação científica em base 2 e não em base 10 como estamos acostumados.
A representação de 32 bits consiste em 3 partes, nesta ordem: i) um bit para representar o sinal; ii) 8 bits bits para representar o expoente (da base 2); e iii) 23 bits para o que se chama de mantissa. O número representado equivalerá a: ± 2expoente × mantissa. Há ainda uma representação de 64 bits, chamada de número de ponto flutuante de precisão dupla, em que o expoente tem 11 bits e a mantissa, 52 bits.
A conversão de um número decimal com parte fracionária em sua representação como número de ponto flutuante consiste em três passos: 1) converter o valor absoluto do número em sua versão binária convencional; 2) obtenção dos campos: sinal, expoente e mantissa a partir da representação binária obtida no primeiro passo. Vejamos como isso é feito, através de um exemplo.
Conversão de um decimal fracionário em binário. Vehjamos, então, como podemos converter um número decimal com parte fracionária em sua representação de ponto flutuante. Tomemos, como exemplo, o número 22.375.
Uma abordagem simples para a conversão é separar a parte inteira e a
parte fracionária, convertê-las independentemente nas representações
binárias e somá-las. Em termos de nosso exemplo, isso consiste em
separar 22.375 em 22 + 0.375, converter cada um deles separadamente e
depois somar as representações binárias. A conversão da parte inteira já
foi coberta antes e resulta em 10110. Vejamos como
converter a parte fracionária.
A conversão da parte fracionária é baseada em multiplicações
sucessivas por 2. A cada multiplicação toma-se a parte inteira do
resultado como um dos bits do resultado. O processo se repete com a
parte fracionária (depois de se dispensar a parte inteira) até que este
resultado seja 0.0 ou até que um número suficiente de bits tenha sido
obtido. Abaixo, segue a sequência de multiplicações para converter o
valor 0.375 que resulta nos bits 011 e,
portanto, na representação 0.011.
0.375 * 2 = 0.75 = 0 + 0.75
0.75 * 2 = 1.5 = 1 + 0.5
0.5 * 2 = 1.0 = 1 + 0.0
Observe os bits separados de cada produto em cada linha acima:
0, 1 e 1. Tomados na ordem
direta, os bits correspondem aos bits que se seguem à vírgula na
representação binária do número original. Assim, a representação binária
de 0.375 é, portanto, 0.011.
Com a conversão das partes inteira e fracionária, podemos retornar à
conversão do número original 22.375 que é a soma dos dois valores: 22 +
0.375 (em base 10) = 10110 + 0.011 (em base 2)
= 10110.011 (em base 2).
Normalização Depois de converter o valor absoluto em sua representação binária, é preciso normalizar a representação para obtermos o expoente e o número do qual retiraremos a mantissa.
A ideia é reescrever o valor binário no estilo notação científica, movendo a vírgula para deixar um único bit antes da vírgula. Obviamente, cada vez que movemos a vírgula uma posição à esquerda, precisamos compensar multiplicando o número por 2 (lembre que estamos na base 2). Da mesma forma, se movermos a vírgula para a direita, precisamos compensar dividindo o valor resultante por 2. É esse número de movimentações da vírgula, portanto, que irá determinar o expoente.
Vejamos como fica nosso exemplo. A sequência abaixo consiste em relocar a vírgula para chegar à posição normal. Veja que em cada linha movemos a vírgula uma posição à esquerda. E que a cada linha aumentamos o expoente em 1. Na última linha chegamos à representação final, a que chamamos de normal.
10110.011 =
1011.0011 * 2^1 =
101.10011 * 2^2 =
10.110011 * 2^3 =
1.0110011 * 2^4
O expoente A última linha da sequência acima determina o expoente. Neste exemplo, o expoente encontrado é 4. Relembre que esse 4 indica que a vírgula foi movida quatro posições à esquerda. Indica também que o número final precisa ser multiplicado 4 vezes por 2 para que retornemos ao valor original. O expoente, contudo, ainda precisa ser representado apropriadamente em excesso de 127.
Da mesma forma que os inteiros (int) , expoentes de
números de ponto flutuante podem ser positivos ou negativos. Contudo, ao
invés de usar uma representação por complemento de dois, o padrão IEEE
754 optou por usar uma representação por excesso de 127. Essa
representação consiste simplesmente em somar 127 ao número antes de
fazer a conversão para binário. Assim, por exemplo, o expoente 4 será
representado por 10000011 que corresponde a 131 que é 4 +
127.
A mantissa A mantissa será formada pelos
bits que se seguem à vírgula na representação normalizada do número. Os
demais bits à direita serão iguais a 0. Em nosso exemplo, a
representação normalizada é 1.0110011. Logo, a mantissa
será iniciada por 0110011 e será complementada por 16
0s, de forma que a mantissa tenha 23 bits, no total,
resultando em 01100110000000000000000.
Observe que a mantissa não inclui o bit
1que vem antes da vírgula. Isso é possível porque esse bit é sempre igual a1, exceto se o número representado for igual a 0.0.
Representação final A representação final do número é a concatenação de todos os bits:
0 para representar o sinal positivo10000011 para representar o expoente e01100110000000000000000 para representar a
mantissaA representação final de 22.375 é, portanto:
01000001101100110000000000000000
floatVejamos como converter um segundo valor: -110.59375.
Comecemos por converter o valor absoluto em sua versão binária. A
parte inteira, 110, corresponde a 1101110 em base 2. A
parte fracionária, 0.59375, corresponde a .10011. Logo,
110.59375 em binário é 1101110.10011.
O número normalizado corresponde a 1.10111010011 ×
26.
Logo, o expoente é 6 e será representado por 6 + 127 = 133 que em
binário é 10000101.
A mantissa será dada pela parte fracionária do número normalizado
(10111010011) complementado com 0s à direita:
10111010011000000000000.
Finalmente, temos a representação final de -110.59375 em
ponto flutuante: sinal 1, expoente 10000101 e
mantissa 10111010011000000000000:
11000010110111010011000000000000
Importante Agora você deve ser capaz de entender
melhor por que os literais 1 e 1.0 têm
significados profundamente diferentes em linguagens como Python.
Uma das coisas mais intrigantes ao lidarmos com computação é
observarmos que o resultado de 0.1 + 0.2 não é
igual a 0.3. Embora seja verdade no mundo
conceitual e matemático ao qual estamos acostumados, em linguagens de
programação isso, em geral, não é verdade.
O motivo para isso é justamente a representação na forma de números de ponto flutuante em binário. Enquanto esses números podem ser representados de forma absolutamente precisa em notação científica e, portanto, como números de ponto flutuante decimais, o mesmo não é verdade para sua representação em binário. O problema é que esses números resultam em representações periódicas em binário. Logo, precisaríamos de um número infinito de bits para representar esses números com precisão absoluta. O resultado é que apenas uma aproximação desses números é possível, na prática. Veja o repl e confira os resultados.