Exercício 2: Python, Numpy e Graphviz

Este exercício apresenta com exemplos práticos os conceitos básicos sobre os tipos de alto nível da linguagem de programação Python e sobre alguns tipos adicionais oferecidos pela biblioteca Numpy. São apresentados também alguns exemplos de visualização de grafos utilizando o Graphviz integrado ao ambiente Adessowiki.

1. Python: Tipos de Alto Nível

Todos os dados em programas Python são representados por objetos e relações entre objetos [1]. Além dos tipos básicos de dados (inteiros, números de ponto flutuante, booleanos), a linguagem Python disponibiliza tipos de alto nível. A seguir estão relacionados alguns deles.

1.1. Sequências

Elas representam conjuntos finitos ordenados indexados por números não negativos. Correspondem aos arrays encontrados em outras linguagens. Podem ser imutáveis ou mutáveis. As sequências imutáveis não podem ser alteradas após sua criação, ou seja, elementos não podem ser adicionados, removidos ou alterados. Strings e Tuplas são sequências imutáveis. As Listas são exemplos de sequências mutáveis [1] e, desta forma, elas podem ser alteradas.

O código abaixo exemplifica a criação de sequências e a manipulação básica de sequências mutáveis.

 1 # Criação de uma lista
 2 # Note que é possível inserir objetos de tipos diferentes em uma mesma lista
 3 l = ["a", "b", 3, 5, "andre"]
 4 print 'Manipulação de LISTAS:'
 5 print "#1: l = ", l
 6 
 7 # Abaixo são realizadas realizadas manipulações em sequências mutáveis
 8 
 9 # Remoção do elemento com índice 2
10 del l[2]
11 print "#2: l = ", l
12 
13 # Inserção de um elemento no final da lista
14 l.append(75)
15 print "#3: l = ", l
16 
17 # Alteração do elemento com índice 1
18 l[1] = 'bola'
19 print "#4: l = ", l
20 
21 # Extensão da lista l com uma nova lista l1
22 l1 = [12, 'banana', 'cobra', 15]
23 l.extend(l1)
24 print "#5: l = ", l
25 ################################################################################
26 # Criação de uma string
27 s = "andre"
28 print '\nSequências imutáveis:'
29 print "String s = ", s
30 
31 # Criação de uma tupla
32 t = ("a", "b", 3, 5, "andre")
33 print "Tupla t = ", t
34 
35 # Apesar de serem imutáveis, Strings e Tuplas permitem o acesso
36 # individual aos seus elementos
37 print 'Primeira letra da string s: ', s[0]
38 print 'Dois elementos da tupla t:', t[1:3]
Manipulação de LISTAS:
#1: l =  ['a', 'b', 3, 5, 'andre']
#2: l =  ['a', 'b', 5, 'andre']
#3: l =  ['a', 'b', 5, 'andre', 75]
#4: l =  ['a', 'bola', 5, 'andre', 75]
#5: l =  ['a', 'bola', 5, 'andre', 75, 12, 'banana', 'cobra', 15]

Sequências imutáveis:
String s =  andre
Tupla t =  ('a', 'b', 3, 5, 'andre')
Primeira letra da string s:  a
Dois elementos da tupla t: ('b', 3)

As sequências no Python suportam a operação de fatiamento (slicing). Sendo uma lista na linguagem Python, a operação seleciona todos os elementos de com índice , tal que [1]. Uma fatia é uma sequência de mesmo tipo que a sequência original.

 1 print 'Lista original l = ', l
 2 
 3 # Impressão em tela de uma fatia da lista "l"
 4 print 'l[2:7] = ',l[2:7]
 5 
 6 # Caso o índice inicial "i" seja omitido, o fatiamento iniciará pelo
 7 # primeiro elemento da sequência
 8 print 'l[:7] = ',l[:7]
 9 
