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:
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)
[ 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():
independente = original[1:4].copy()
independente[0] = -1
print(original[1], np.shares_memory(original, independente), independente.base is None)
99 False True
| Operação | Devolve | Observação |
|---|---|---|
Fatia a[1:4] | View | Sempre |
a.reshape(...) | View, se possível | Cópia só se a memória não permitir |
a.T, a.transpose() | View | |
a.ravel() | View, se possível | flatten() sempre copia |
a[mascara], a[[0, 2]] | Cópia | |
a + 1, np.sqrt(a) | Array novo | Operaçõ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:
a = np.array([1, 2, 3])
b = a
b += 10
print(a)
b = b + 10
print(a, b)
[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:
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)
[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:
x = np.arange(3)
print(np.asarray(x) is x, np.array(x) is x)
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.