Uma tradução do artigo foi preparada especificamente para os alunos do curso Python Developer .

Quando você escreve em um idioma de baixo nível, como C, fica preocupado em escolher o tipo e os qualificadores de dados corretos para seus números inteiros, em cada etapa você analisa se será suficiente usá-lo simplesmente int
ou se você deseja adicionar long
ou até mesmo long double
. No entanto, ao escrever código em Python, você não precisa se preocupar com essas coisas "secundárias", porque o Python pode trabalhar com números de integer
qualquer tipo de tamanho.Em C, se você tentar calcular 2 20000 usando a função interna powl
, obterá a saída inf
.
int main(void) {
printf("%Lf\n", powl(2, 20000));
return 0;
}
$ ./a.out
inf
Mas no Python, tornar isso mais fácil do que nunca é fácil:>>> 2 ** 20000
39802768403379665923543072061912024537047727804924259387134 ...
...
... 6021 digits long ...
...
6309376
Deve ser óbvio que o Python está fazendo algo muito bonito, e hoje vamos descobrir o que ele faz para trabalhar com números inteiros de tamanho arbitrário!Apresentação e Definição
Integer
em Python, essa é uma estrutura C definida da seguinte maneira:struct _longobject {
PyObject_VAR_HEAD
digit ob_digit[1];
};
PyObject_VAR_HEAD
É uma macro, ela se expande para PyVarObject
, que possui a seguinte estrutura:typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
Outros tipos que possuem PyObject_VAR_HEAD
:- PyBytesObject
- PyTupleObject
- PyListObject
Isso significa que um número inteiro, como uma tupla ou uma lista, tem um comprimento variável, e este é o primeiro passo para entender como o Python pode suportar o trabalho com números gigantes. Uma vez expandida, a macro _longobject
pode ser considerada como:struct _longobject {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
digit ob_digit[1];
};
Existem PyObject
alguns meta-campos na estrutura que são usados para a contagem de referência (coleta de lixo), mas, para falar sobre isso, precisamos de um artigo separado. O campo no qual focaremos isso ob_digit
e daqui a pouco ob_size
.Descriptografia ob_digit
ob_digit
É uma matriz alocada estaticamente do tamanho da unidade do tipo digit (typedef uint32_t)
. Como essa é uma matriz, ob_digit
é principalmente um ponteiro para um número e, portanto, se necessário, pode ser aumentada usando a função malloc para qualquer comprimento. Dessa forma, o python pode representar e processar números muito longos.Normalmente, em linguagens de baixo nível, como C, a precisão de números inteiros é limitada a 64 bits, no entanto, o Python suporta números inteiros de precisão arbitrária . Começando com o Python 3, todos os números são apresentados no formulário bignum
e são limitados apenas pela memória do sistema disponível.Descriptografia ob_size
ob_size
armazena o número de itens em ob_digit
. O Python substitui e usa o valor ob_size
para determinar o número real de elementos contidos na matriz para aumentar a eficiência da alocação de memória na matriz ob_digit
.Armazenamento
A maneira mais ingênua de armazenar números inteiros é armazenar um dígito decimal em um elemento da matriz, para que operações como adição e subtração possam ser realizadas de acordo com as regras da matemática do ensino fundamental.Com essa abordagem, o número 5238 será salvo assim:
Essa abordagem é ineficiente, pois usaremos dígitos de até 32 bits (uint32_t) para armazenar um dígito decimal, que na verdade varia de 0 a 9 e pode ser facilmente representado com apenas 4 bits, pois, ao escrever algo tão versátil quanto python, o desenvolvedor do kernel deve ser ainda mais inventivo.Então, podemos fazer melhor? Claro, caso contrário, eu não teria postado este artigo. Vamos dar uma olhada em como o Python armazena um número inteiro extra longo.Caminho do Python
Em vez de armazenar apenas um dígito decimal em cada elemento da matriz ob_digit
, o Python converte números do sistema numérico com base de 10 em números em um sistema com base de 2 30 e chama cada elemento como um dígito cujo valor varia de 0 a 2 30 - 1.No sistema numérico hexadecimal, a base 16 ~ 2 4 significa que cada "dígito" do número hexadecimal varia de 0 a 15 no sistema numérico decimal. No Python, da mesma forma, um “número” com uma base de 2 30 , o que significa que o número varia de 0 a 2 30 - 1 = 1073741823 em decimal.Portanto, o Python efetivamente usa quase todo o espaço alocado de 32 bits por dígito, economiza recursos e ainda executa operações simples, como adição e subtração no nível matemático do ensino fundamental.Dependendo da plataforma, o Python usa matrizes inteiras não assinadas de 32 bits ou matrizes inteiras não assinadas de 16 bits com dígitos de 15 bits. Para executar as operações que serão discutidas posteriormente, você precisa apenas de alguns bits.Exemplo: 1152921504606846976Como mencionado, para Python, os números são representados em um sistema base 2 30 , ou seja, se você converter 1152921504606846976 em um número base com 2 30 bases , obtém 100.11529215046068464676 = 100 * 1152921504606846976 = 1 * (2 30) 2 + 0 * (2 30 ) 1 + 0 * (2 30 ) 0Como o ob_digit
dígito menos significativo é armazenado primeiro, ele é salvo como 001 como três dígitos.A estrutura _longobject
para esse valor conterá:ob_size
like 3ob_digit
como [0, 0, 1]
Eu criei uma demonstração REPL que irá mostrar como Python armazena um inteiro dentro de si, e também se refere a membros da estrutura, tais como ob_size
, ob_refcount
etc.Operações Inteiras Longas
Agora que temos uma idéia clara de como o Python implementa números inteiros de precisão arbitrária, é hora de entender como várias operações matemáticas são executadas com eles.Adição
Os números inteiros são armazenados "em números", o que significa que a adição é tão simples quanto na escola primária, e o código fonte do Python nos mostra que é assim que a adição é implementada. Uma função com um nome x_add
em um arquivo longobject.c
adiciona dois números....
for (i = 0; i < size_b; ++i) {
carry += a->ob_digit[i] + b->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
for (; i < size_a; ++i) {
carry += a->ob_digit[i];
z->ob_digit[i] = carry & PyLong_MASK;
carry >>= PyLong_SHIFT;
}
z->ob_digit[i] = carry;
...
O trecho de código acima é obtido de uma função x_add
. Como você pode ver, ele itera sobre um número por números e realiza a adição de números, calcula o resultado e adiciona hifenização.Torna-se mais interessante quando o resultado da adição é um número negativo. O sinal ob_size
é um número inteiro, ou seja, se você tiver um número negativo, ob_size
será um sinal de menos. O ob_size
módulo de valor determinará o número de dígitos em ob_digit
.Subtração
Assim como a adição ocorre, a subtração também ocorre. Uma função com um nome x_sub
no arquivo longobject.c
subtrai um número de outro....
for (i = 0; i < size_b; ++i) {
borrow = a->ob_digit[i] - b->ob_digit[i] - borrow;
z->ob_digit[i] = borrow & PyLong_MASK;
borrow >>= PyLong_SHIFT;
borrow &= 1; /* Keep only one sign bit */
}
for (; i < size_a; ++i) {
borrow = a->ob_digit[i] - borrow;
z->ob_digit[i] = borrow & PyLong_MASK;
borrow >>= PyLong_SHIFT;
borrow &= 1; /* Keep only one sign bit */
}
...
O trecho de código acima é obtido de uma função x_sub
. Nele, você vê como ocorre a enumeração de números e a subtração é realizada, o resultado é calculado e a transferência é distribuída. De fato, é muito semelhante à adição.Multiplicação
E, novamente, a multiplicação será implementada da mesma maneira ingênua que aprendemos nas lições da matemática no ensino fundamental, mas não é muito eficiente. Para manter a eficiência, o Python implementa o algoritmo Karatsuba , que multiplica dois números de n dígitos em etapas simples de O (n log 2 3 ).O algoritmo não é simples e sua implementação está além do escopo deste artigo, mas você pode encontrar a sua implementação em funções k_mul
e k_lopsided_mul
no arquivo longobject.c
.Divisão e outras operações
Todas as operações em números inteiros são definidas no arquivo longobject.c
, são muito simples de encontrar e rastrear o trabalho de cada uma. Atenção: Um entendimento detalhado do trabalho de cada um deles levará tempo, portanto faça um pré-estoque com pipoca .O Python pré - aloca um pequeno número de números inteiros na memória, variando de -5 a 256. Essa alocação ocorre durante a inicialização e, como não podemos alterar os números inteiros (imutabilidade), esses números pré-alocados são singleton e são referenciados diretamente em vez de serem alocados. Isso significa que toda vez que usamos / criamos um número pequeno, o Python, em vez de vendê-lo, simplesmente retorna uma referência ao número alocado anteriormente.Essa otimização pode ser rastreada na macro IS_SMALL_INT
e na função get_small_int
c longobject.c
. Portanto, o Python economiza muito espaço e tempo no cálculo de números inteiros comumente usados.Este é o segundo artigo da série Python Internals. O primeiro artigo foi sobre como mudei minha versão do Python para torná-loambíguo . Isso ajudará você a dar os primeiros passos para entender o código fonte do Python e continuar o caminho para se tornar um desenvolvedor de kernel do Python.Se você quiser ver mais artigos semelhantes, assine a minha newsletter e receba-os diretamente na sua caixa de entrada. Escrevo sobre engenharia, engenharia de sistemas e um pouco sobre programação toda sexta-feira. Envie- me um e- mail para @arpit_bhayani . Você pode encontrar meus artigos anteriores em @ arpitbhayani.me / blogs .Isso é tudo. Vejo você no curso !