6.2. O ndarray

O ndarray é o tipo que armazena dados numéricos em numpy. É duas coisas numa só: um único bloco de dados compacto e um pequeno descritor à frente desse bloco que indica como lê-lo.

6.2.1. Dentro da caixa

O bloco de dados armazena todos os elementos do array consecutivamente, sem nada extra entre eles. Cada elemento ocupa o mesmo número de bytes – um para um array de valores uint8, dois para uint16, quatro para float. Um array uint8 de 256 elementos ocupa exatamente 256 bytes de dados; os mesmos 256 números numa list Python ocupam um kilobyte – um slot de 32 bits por elemento, independentemente de quantos bits o valor realmente necessita.

O descritor regista o que o bloco significa. Cinco valores são suficientes para descrever qualquer array retangular, independentemente do número de dimensões:

  • dtype – o tipo de elemento. Define quantos bytes cada elemento ocupa e o intervalo de valores que pode conter (abordado em Dtypes).

  • itemsize – a largura em bytes de um elemento, derivada do dtype.

  • ndim – o número de dimensões (1 para um vetor, 2 para uma matriz, 3 para um volume, até 4).

  • shape – o tamanho ao longo de cada dimensão como um tuplo.

  • strides – como percorrer o bloco de dados para iterar cada eixo. Abordado em Forma e strides.

É isso. Todos os caminhos rápidos em numpy – aritmética, reduções, broadcasting, fatiamento – funcionam diretamente a partir desses cinco valores mais o apontador para os dados, sem overhead Python por elemento.

6.2.2. O que o design proporciona

Três propriedades resultam de «bloco compacto + pequeno descritor» e definem como o resto da secção se comporta.

A aritmética por elemento é executada numa única chamada. a + b entre dois arrays de forma coincidente soma os dois buffers e escreve um terceiro, tudo dentro de uma única chamada de biblioteca. np.sin(a) faz o mesmo para o seno de cada elemento. Os operadores aritméticos, de comparação e bit a bit funcionam todos desta forma.

Ver os mesmos dados de forma diferente é gratuito. Pedir uma sub-região de um array, ou os mesmos dados organizados sob uma forma diferente, não move nenhum byte. A operação devolve um novo descritor apontando para o mesmo bloco de dados. O resultado chama-se view – uma segunda janela sobre o mesmo buffer subjacente. Escrever através de uma view escreve na fonte.

Formas mistas ainda funcionam. Um array mais curto contra um mais longo, uma linha contra uma matriz, uma coluna contra uma linha – o numpy alinha-os por broadcasting, um conjunto reduzido de regras que decide qual o eixo curto que se expande para coincidir com o longo. A expansão é virtual; nenhum dado é duplicado.

6.2.3. O que o design custa

Duas restrições resultam do mesmo design.

Todos os elementos têm o mesmo tipo. Uma lista pode conter um int ao lado de um str ao lado de uma lista de mais três valores int; um ndarray não pode. O dtype é fixado na criação. A página Dtypes abrange o conjunto reduzido de tipos que numpy suporta e as regras que decorrem de fixar um.

Aumentar o tamanho de um array não é gratuito. Uma lista mantém slots livres no final e suporta .append de forma eficiente. Um ndarray tem exatamente o tamanho de que necessita; acrescentar elementos implicaria alocar um novo buffer maior e copiar o conteúdo antigo para ele. Não existe método append(), propositadamente. O padrão correto na câmara é pré-alocar o destino no seu tamanho final e preenchê-lo; Desempenho aborda a técnica.

Com um buffer tipado e compacto para os dados, um pequeno descritor para os metadados, e três garantias comportamentais (aritmética por elemento rápida, views alternativas sem cópia dos mesmos dados, e formas que fazem broadcasting), o ndarray é o fundamento sobre o qual repousa o resto do capítulo. Como um array efetivamente passa a existir – a partir de um literal, de uma alocação pré-preenchida, de um buffer de periférico – é a próxima questão prática.