Capítulo 37, Avançado
Tipagem, API e o NumPy 2
Como declarar o que uma função aceita e devolve, como validar formas em tempo de execução, e o que mudou na versão 2 do NumPy, porque grande parte do material que você encontra ainda usa a versão 1.
Código deste capítulo: avancado/cap37_tipagem_numpy2.py
Anotar funções que usam arrays
O módulo numpy.typing oferece dois nomes que resolvem quase tudo. O ArrayLike é "qualquer coisa que o NumPy converta em array" (listas, tuplas, números, arrays), e é o tipo certo para entradas. O NDArray[...] é um array de um tipo específico, e é o tipo certo para saídas. O padrão é aceitar generosamente e devolver com precisão, convertendo na entrada com np.asarray:
import numpy as np
import numpy.typing as npt
def normalizar(x: npt.ArrayLike) -> npt.NDArray[np.float64]:
a = np.asarray(x, dtype=np.float64)
return (a - a.mean()) / a.std()
print(normalizar([1, 2, 3]).round(3))
print(normalizar(np.array([10, 20, 30])).round(3))
[-1.225 0. 1.225]
[-1.225 0. 1.225]
O np.asarray não copia o que já é um array do tipo certo (capítulo 10), e converte listas, de modo que a função aceita as duas formas sem custo extra. Um verificador de tipos como o mypy confere as anotações, mas não consegue verificar a forma (quantas dimensões, quantas linhas): o sistema de tipos do NumPy conhece o tipo dos elementos, e não as dimensões.
Validar a forma em tempo de execução
Como a forma não é checada estaticamente, uma função que depende dela deve validar na entrada, com uma mensagem clara. É melhor falhar no começo, com a explicação, do que mais adiante, com um erro de broadcasting incompreensível. Um None na forma esperada significa "qualquer tamanho nesse eixo":
def exigir_forma(a, esperada):
if a.ndim != len(esperada) or any(e is not None and e != s for e, s in zip(esperada, a.shape)):
raise ValueError(f"forma {a.shape} incompatível com {esperada}")
return a
try:
exigir_forma(np.zeros((3, 4)), (None, 5))
except ValueError as erro:
print(erro)
forma (3, 4) incompatível com (None, 5)
Eu também documento a forma esperada na docstring, no estilo X : (n_amostras, n_características), que é a convenção do scikit-learn.
O que mudou no NumPy 2
A versão 2.0 foi a primeira mudança incompatível em muitos anos. Muitos nomes antigos foram removidos para deixar a API mais enxuta, e outros comportamentos mudaram. Eu verifiquei, na versão 2.4 usada neste curso, quais nomes comuns não existem mais:
removidos = ["float_", "NaN", "Inf", "in1d", "trapz", "round_", "msort"]
print({nome: hasattr(np, nome) for nome in removidos})
print(hasattr(np, "trapezoid"), hasattr(np, "isin"))
{'float_': False, 'NaN': False, 'Inf': False, 'in1d': False, 'trapz': False, 'round_': False, 'msort': False}
True True
| Nome antigo | Use agora |
|---|---|
np.float_, np.complex_ | np.float64, np.complex128 |
np.NaN, np.Inf, np.PINF | np.nan, np.inf |
np.in1d | np.isin |
np.trapz | np.trapezoid |
np.round_ | np.round |
np.msort(a) | np.sort(a, axis=0) |
Outras mudanças que aparecem na prática:
- A representação dos escalares. O
reprde um escalar agora mostra o tipo, e oprintcontinua igual. Em um resultado como(np.int64(1), np.int64(0))os números são os mesmos de sempre. - Promoção de tipos (NEP 50). Um número Python puro não promove o tipo do array (capítulo 7).
- O inteiro padrão é
int64também no Windows. Na série 1.x, o Windows usavaint32, e o mesmo código dava resultados diferentes entre sistemas. copy=Falsemudou de significado. Antes: "evite copiar, se puder". Agora: "nunca copie, e falhe se for preciso". Para o comportamento antigo, usecopy=None(o padrão) ounp.asarray.
print(repr(np.float64(3.0)), str(np.float64(3.0)))
lista = [1, 2, 3]
try:
np.array(lista, copy=False)
except ValueError as erro:
print("copy=False falha quando é preciso copiar:", type(erro).__name__)
x = np.array([1.0, 2.0])
print(np.asarray(x, copy=False) is x, np.array(x, copy=None) is x)
np.float64(3.0) 3.0
copy=False falha quando é preciso copiar: ValueError
True True
A versão 2 também trouxe funções novas alinhadas ao padrão de arrays da comunidade, como o np.unstack (o inverso do stack), o np.vecdot e o np.linalg.matrix_transpose:
print(np.unstack(np.arange(6).reshape(2, 3)))
(array([0, 1, 2]), array([3, 4, 5]))
Achar o código antigo automaticamente
Você não precisa caçar os nomes antigos à mão. O ruff tem uma regra de migração, a NPY201, que marca cada uso de um nome removido e, em vários casos, sugere (ou aplica com --fix) a troca. Este arquivo mistura nomes da série 1:
import numpy as np
x = np.float_(3)
y = np.NaN
z = np.in1d([1], [1])
w = np.trapz([1, 2])
cd exemplos
ruff check --select NPY201 velho_numpy1.py --output-format concise
velho_numpy1.py:3:5: NPY201 [*] `np.float_` will be removed in NumPy 2.0. Use `numpy.float64` instead.
velho_numpy1.py:4:5: NPY201 [*] `np.NaN` will be removed in NumPy 2.0. Use `numpy.nan` instead.
velho_numpy1.py:5:5: NPY201 `np.in1d` will be removed in NumPy 2.0. Use `np.isin` instead. Unlike `np.in1d`, `np.isin` preserves the shape of its input, so `np.in1d(ar1, ar2)` is equivalent to `np.isin(ar1, ar2).ravel()`.
velho_numpy1.py:6:5: NPY201 `np.trapz` will be removed in NumPy 2.0. Use `numpy.trapezoid` on NumPy 2.0, or ignore this warning on earlier versions.
Found 4 errors.
[*] 2 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
Dependências que ainda não migraram
Se uma biblioteca que você usa foi compilada contra a série 1.x, ela pode quebrar com o NumPy 2. Ao atualizar um projeto, veja se as suas dependências declaram suporte ao NumPy 2, e fixe
numpy>=2nopyproject.tomlsó depois de conferir. Ouve opipavisam de conflitos de versão na hora de resolver.
Exercício 1
Validar uma matriz de entrada
Escreva validar_matriz(a) que converta a entrada para float64, recuse (com ValueError) o que não for 2D ou tiver nan ou infinitos, e devolva o array validado.