10 # Similarmente, omitir o índice final faz com que o fatiamento termine
11 # ao atingir o último elemento da sequência
12 print 'l[2:] = ',l[2:]
13 
14 # É possível também mudar o passo do fatiamento e selecionar
15 # elementos não adjacentes
16 # Na linha abaixo é realizada a seleção dos elementos 2 ao 7,
17 # pulando de 2 em 2. Ou seja, são selecionados os elementos
18 # 2, 4 e 6 da lista
19 print 'l[2:7:2] = ',l[2:7:2]
20 
21 # O passo pode ser usado também para inverter a ordem dos elementos
22 # Isso é feito especificando-se um passo negativo e iniciando o
23 # fatiamento pelo elemento de maior índice, como abaixo
24 print 'l[7:2:-1] = ',l[7:2:-1]
Lista original l =  ['a', 'bola', 5, 'andre', 75, 12, 'banana', 'cobra', 15]
l[2:7] =  [5, 'andre', 75, 12, 'banana']
l[:7] =  ['a', 'bola', 5, 'andre', 75, 12, 'banana']
l[2:] =  [5, 'andre', 75, 12, 'banana', 'cobra', 15]
l[2:7:2] =  [5, 75, 'banana']
l[7:2:-1] =  ['cobra', 'banana', 12, 75, 'andre']

1.2. Dicionários

Dicionários são estruturas de mapeamento em que possibilitam usar índices de tipos variados, desde que estes sejam do tipo imutável (números, tuplas e strings, por exemplo) [1].

O código abaixo exemplifica o uso de dicionários.

 1 # Criação de um dicionário
 2 d = {"classe" : "laranja", "peso_medio" : 0.150}
 3 
 4 print "Classe: ", d["classe"]
 5 print "Peso medio: %f kg" % d["peso_medio"]
 6 
 7 # Lista as chaves do dicionário "d"
 8 print '\nChaves do dicionário: ', d.keys()
 9 
10 # Lista os valores do dicionário "d"
11 print 'Valores do dicionário:', d.values()
Classe:  laranja
Peso medio: 0.150000 kg

Chaves do dicionário:  ['peso_medio', 'classe']
Valores do dicionário: [0.14999999999999999, 'laranja']

1.3. Identação

Uma particularidade da linguagem Python é a identação. Ela é extremamente importante para definir blocos de execução [1].

O código abaixo exemplifica a definição de blocos usando identação:

1 # Definição de um bloco usando indentação
2 for i in range(3):
3     print i
4 
5 # Se não houvesse identação, como abaixo, o código resultaria em um erro
6 #for i in range(3):
7 #print i
0
1
2

2. NumPy: Operações Matriciais no Python

O NumPy é uma biblioteca para Python que possibilita o uso de operações matriciais. O tipo de dados ndarray suporta a criação de arrays multidimensionais que podem ser manipulados por meio de uma variedade de rotinas que, tipicamente, são mais rápidas e exigem menos código. São as chamadas operações matriciais [2]. O código a seguir apresenta algumas das formas para se criar ndarrays.

 1 import numpy as np
 2 
 3 # 1ª Criar um array vazio, ou seja, com elementos não inicializados (aleatórios).
 4 #
 5 # Observe que são passados como parâmetros uma tupla, para informar as dimensões do array,
 6 # e o tipo de dados do array (opcional, padrão=double)
 7 #
 8 shape = (5, 3)
 9 array_vazio = np.empty(shape, dtype=int)
10 print "ARRAY VAZIO:"
11 print array_vazio, "\n\n"
12 
13 # 2ª Criar um array com elementos inicializados com o valor 1
14 array_uns = np.ones((2,5), dtype=int)
15 print "ARRAY DE UNS:"
16 print array_uns, "\n\n"
17 
18 # 3ª Criar um array com elementos inicializados com o valor 0
19 array_uns = np.zeros((2,5), dtype=int)
20 print "ARRAY DE ZEROS:"
21 print array_uns, "\n\n"
22 
23 # 4ª Criar um array a partir de uma sequência padrão do Python
24 l = [1, 2, 3, 4, 5]
25 array_1 = np.array(l)
26 print "ARRAY DE 1-D a partir de uma lista:"
27 print array_1, "\n\n"
28 
29 # Criar um arranjo numérico
30 # a1 - uma sequência de 0 a 7
31 # a2 - uma sequência de números ímpares de 1 a 13
32 # a3 - uma sequência inversa de números pares de 14 a 2
33 a1 = arange(8)
34 a2 = arange(1, 14, 2)
35 a3 = arange(14, 1, -2)
36 print "UMA SEQUÊNCIA DE 0 A 7"
37 print a1, "\n\n"
38 print "UMA SEQUÊNCIA DE NÚMEROS ÍMPARES DE 1 A 13"
39 print a2, "\n\n"
40 print "UMA SEQUÊNCIA DE NUMEROS PARES DE 14 A 2"
41 print a3
ARRAY VAZIO:
[[    139999199112968            65791792 6858807862792383791]
 [8247620764386358136 5852487309534520691 7002871132427801393]
 [5283079906893194350 3689909583155249485 7957911347540287282]
 [                103            66146296                  33]
 [           66135152     139999199112872                 128]] 


