#2 - Copy vs View, Shape e Iterando Arrays no NumPy

A diferença entre copy e view no NumPy, como consultar e mudar o shape de um array com reshape, e as formas de iterar sobre arrays de qualquer dimensão.

Imagem de capa

No post anterior você viu como criar arrays e acessar elementos com indexação e slicing. Agora entra um detalhe que costuma pegar muita gente de surpresa: quando você faz uma “cópia” de um array no NumPy, ela é realmente uma cópia independente, ou só uma referência pro mesmo dado? A resposta muda o comportamento do seu código, então vale entender bem antes de seguir. Depois disso, você vai ver como consultar e mudar o formato (shape) de um array, e as diferentes formas de percorrer seus elementos.

Copy vs View

Existem duas formas de “duplicar” um array no NumPy, e elas se comportam de um jeito bem diferente.

O método copy()

.copy() cria um array totalmente novo, com seus próprios dados na memória. Mudar o original depois não afeta a cópia, e vice-versa.

import numpy as np

arr = np.array([1, 2, 3, 4, 5])
copia = arr.copy()
arr[0] = 42

print('original:', arr)
print('cópia:', copia)

A saída é:

original: [42  2  3  4  5]
cópia: [1 2 3 4 5]

Repare que mudar arr depois de tirar a cópia não afetou copia em nada — ela já é independente.

O método view()

.view() cria um novo objeto array, mas ele não tem dados próprios: é só uma janela pros mesmos dados do array original. Mudar um afeta o outro, nos dois sentidos.

arr = np.array([1, 2, 3, 4, 5])
visao = arr.view()
arr[0] = 42

print('original:', arr)
print('view:', visao)

visao[1] = 99
print('original depois de mudar a view:', arr)

A saída é:

original: [42  2  3  4  5]
view: [42  2  3  4  5]
original depois de mudar a view: [42 99  3  4  5]

Descobrindo quem é dono dos dados com base

Toda instância de array tem um atributo base. Se o array for dono dos próprios dados, base retorna None. Se for um view, base retorna o objeto original.

arr = np.array([1, 2, 3, 4, 5])
copia = arr.copy()
visao = arr.view()

print(copia.base)
print(visao.base)

A saída é:

None
[1 2 3 4 5]

É um jeito rápido de checar, em código, se um array que você recebeu de algum lugar é independente ou está “grudado” em outro.

Nota: slicing também retorna uma view, não uma cópia! Se você fizer pedaco = arr[1:3] e mudar pedaco, o arr original muda junto. Se precisar de um pedaço independente, use .copy() explicitamente.

Shape de um array

O atributo .shape retorna uma tupla com o número de elementos em cada dimensão do array.

Consultando o shape

arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
print(arr.shape)

A saída é:

(2, 4)

Isso significa: 2 linhas, 4 colunas. O primeiro valor da tupla é a dimensão mais “externa”, e por aí vai.

Shape em arrays de mais dimensões

arr = np.array([1, 2, 3, 4], ndmin=5)
print(arr)
print('shape:', arr.shape)

A saída é:

[[[[[1 2 3 4]]]]]
shape: (1, 1, 1, 1, 4)

Cada posição da tupla corresponde a uma dimensão: as 4 primeiras dimensões têm só 1 elemento (foram criadas artificialmente pelo ndmin), e a última tem os 4 valores reais.

Reshape: mudando o formato de um array

Reshape é pegar os elementos de um array e reorganizá-los em outro formato, sem alterar os dados em si — só a “moldura” onde eles ficam.

Exemplo 1 — de 1-D pra 2-D

arr = np.arange(1, 13)
print(arr)

novo = arr.reshape(4, 3)
print(novo)

A saída é:

[ 1  2  3  4  5  6  7  8  9 10 11 12]
[[ 1  2  3]
 [ 4  5  6]
 [ 7  8  9]
 [10 11 12]]

(Usei np.arange(1, 13) só pra gerar rápido os números de 1 a 12 — é o range() do Python, só que retornando um ndarray.)

Exemplo 2 — de 1-D pra 3-D

arr = np.arange(1, 13)
novo = arr.reshape(2, 3, 2)
print(novo)

A saída é:

