Capítulo 7, Básico
Tipos de dados: dtype
Todo elemento de um array tem o mesmo tipo, e esse tipo decide a memória, a precisão e o que acontece quando um número não cabe. Aqui moram as surpresas mais caras do NumPy.
Código deste capítulo: básico/cap07_tipos_dtype.py
Os tipos mais comuns
O NumPy descobre o tipo a partir dos valores, mas você pode (e muitas vezes deve) escolher:
import numpy as np
print(np.array([1, 2, 3]).dtype)
print(np.array([1.0, 2, 3]).dtype)
print(np.array([True, False]).dtype)
print(np.array([1 + 2j]).dtype)
int64
float64
bool
complex128
| Tipo | Bytes | Faixa ou uso |
|---|---|---|
int8, int16, int32, int64 | 1, 2, 4, 8 | Inteiros com sinal. O int64 é o padrão |
uint8, uint16, uint32, uint64 | 1, 2, 4, 8 | Inteiros sem sinal (o uint8 vai de 0 a 255, como os pixels) |
float32, float64 | 4, 8 | Decimais. O float64 é o padrão |
bool | 1 | Verdadeiro ou falso |
complex128 | 16 | Números complexos |
Converter com astype
O astype cria um array novo com outro tipo. A conversão de decimal para inteiro trunca (descarta a parte decimal), em vez de arredondar:
notas = np.array([7.9, 8.5, 9.99])
print(notas.astype(int))
print(np.round(notas).astype(int))
[7 8 9]
[ 8 8 10]
O np.round usa o arredondamento "para o par mais próximo" quando o valor está exatamente no meio: o 8.5 virou 8. É o mesmo comportamento do round do Python.
Quando o número não cabe
Cada tipo tem uma faixa fixa. Dentro de uma conta, passar do limite dá a volta (wraparound), sem erro e sem aviso. Um uint8 que chega a 255 e recebe mais 10 volta ao começo:
pequeno = np.array([250, 5], dtype=np.uint8)
print(pequeno + 10)
[ 4 15]
O 250 + 10 deu 4, e não 260. É um erro silencioso, e por isso um dos mais perigosos: nenhum aviso, e o resultado parece plausível. Já na criação, o NumPy 2 protege você quando o valor é impossível:
try:
np.array([300], dtype=np.uint8)
except OverflowError as erro:
print("OverflowError:", erro)
OverflowError: Python integer 300 out of bounds for uint8
Precisão de decimais
Os decimais binários não representam a maioria das frações exatamente, e o float32 tem ainda menos precisão do que o float64. A comparação por igualdade é uma armadilha, e a comparação com tolerância é o remédio:
print(0.1 + 0.2 == 0.3, np.isclose(0.1 + 0.2, 0.3))
print(np.finfo(np.float64).eps)
print(np.float32(16_777_216) + np.float32(1))
False True
2.220446049250313e-16
1.6777216e+07
O eps é a menor diferença que o float64 distingue perto de 1. E o último resultado (1.6777216e+07, ou seja, 16.777.216) mostra o limite do float32: a partir de 2²⁴ ele não consegue mais representar todos os inteiros, e somar 1 a 16.777.216 devolve o mesmo número.
O que acontece quando tipos diferentes se encontram
Quando você combina tipos em uma conta, o NumPy decide o tipo do resultado por regras de promoção. O NumPy 2 mudou uma regra importante (a NEP 50): um número Python puro ("escalar fraco") não promove o tipo do array, enquanto um escalar NumPy promove:
a = np.array([1, 2, 3], dtype=np.int8)
print((a + 1).dtype)
print((a + np.int64(1)).dtype)
print((np.float32(3) + 3.0).dtype)
print(np.result_type(np.int8, np.float32), np.result_type(np.int64, np.float32))
int8
int64
float32
float32 float64
O a + 1 continua int8, porque o 1 é um inteiro Python e "se adapta" ao array. Mas o a + np.int64(1) virou int64. E o np.result_type responde, sem fazer conta, qual seria o tipo do resultado.
Dados misturados e tipos pequenos
Duas fontes de bug silencioso: usar tipos pequenos (
uint8,int16) em contas que passam da faixa, e misturar números com texto emnp.array, que converte tudo para texto (capítulo 4). Quando um resultado parece estranho, imprima odtypeprimeiro.
Exercício 1
Economizar memória com o menor tipo possível
Escreva economizar(idades) que converta um array de idades (inteiros positivos) para o menor tipo sem sinal que comporta o maior valor, usando np.min_scalar_type.