ARRAY DE UNS:
[[1 1 1 1 1]
 [1 1 1 1 1]] 


ARRAY DE ZEROS:
[[0 0 0 0 0]
 [0 0 0 0 0]] 


ARRAY DE 1-D a partir de uma lista:
[1 2 3 4 5] 


UMA SEQUÊNCIA DE 0 A 7
[0 1 2 3 4 5 6 7] 


UMA SEQUÊNCIA DE NÚMEROS ÍMPARES DE 1 A 13
[ 1  3  5  7  9 11 13] 


UMA SEQUÊNCIA DE NUMEROS PARES DE 14 A 2
[14 12 10  8  6  4  2]

Enquanto o array (lista) da linguagem Python pode mudar de tamanho dinamicamente, o tamanho do ndarray é fixado no momento da criação [2]. A única forma de alterar o tamanho do ndarray é criando-se um novo e apagando-se o original, ou seja, por meio de uma cópia.

2.1. Cópia e visualização de arrays do NumPy

Algumas vezes os dados dos arrays do NumPy são copiados para um novo array durante a execução de operações; porém, isso nem sempre ocorre. Este cenário muitas vezes é uma fonte de confusão. Há 3 casos possíveis [1]:

1º Caso: nenhuma cópia é feita

Atribuições simples não realizam qualquer cópia dos objetos do tipo ndarray e nem de seus dados, como ilustra o código a seguir:

 1 # Abaixo, "a" é criado como uma sequência de 0 a 7.
 2 # A atribuição de "a" a "b" apenas faz "b" apontar para "a". É uma
 3 # simples cópia de referência do objeto
 4 a = arange(8)
 5 b = a
 6 print "a é b?", a is b
 7 
 8 # Se alterarmos o shape de "b", o de "a" também será alterado
 9 print "Antigo shape de a: ", a.shape
10 b.shape = (2, 4)
11 print "Novo shape de a: ", a.shape
a é b? True
Antigo shape de a:  (8,)
Novo shape de a:  (2, 4)

2º Caso: cópia superficial e visualização

Objetos do tipo ndarray podem compartilhar os mesmos dados. O método view() cria um novo ndarray, ou seja, faz uma cópia. Todavia, os dados do novo ndarray são os mesmo dados do antigo. Isso significa, essencialmente, que alterar os dados de um dos array vai refletir imediatamente no outro array. Esse tipo de cópia é denominado cópia superficial.

 1 # Abaixo, "a" é criado como uma sequência de 0 a 7.
 2 #
 3 # "b" agora é uma visualização de "a", ou seja, um objeto
 4 # diferente de "a" que aponta para os mesmo dados de "a"
 5 #
 6 a = arange(8)
 7 b = a.view()
 8 print "a é b?", a is b
 9 
10 # Agora, se alterarmos o shape de "b", o de "a" não será alterado
11 print "Antigo shape de a: ", a.shape
12 b.shape = (2, 4)
13 print "Novo shape de a: ", a.shape
14 
15 # Mas se alteramos os dados de "b", os dados de "a" também serão alterados
16 print "Antigo a: ", a
17 b[0,2] = 30
18 b[1,0] = 15
19 print "Novo a: ", a
a é b? False
Antigo shape de a:  (8,)
Novo shape de a:  (8,)
Antigo a:  [0 1 2 3 4 5 6 7]
Novo a:  [ 0  1 30  3 15  5  6  7]

