Pular para o conteúdo

    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:

    avancado/cap37_tipagem_numpy2.pylinhas 10 a 20
    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))
    
    Saída
    [-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":

    avancado/cap37_tipagem_numpy2.pylinhas 25 a 34
    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)
    
    Saída
    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:

    avancado/cap37_tipagem_numpy2.pylinhas 39 a 41
    removidos = ["float_", "NaN", "Inf", "in1d", "trapz", "round_", "msort"]
    print({nome: hasattr(np, nome) for nome in removidos})
    print(hasattr(np, "trapezoid"), hasattr(np, "isin"))
    
    Saída
    {'float_': False, 'NaN': False, 'Inf': False, 'in1d': False, 'trapz': False, 'round_': False, 'msort': False}
    True True
    
    Nome antigoUse agora
    np.float_, np.complex_np.float64, np.complex128
    np.NaN, np.Inf, np.PINFnp.nan, np.inf
    np.in1dnp.isin
    np.trapznp.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 repr de um escalar agora mostra o tipo, e o print continua 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 é int64 também no Windows. Na série 1.x, o Windows usava int32, e o mesmo código dava resultados diferentes entre sistemas.
    • copy=False mudou de significado. Antes: "evite copiar, se puder". Agora: "nunca copie, e falhe se for preciso". Para o comportamento antigo, use copy=None (o padrão) ou np.asarray.
    avancado/cap37_tipagem_numpy2.pylinhas 43 a 52
    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)
    
    Saída
    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:

    avancado/cap37_tipagem_numpy2.pylinha 54
    print(np.unstack(np.arange(6).reshape(2, 3)))
    
    Saída
    (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:

    exemplos/velho_numpy1.py
    import numpy as np
    
    x = np.float_(3)
    y = np.NaN
    z = np.in1d([1], [1])
    w = np.trapz([1, 2])
    
    Terminal
    cd exemplos
    ruff check --select NPY201 velho_numpy1.py --output-format concise
    
    Saída
    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>=2 no pyproject.toml só depois de conferir. O uv e o pip avisam 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.