[[[ 1  2]
  [ 3  4]
  [ 5  6]]

 [[ 7  8]
  [ 9 10]
  [11 12]]]

Nota: o número total de elementos precisa bater. Um array com 12 elementos pode virar (4, 3), (2, 6), (2, 3, 2) etc, mas nunca (3, 5) — 15 é diferente de 12, e o NumPy levanta ValueError se você tentar.

Dimensão desconhecida com -1

Se você não quer calcular uma das dimensões na mão, passa -1 nela e deixa o NumPy resolver sozinho:

arr = np.arange(1, 13)
novo = arr.reshape(3, -1)
print(novo)
print('shape:', novo.shape)

A saída é:

[[ 1  2  3  4]
 [ 5  6  7  8]
 [ 9 10 11 12]]
shape: (3, 4)

Como pedi 3 linhas e o array tem 12 elementos, o NumPy calculou sozinho que a outra dimensão precisa ser 4.

Nota: você só pode usar -1 em uma dimensão por vez — o NumPy não tem como adivinhar duas incógnitas ao mesmo tempo.

Achatando um array com reshape(-1)

Pra transformar qualquer array multidimensional em um array 1-D (o processo chamado de flatten), basta usar reshape(-1):

arr = np.array([[1, 2, 3], [4, 5, 6]])
achatado = arr.reshape(-1)
print(achatado)

A saída é:

[1 2 3 4 5 6]

Nota: assim como o slicing, reshape() geralmente retorna uma view dos dados originais quando possível — outro motivo pra ter o conceito de copy vs view fresco na cabeça.

Iterando arrays

Iterando um array 1-D

Um array 1-D se comporta como qualquer sequência do Python em um for:

arr = np.array([1, 2, 3])
for elemento in arr:
    print(elemento)

A saída é:

1
2
3

Iterando um array 2-D

Um for simples em um array 2-D itera pelas linhas, não pelos elementos individuais:

arr = np.array([[1, 2, 3], [4, 5, 6]])
for linha in arr:
    print(linha)

A saída é:

[1 2 3]
[4 5 6]

Pra chegar nos elementos individuais, você precisa de um for aninhado:

arr = np.array([[1, 2, 3], [4, 5, 6]])
for linha in arr:
    for elemento in linha:
        print(elemento)

A saída é:

1
2
3
4
5
6

Iterando um array 3-D

A lógica se repete: cada nível de for a mais remove uma dimensão, até sobrarem só os escalares.

arr = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
for bloco in arr:
    for linha in bloco:
        for elemento in linha:
            print(elemento)

A saída é:

1
2
3
4
5
6
7
8

Iterando com np.nditer()

Empilhar fors aninhados fica chato rápido conforme a dimensão cresce. A função np.nditer() resolve isso pra qualquer número de dimensões, sem você precisar escrever um loop por nível:

arr = np.array([[[1, 2], [3, 4]], [[5, 6], [7, 8]]])
for elemento in np.nditer(arr):
    print(elemento)

A saída é:

1
2
3
4
5
6
7
8

Um recurso legal do nditer() é poder pular elementos usando slicing dentro dele — por exemplo, pra iterar só os elementos de índice par de cada linha:

arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8]])
for elemento in np.nditer(arr[:, ::2]):
    print(elemento)

A saída é:

1
3
5
7

Iterando com índices usando np.ndenumerate()

Se além do valor você também precisa saber a posição de cada elemento, np.ndenumerate() retorna pares (índice, valor):

arr = np.array([[1, 2, 3], [4, 5, 6]])
for indice, elemento in np.ndenumerate(arr):
    print(indice, elemento)

A saída é:

(0, 0) 1
(0, 1) 2
(0, 2) 3
(1, 0) 4
(1, 1) 5
(1, 2) 6

Cada índice é uma tupla com a posição exata do elemento em cada dimensão — útil quando você precisa não só do valor, mas também de onde ele está no array.

Com copy/view, shape, reshape() e as formas de iterar na manga, você já consegue manipular a estrutura de um array com bastante liberdade. No próximo post da série a gente parte pra juntar, dividir, buscar e ordenar arrays.

Fonte adaptada: NumPy Array Copy vs View, NumPy Array Shape, NumPy Array Reshape, NumPy Array Iterating