Pular para o conteúdo

    Capítulo 10, Básico

    Cópias e views

    Quase todo bug confuso com arrays é, no fundo, um caso de "alterei aqui e mudou ali". Entender quando o NumPy copia e quando compartilha elimina essa classe de erros.

    Código deste capítulo: básico/cap10_copias_views.py

    Uma view compartilha a memória

    Quando você fatia um array, o NumPy não copia os números: cria um novo objeto que aponta para a mesma memória, com outro começo e outro tamanho. É por isso que o fatiamento custa quase nada, mesmo em arrays enormes. A consequência é que escrever na fatia escreve no original:

    básico/cap10_copias_views.pylinhas 10 a 16
    import numpy as np
    
    original = np.arange(6)
    fatia = original[1:4]
    fatia[0] = 99
    print(original)
    print(np.shares_memory(original, fatia), fatia.base is original)
    
    Saída
    [ 0 99  2  3  4  5]
    True True
    

    O atributo base aponta para o array que é o dono da memória. Uma view tem base, e um array que possui os próprios dados tem base igual a None.

    Quando você quer um array independente

    Para uma cópia de verdade, peça explicitamente com .copy():

    básico/cap10_copias_views.pylinhas 21 a 23
    independente = original[1:4].copy()
    independente[0] = -1
    print(original[1], np.shares_memory(original, independente), independente.base is None)
    
    Saída
    99 False True
    
    OperaçãoDevolveObservação
    Fatia a[1:4]ViewSempre
    a.reshape(...)View, se possívelCópia só se a memória não permitir
    a.T, a.transpose()View
    a.ravel()View, se possívelflatten() sempre copia
    a[mascara], a[[0, 2]]Cópia
    a + 1, np.sqrt(a)Array novoOperações aritméticas criam resultado novo

    O mesmo objeto, dois nomes

    A atribuição b = a não copia nada: cria um segundo nome para o mesmo array (o mesmo modelo de etiquetas do curso de Python). E os operadores de atribuição composta (+=) alteram o array no lugar, enquanto b = b + 10 cria um novo:

    básico/cap10_copias_views.pylinhas 28 a 33
    a = np.array([1, 2, 3])
    b = a
    b += 10
    print(a)
    b = b + 10
    print(a, b)
    
    Saída
    [11 12 13]
    [11 12 13] [21 22 23]
    

    O b += 10 mudou também o a, porque os dois são o mesmo objeto. Já o b = b + 10 criou um array novo e o ligou ao nome b, e o a ficou como estava.

    Funções que alteram o que recebem

    Um array passado para uma função é o mesmo objeto, e não uma cópia. Se a função altera o array, quem a chamou vê a mudança. Isso é útil para economizar memória e perigoso quando acontece sem querer:

    básico/cap10_copias_views.pylinhas 38 a 45
    def zerar_negativos(a):
        a[a < 0] = 0
        return a
    
    
    dados = np.array([3, -1, 4, -5])
    resultado = zerar_negativos(dados)
    print(dados, resultado is dados)
    
    Saída
    [3 0 4 0] True
    

    Os dados originais foram modificados, e o resultado é o mesmo objeto. Uma versão que não altera a entrada usa uma operação que cria resultado novo, como np.maximum(a, 0).

    `asarray` e `array`

    O np.asarray(x) não copia se x já for um array, e o np.array(x) copia por padrão. Funções que aceitam "qualquer coisa parecida com um array" costumam começar com np.asarray(entrada), o que converte listas sem copiar o que já é array:

    básico/cap10_copias_views.pylinhas 50 a 51
    x = np.arange(3)
    print(np.asarray(x) is x, np.array(x) is x)
    
    Saída
    True False
    

    A regra que eu sigo

    Se vou modificar um array que veio de fora (de outra função, de um parâmetro, de uma fatia), faço .copy() antes. Se só vou ler, não copio. E quando uma função altera o argumento de propósito, o nome ou a documentação dela diz isso.

    Exercício 1

    Normalizar sem mutar a entrada

    Escreva normalizar(a), que devolva (a - mínimo) / (máximo - mínimo) sem alterar o array recebido.