AQUI!!!!

ints e floats

Dalton Serey

Nem 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.

Inteiros

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.

Conversão de inteiros em base 10 para base 2 (binária)

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.

Números negativos e complemento de dois

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:

  1. Fazemos |-3| - 1 = 2
  2. Convertemos 2 para a representação binária direta: 0010
  3. Invertemos todos os bits, obtendo: 1101.

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:

  1. Fazemos |-6| - 1 = 5.
  2. Convertemos 5 para a representação binária direta: 0101
  3. Invertemos todos os bits, obtendo: 1010.

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.

  1. Fazemos |-19| - 1 = 18.
  2. Convertemos 18 para a representação binária direta: 00010010
  3. Invertemos todos os bits, obtendo: 11101101.

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.

Literais octais, hexadecimais e binários

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.

Ponto Flutuante

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.

Baseada em Notação Científica

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 1 que vem antes da vírgula. Isso é possível porque esse bit é sempre igual a 1, 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:

A representação final de 22.375 é, portanto:

01000001101100110000000000000000

Outro exemplo de conversão para float

Vejamos 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.

Erros

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.