Mais sobre arrays ================== Nessa aula vamos passar a usar o ndarray do módulo NumPy e esconder um pouco sua implementação e representação interna que foi o foco das aulas anteriores quando desenvolvemos a classe Array2D. Objetivos de aprendizagem ------------------------- Realize seus estudos para que, ao final dessa aula, você seja capaz de: * acessar elementos e fatias de ``ndarrays``; * realizar operações elemento-a-elemento com ``ndarrays``; * criar e usar objetos do tipo ``ndarray`` em seus programas. Introdução ---------- Na aula anterior, interrompemos a nossa descrição do tipo ``ndarray`` para reforçar os conceitos de ``vista`` e ``cópia`` de arrays. Como as fatias de arrays geram ``vistas`` (ou seja, não geram ``cópias`` como as fatias de listas), o entendimento desses conceitos é fundamental para entender o comportamento das operações com arrays e prever os resultados das operações para que você possa utilizá-las de forma correta em seus programas. Vistas e cópias de Array2D ........................... Os conceitos de ``vista`` e ``cópia`` foram treinados nos exercícios e atividade da aula anterior ao desenvolver os seguintes métodos da classe Array2D: * ``def copy(self)``: retorna um Array2D, cópia de self. * ``def reshape(self, tupla)``: retorna um Array2D, vista de self, com novo formato (shape) definido pelo parâmetro ``tupla``. * ``def carregue_vista(self, lista)``: * ``def carregue_copia(self, lista)``: O Trinket abaixo ilustra uma implementação desses métodos. .. trinket .. raw:: html Observe que na definição da classe Array2D que a criação de vistas nos métodos ``reshape()`` e ``carregue_vista`` implica em modificar o atributo ``data`` para **compartilhar** os dados. Dessa forma, uma alteração de um elemento da lista ``data`` é "vista" em todas as demais ``vistas``. O método ``copy(self)`` cria uma nova lista, incialmente com conteúdo idêntico a ``self``. Apesar de serem inicialmente 'iguais', os dados são armazenados **separadamente**. Ao mudar o valor de um elemento de ``self`` essa mudança não será "vista" na cópia e, portanto, deixarão de ser cópias uma da outra. Finalmente, observe que o método ``carregue_copia(self, lista)`` não cria uma nova lista mas varre ``lista`` e copia, elemento-a-elemento, os valores para o atributo ``self.data``. E por que não fazer, por exemplo, ``self.data = lista[:]``, que cria uma cópia de ``lista`` e associa a ``self.data``? Essa forma não é recomendada pois, nesse caso, perderíamos as vistas já existentes de ``self`` pois essas vistas continuariam a fazer referência à lista antiga. Escreva testes para ``carregue_copia()`` e ``carregue_vista()`` na função ``main()`` do Trinket acima. Mais sobre o uso de tuplas para acessar elementos de um array ------------------------------------------------------------- Observe como a classe Array2D definida acima implementa os métodos especiais ``__getitem__()`` e ``__setitem__()``. Esses métodos recebem tuplas para selecionar um elemento do array. Assim, dado uma Array2D `ar`, devemos usar a notação ``ar[3,2]`` para acessar o elemento na linha de índice 3 e coluna de índice 2, ao invés de usar pares de colchetes como usados em uma matriz aninhada `ma`, como em ``ma[3][2]``. Para acessar um elemento apenas, essa notação usando tuplas não traz nenhum grande benefício pois o comportamento é exatamente o mesmo. No entanto, o uso de tuplas dá muito mais flexibilidade e poder para selecionar uma região retangular de arrays usando fatias. A começar, por exemplo, com a possibilidade de indicar colunas, o que não é possível usando apenas fatias de matrizes representadas usando listas aninhadas. Para ilustrar essa características, considere as funções ``inverta_linhas()`` e ``inverta_colunas()`` abaixo, para inverter as linhas e as colunas de um array. Essas funções mostram como um vista de uma linha e coluna podem ser criadas e copiadas. Observe que a vista pode inclusive ser usada do lado esquerdo de uma atribuição. Nesse caso a fatia do array é tratada como uma região que pode receber os valores de outro array de mesmo tamanho elemento-a-elemento. .. raw:: html .. .. code:: python import numpy as np def main(): ar = np.array(range(12)).reshape(3,4) print(f"Array:\n{ar}\n") inverta_linhas(ar) print(f"Array após inverta_linhas:\n{ar}\n") inverta_colunas(ar) print(f"Array após inverta_colunas:\n{ar}\n") def inverta_linhas( ar ): ''' (ndarray) -> None ''' nlin, ncol = ar.shape for lin in range(nlin//2): cp = ar[lin, :].copy() ar[lin, :] = ar[nlin-lin-1, :] ar[nlin-lin-1, :] = cp def inverta_colunas(ar): ''' (ndarray) -> None ''' nlin, ncol = ar.shape for col in range(ncol//2): cp = ar[:, col].copy() ar[ :, col] = ar[ :, ncol-col-1] ar[ :, ncol-col-1] = cp if __name__ == '__main__': main() Essa forma de atribuição múltipla para fatias de arrays é bastante útil, como ilustrado no trecho de código abaixo. .. raw:: html Impressão de listas aninhadas e arrays -------------------------------------- Observe o resultado dos prints abaixo: .. code:: python >>> import numpy as np >>> A = [ [1,2,3,4], [5,6,7,8] ] >>> print( A ) [[1, 2, 3, 4], [5, 6, 7, 8]] >>> >>> B = np.array(A) >>> print(B) [[1 2 3 4] [5 6 7 8]] >>> Note que a lista A é impressa em uma única linha, enquanto ``print(B)`` mostra a estrutura do array, imprimindo cada linha separadamente. É possível criar ``ndarray``s com 2, 3 ou mais dimensões. Ao imprimir arrays de ``n`` dimensões, a impressão obedece a mesma ordem dos índices. Assim, um array 3D seria impresso da seguinte forma: .. code:: python >>> tri = np.array( range(24) ).reshape(2,3,4) >>> print(tri) [[[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] [[12 13 14 15] [16 17 18 19] [20 21 22 23]]] >>> bi = tri.reshape((4,6)) >>> print(bi) [[ 0 1 2 3 4 5] [ 6 7 8 9 10 11] [12 13 14 15 16 17] [18 19 20 21 22 23]] Nesse exemplo um array de uma dimensão é criado com os inteiros de 0 a 23 (resultado do ``range(24)``). Uma forma alternativa seria usar diretamente o método ``np.arrange(24)``. O método ``reshape()`` permite alterar as dimensões do array, sem alterar os dados, de forma similar ao comportamento da classe Array2D. Para isso o produto dos argumentos usados no reshape, nesse caso a tupla (2,3,4), deve ser igual ao número de elementos do array (2x3x4 = 24). Como resultado são impressas 2 matrizes de 3 linhas e 4 colunas cada, que representa o conteúdo do array de 3 dimensões. Observe que podemos usar ``reshape()`` para criar uma vista do array com outro formato, como no exemplo, uma array $4\times6$ de nome ``bi``. Uma outra conveniência fornecida para arrays muito grandes é a impressão apenas dos cantos, como no exemplo abaixo. Você pode modificar esse comportamento pelo método `set_printoptions `__. .. code:: python >>> grande = np.arange(10000).reshape(100,100) >>> print(grande) [[ 0 1 2 ... 97 98 99] [ 100 101 102 ... 197 198 199] [ 200 201 202 ... 297 298 299] ... [9700 9701 9702 ... 9797 9798 9799] [9800 9801 9802 ... 9897 9898 9899] [9900 9901 9902 ... 9997 9998 9999]] >>> Manipulando as dimensões de um array ------------------------------------ As dimensões de um array podem ser manipuladas criando vistas diferentes usando, por exemplo, os métodos ``ravel()`` e ``reshape()`` e o atributo ``T``, como ilustrado no trecho de código a seguir. .. code:: python >>> bi = np.arange(6).reshape(2,3) >>> bi array([[0, 1, 2], [3, 4, 5]]) >>> bi.ravel() array([0, 1, 2, 3, 4, 5]) >>> bi array([[0, 1, 2], [3, 4, 5]]) >>> bi.T array([[0, 3], [1, 4], [2, 5]]) >>> bi array([[0, 1, 2], [3, 4, 5]]) >>> bi.T.shape (3, 2) >>> bi.shape (2, 3) >>> bi.resize(3,2) >>> bi array([[0, 1], [2, 3], [4, 5]]) >>> Esse exemplo mostra a criação de um array 2D referenciado por ``bi``. O método ``ravel()`` retorna uma **vista** (view) 1D do array, mas sem modificar o seu conteúdo. O atributo ``T`` retorna a transposta do array e também não modifica o array ``bi``. As dimensões do array podem ser alteradas **definitivamente** pelo método ``resize()``. Ao usar métodos que retornam vistas do array lembre-se que, apesar de possuírem dimensões (``shape``) diferentes, seu conteúdo no atributo ``data`` é o mesmo, ou seja, todos compartilham os mesmos dados. Apesar desse compartilhamento tornar o processamento mais eficiente pois se evita a cópia dos dados, isso pode ser indesejável em certas situações. Utilize o método ``copy()`` sempre que precisar criar arrays com dados separados. As cópias, ao serem modificadas, deixam de ser cópias pois os arrays originais não são alterados. .. , como mostra o trecho de código a seguir. .. code:: python >>> ci = bi.reshape(3,2) >>> ci array([[0, 1], [2, 3], [4, 5]]) >>> bi array([[0, 1, 2], [3, 4, 5]]) >>> bi[1,1] = -1 >>> bi array([[ 0, 1, 2], [ 3, -1, 5]]) >>> ci array([[ 0, 1], [ 2, 3], [-1, 5]]) >>> A variável ``ci`` compartilha o mesmo conteúdo (dados) que a variável ``bi``, apesar de terem ``shapes`` diferentes (``bi`` é (2,3) enquanto ``ci`` é (3,2)). Quando o elemento ``bi[1,1]`` é alterado, é possível ver o resultado em ``ci``. Caso esse efeito, devemos criar uma cópia do array usando o método ``copy()`` como abaixo. .. code:: python >>> bi = np.arange(6).reshape(2,3) >>> ci = bi.reshape(3,2) >>> di = ci.copy() >>> bi[1,1] = 99 >>> bi array([[ 0, 1, 2], [ 3, 99, 5]]) >>> ci array([[ 0, 1], [ 2, 3], [99, 5]]) >>> di array([[0, 1], [2, 3], [4, 5]]) >>> O ``copy()`` cria um clone do array, ou seja, replica os dados em outro bloco de memória. Como ``di`` é uma cópia de ``ci``, quando ``bi[1,1]`` é alterado, o conteúdo de ``ci`` se altera pois compartilha o mesmo bloco de dados (dizemos que tanto ``bi`` quanto ``ci`` fazem referência ao mesmo bloco de dados), mas o conteúdo de ``di`` permanece o mesmo. Vale dizer que criar uma cópia de uma estrutura muito grande é computacionalmente caro e deve ser evitado se possível. Por criar apenas vistas diferentes usando referências para o mesmo bloco de dados, essas manipulações de dimensão são operações bastante eficientes em NumPy mesmo para estruturas grandes. Operações básicas ----------------- Apesar da semelhança do tipo ``numpy.ndarray`` com as listas de Python, o NumPy oferece muito mais recursos para operação e manipulação de arrays que facilitam a computação científica. Em ciência da computação, **vetorização** (`vectorization `__) é a generalização das operações em escalares a vetores e matrizes de várias dimensões. Linguagens de programação que aplicam de maneira transparente e concisa operação sobre arrays são chamadas de `array programming languages `__. Exemplos de linguagens que admitem vetorização são `GNU Octave `__, `Julia `__, `R `__ e `Python NumPy `__. Os arrays do NumPy permitem usar os símbolos das operações básicas como `*`, `+`, etc, para calcular um novo array "por elemento", como ilustrado no trecho de código abaixo. .. code:: python >>> import numpy as np >>> a = np.array([[1,2],[3,4]]) >>> a array([[1, 2], [3, 4]]) >>> b = np.array([[0,2],[3,1]]) >>> b array([[0, 2], [3, 1]]) >>> a+b array([[1, 4], [6, 5]]) >>> a*b array([[0, 4], [9, 4]]) >>> b/a array([[ 0. , 1. ], [ 1. , 0.25]]) Observe que não basta ter o mesmo número de elementos, mas a forma (``shape``) do array também é importante, como mostra o exemplo a seguir. .. code:: python >>> import numpy as np >>> a = np.array([[1,2],[3,4]]) >>> a.shape (2, 2) >>> c = np.array([5,6,7,8]) >>> c array([5, 6, 7, 8]) >>> c.shape (4, ) >>> a + c Traceback (most recent call last):s File "", line 1, in ValueError: operands could not be broadcast together with shapes (2,2) (4,) >>> Operações entre arrays de tamanhos distintos ............................................ Em muitos casos, é necessário trabalhar com arrays com um número distinto de elementos, ou de formas diferentes. Nesse caso, é necessário entender as regras do NumPy que difundem as informações do array menor para o array maior. O termo **difusão** (`broadcasting `__) descreve como NumPy trata arrays de dimensões diferentes em operações aritméticas. O array menor é "difundido" a uma array maior para que tenham as mesmas dimensões. No caso de um escalar, o valor do escalar é difundido para todos os elementos do array maior, como no exemplo: .. code:: python >>> import numpy as np >>> a = np.array([[1,2],[3,4]]) >>> a array([[1, 2], [3, 4]]) >>> a + 1 array([[2, 3], [4, 5]]) >>> a * 2 array([[2, 4], [6, 8]]) >>> a / 2 array([[ 0.5, 1. ], [ 1.5, 2. ]]) >>> No caso de arrays com uma das dimensões unitárias (um array linha ou coluna por exemplo), mas onde as demais dimensões sejam compatíveis com a do array maior, o array menor é difundido para a outra dimensão do array maior. Veja os exemplos abaixo. .. code:: python >>> import numpy as np >>> a = np.array(range(1,13,2)).reshape(2,3) >>> a array([[ 1, 3, 5], [ 7, 9, 11]]) >>> b = ([4,5,6]) >>> a - b array([[-3, -2, -1], [ 3, 4, 5]]) >>> c = np.array([[0],[1]]) >>> c array([[0], [1]]) >>> c.shape (2, 1) >>> a * c array([[ 0, 0, 0], [ 7, 9, 11]]) >>> d = np.array([1,2]) >>> a * d Traceback (most recent call last): File "", line 1, in ValueError: operands could not be broadcast together with shapes (2,3) (2,) >>> b * c array([[0, 0, 0], [4, 5, 6]]) >>> c + b array([[4, 5, 6], [5, 6, 7]]) >>> Observe que nos dois últimos exemplos (``b*c`` e ``b+c``), quando a operação envolve dois arrays com uma dimensão unitária, por exemplo, ``(1,3)`` e ``(2,1)``, o resultado é um array de dimensão ``(2,3)``. Outras funções do NumPy ----------------------- Talvez você se sinta incomodado que o operador `*` não calcula o produto de duas matrizes, mas o produto elemento a elemento de dois arrays de mesma dimensão. Felizmente, o NumPy oferece vários outros recursos como para: * **Álgebra Linear** O produto de matrizes pode ser calculado usando o método ``dot`` .. code:: python >>> import numpy as np >>> a = np.array(range(1,13,2)).reshape(2,3) >>> a array([[ 1, 3, 5], [ 7, 9, 11]]) >>> b = np.array(range(11,-1,-1)).reshape(3,4) >>> b array([[11, 10, 9, 8], [ 7, 6, 5, 4], [ 3, 2, 1, 0]]) >>> a.dot(b) array([[ 47, 38, 29, 20], [173, 146, 119, 92]]) >>> b.dot(a) Traceback (most recent call last): File "", line 1, in ValueError: shapes (3,4) and (2,3) not aligned: 4 (dim 1) != 2 (dim 0) >>> Além de calcular o produto de vetores e matrizes, o NumPy oferece também vários outros recursos para inversão de matrizes e solução de sistemas lineares, cálculo de normas, de auto-valores e auto-vetores, etc. Para saber mais, consulte a documentação sobre os recursos de `Álgebra Linear `__. * **Estatística** `Rotinas estatísticas `__ para cálculo de média, variância e desvio padrão, entre outras, além de funções para vários tipos de `amostragem `__, inclusive um gerador de números pseudo-aleatórios. **Matemática** Diversas `funções matemáticas `__ como `sqrt`, `log`, `exp`, `min`, `max`, `sum`, `tan`, `sin`, `cos`, etc. * **e muitas outras** Como funções financeiras, Fast Fourier Transform, polinômios, ordenação, etc. Dê uma olhada no conjunto de rotinas do NumPy em `http://docs.scipy.org/doc/numpy-dev/reference/routines.html `__. Exercícios ---------- O **Jogo da Vida** (`The Game of Life `__) é um autômato celular (`cellular automaton `__) introduzido por John Horton Conway em 1970. Um automato celular consiste de uma rede de células. Cada célula pode estar em um número finito de estados, como *morta* ou *viva*. O "jogo" é na verdade uma simulação que permite observar a evolução de um processo a partir de uma certa condição inicial. O jogo se desenvolve sobre uma matriz bi-dimensional que pode ser tão grande quanto se queira. Vamos chamar essa matriz de **mapa**. Cada posição ou célula do mapa pode estar vazia (= célula morta) ou ocupada por um agente (= célula viva). Cada posição possui também até 8 posições vizinhas: imediatamente acima, abaixo, aos lados e nas diagonais. Em um determinado instante, o mapa contém uma geração de agentes. A geração no instante seguinte é determinada segundo as regras abaixo: - se uma célula [i,j] está vazia então: - um novo agente nasce em [i,j] se essa célula possui exatamente 3 agentes vizinhos; - se uma célula [i,j] possui um agente então: - o agente em [i,j] sobrevive se possui 2 ou 3 agentes vizinhos; - o agente em [i.j] morre se possuir menos de 2 agentes vizinhos, por falta de recursos; - o agente em [i.j] morre se possuir mais de 3 agentes vizinhos, por excesso de competição. Supondo que um mapa M é representado por um ``ndarray`` onde células vazias tem valor ``0`` (zero) e agentes tem valor ``1`` (um), escreva a seguinte função: .. code:: python def iteracao(M): ''' (mapa) -> None Recebe uma mapa (ndarray) com a geração de agentes em um determinado instante e atualiza o mapa de tal forma que represente geração de agentes no instante seguinte. ''' A cada iteração, o mapa deve ser varrido e, para cada célula, a função deve calcular o seu novo valor baseado nas regras do jogo. Observe que o próximo estado do mapa (no instante seguinte) depende **exclusivamente** do estado (no instante) atual. Veja alguns padrões produzidos pelo jogo da vida em `Glider `__. Variantes do jogo da vida são `Day and Night `__, `Highlife `__, `Life without Death `__ e `Seeds `__. **Dicas**: #. o método ``sum()`` calcula a soma dos elementos de um array e pode ser aplicado em fatias de arrays. Por exemplo, execute o seguinte trecho de código para ver o resultado. .. code:: python import numpy as np x = np.array( range(12) ).reshape(3,4) print(x) print(x[1:3,1:3]) print(x[1:3, 1:3].sum()) #. Teste usando ndarrays bem pequenos. Onde estamos e para onde vamos? ------------------------------- Arrays são estruturas muito utilizadas em computação científica. Nessa aula começamos a abstrair a implementação para usar essas estruturas como mapas ou matrizes e manipulando partes dessas estruturas usando fatias e operações com arrays. Nas próximas aulas vamos continuar esse processo de abstração para fortalecer seu domínio sobre arrays e operações com arrays. Para saber mais --------------- * `Matrizes usando listas aninhadas `__. * `NumPy quickstart `__.