As operações de fatiamento e o método reshape() também produzem uma cópia superficial do array, como é mostrado a seguir.

 1 # Abaixo, "a" é criado como uma sequência de 0 a 7.
 2 # "b" agora é uma visualização de a criada por meio de
 3 # uma operação de fatiamento (slicing)
 4 a = arange(8)
 5 b = a[2:]
 6 c = a.reshape(4,2)
 7 print "a é b?", a is b
 8 print "a é c?", a is c
 9 print "A base de b é a?", b.base is a
10 print "A base de c é a?", c.base is a
11 
12 # Agora, se alterarmos o shape de "b", o de "a" e o de "c" não será alterado
13 print "Antigo shape de a e de c: ", a.shape, " e ", c.shape
14 b.shape = (2, 3)
15 print "Novo shape de a e de c: ", a.shape, " e ", c.shape
16 
17 # Mas se alteramos os dados de "b", os dados de "a" e de "c" também serão alterados
18 print "Antigo a, b e c: \n", a, "\n\n", b, "\n\n", c, "\n\n"
19 b[0,1] = 30
20 b[1,0] = 15
21 print "Novo a, b e c: \n", a, "\n\n", b, "\n\n", c
a é b? False
a é c? False
A base de b é a? True
A base de c é a? True
Antigo shape de a e de c:  (8,)  e  (4, 2)
Novo shape de a e de c:  (8,)  e  (4, 2)
Antigo a, b e c: 
[0 1 2 3 4 5 6 7] 

[[2 3 4]
 [5 6 7]] 

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


Novo a, b e c: 
[ 0  1  2 30  4 15  6  7] 

[[ 2 30  4]
 [15  6  7]] 

[[ 0  1]
 [ 2 30]
 [ 4 15]
 [ 6  7]]

Para entender melhor a cópia superficial, deve-se entender o atributo base do ndarray. O atributo base armazena a referência ao objeto que é o proprietário da "memória" (dados brutos) do array.

1 # Abaixo, "a" é criado como uma sequência de 0 a 7.
2 # A base de um array que é proprietário de sua própria
3 # memória é None
4 a = arange(8)
5 print "A base de a é None?", a.base is None
6 
7 # Se criamos "b" como uma cópia superfical de "a", a base de "b" será "a"
8 b = a.view()
9 print "A base de b é a?", b.base is a
A base de a é None? True
A base de b é a? True

3º Caso: cópia profunda

O método copy() do ndarray realiza uma cópia completa do objeto e de seus dados.

 1 # Abaixo, "a" é criado como uma sequência de 0 a 7.
 2 # "b" agora é uma cópia completa de a, ou seja, não
 3 # possui nada em comum com o objeto "a"
 4 a = arange(8)
 5 b = a.copy()
 6 print "a é b?", a is b
 7 print "A base de b é a?", b.base is a
 8 
 9 # Agora, se alterarmos o shape de "b", o de "a" não será alterado
10 print "Antigo shape de a: ", a.shape
11 b.shape = (2, 4)
12 print "Novo shape de a: ", a.shape, "\n\n"
13 
14 # Assim como, se alteramos os dados de "b", os dados de "a" também NÃO serão alterados
15 print "Antigo a e b: \n", a, "\n\n", b, "\n\n"
16 b[0,2] = 30
17 b[1,0] = 15
18 print "Novo a e b: \n", a, "\n\n", b
a é b? False
A base de b é a? False
Antigo shape de a:  (8,)
Novo shape de a:  (8,) 


Antigo a e b: 
[0 1 2 3 4 5 6 7] 

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


Novo a e b: 
[0 1 2 3 4 5 6 7] 

[[ 0  1 30  3]
 [15  5  6  7]]

O real benefício de se utilizar os arrays do NumPy é a facilidades para realizar operações matemáticas avançadas entre arrays. Tipicamente, essas operações são executadas de forma muito eficientemente, além de exigirem menos código. A seguir são apresentadas algumas das possíveis operações.

 1 # Abaixo, "A" e "B" são criados como matrizes de tamanho 4 x 4.
 2 A = arange(16).reshape((4,4))
 3 B = zeros((4,4))
 4 print "A = ", A
 5 print "B = ", B
 6 
 7 # Abaixo são realizadas diversas operações que afetam todos os
 8 # elementos das matrizes A e B
 9 C = A + B
10 print "Soma de matrizes:"
11 print "A + B = ", C
12 C = A * B
13 print "Multiplicação elemento-a-elemento:"
14 print "A * B = ", C
15 C = dot(A, B)
16 print "Multiplicação de matrizes:"
17 print "A . B = ", C
18 C = A + 5
19 print "Soma uma matriz e um escalar:"
20 print "A + 5 = ", C
21 C = A * 3
22 print "Multiplicação de uma matriz por um escalar:"
23 print "A + 3 = ", C
A =  [[ 0  1  2  3]
 [ 4  5  6  7]
 [ 8  9 10 11]
 [12 13 14 15]]
B =  [[ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]
Soma de matrizes:
A + B =  [[  0.   1.   2.   3.]
 [  4.   5.   6.   7.]
 [  8.   9.  10.  11.]
 [ 12.  13.  14.  15.]]
Multiplicação elemento-a-elemento:
A * B =  [[ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]
Multiplicação de matrizes:
A . B =  [[ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]
Soma uma matriz e um escalar:
A + 5 =  [[ 5  6  7  8]
 [ 9 10 11 12]
 [13 14 15 16]
 [17 18 19 20]]
Multiplicação de uma matriz por um escalar:
A + 3 =  [[ 0  3  6  9]
 [12 15 18 21]
 [24 27 30 33]
 [36 39 42 45]]

3. GraphViz: Visualização de Grafos

O GraphViz é um software que tem como propósito criar visualizações de grafos. Uma das formas para se especificar um grafo é por meio do uso da linguagem "DOT". Essa é também a forma escolhida para ser utilizada no ambiente adessowiki. A seguir são apresentados exemplos sobre como uma visualização de um grafo pode ser crianda no ambiente adessowiki.

Usando-se a diretiva de código "graphviz" para especificar as instruções DOT:

1. graph konigsberg {
2.      fontname = "Times"
3.      fontsize = 10
4.      a -- {c d};
5.      a -- c;
6.      b -- {c d};
7.      b -- c;
8.      c -- d;
9. }
/media/_xsb/courseIA368Q1S2012/and_2/GRVIZ71532_001.png

Pela função *mmgraphviz* com as instruções DOT especificadas em um arquivo de texto:

 1 graph_specification = '''
 2                         graph konigsberg {
 3                              fontname = "Times"
 4                              fontsize = 10
 5                              a -- {c d};
 6                              a -- c;
 7                              b -- {c d};
 8                              b -- c;
 9                              c -- d;
10                         }
11                       '''
12 mmgraphviz(graph_specification, title='As Sete Pontes de Königsberg')
/media/_xsb/courseIA368Q1S2012/and_2/GRVIZ71532_002.png

As Sete Pontes de Königsberg

E, finalmente, por meio de programação utilizando-se a classe GvGen.

 1 import StringIO
 2 
 3 graph = gvgen.GvGen()
 4 graph.smart_mode = 1
 5 a = graph.newItem('a')
 6 b = graph.newItem('b')
 7 c = graph.newItem('c')
 8 d = graph.newItem('d')
 9 graph.newLink(a, c)
10 graph.newLink(a, c)
11 graph.newLink(a, d)
12 graph.newLink(b, c)
13 graph.newLink(b, c)
14 graph.newLink(b, d)
15 graph.newLink(c, d)
16 
17 graph.propertyAppend(c, "color", "yellow")
18 graph.propertyAppend(d, "color", "yellow")
19 
20 fd = StringIO.StringIO()
21 graph.dot(fd)
22 dot_specification = fd.getvalue().replace('digraph','graph').replace('>','-')
23 
24 mmgraphviz(dot_specification, title='As Sete Pontes de Königsberg')
/media/_xsb/courseIA368Q1S2012/and_2/GRVIZ71532_003.png

As Sete Pontes de Königsberg

[1](1, 2, 3, 4, 5, 6) Python - Site oficial. http://www.python.org. Último acesso: 28/02/2011.
[2](1, 2) SciPy - Site oficial. http://www.scipy.org. Último acesso: 28/02/2011.