Lab 1: Revisão de Conceitos Básicos

Nesse notebook revisaremos os conceitos básicos da linguagem de programação Python, Algebra Linear, o uso da biblioteca NumPy e conceitos de Programação Funcional.

A primeira parte abordará as diferenças de sintaxe da linguagem Python em relação ao Java e ao C/C++ e formas de otimizar o código. A segunda parte mostrará, os conceitos básicos da biblioteca Numpy e como utilizá-la para aplicar conceitos de Algebra Linear. Finalmente, a terceira parte mostrará os conceitos de Programação Funcional como Expressões Lambda e Funções de Ordem Alta.

Para navegar pelo notebook execute cada uma das células utilizando a tecla de atalho SHIFT-ENTER, isso executará as intruções da célula In mostrando o resultado na célula Out correspondente. Todas as variáveis criadas em uma célula podem ser acessadas em todas as células subsequentes.

As células-exercícios iniciam com o comentário # EXERCICIO e os códigos a serem completados estão marcados pelos comentários <COMPLETAR>.

Nesse notebook:

Parte 1: Python

Parte 2: NumPy

Parte 3: Programação Funcional

Parte 1: Python

(1a) Declarações de Variáveis

O tipo de variáveis no Python é definido dinâmicamente pelo interpretador. Não existe necessidade em declarar.


In [4]:
x = 10          # x é um inteiro
print type(x)

x = 1.3         # x é um ponto flutuante
print type(x)

x = "Ola"       # x é uma string
print type(x)

x = [1, 5, 10]  # x é uma lista
print type(x)


<type 'str'>
<type 'float'>
<type 'str'>
<type 'list'>

(1b) Indentações

No Python a indentação faz o papel das chaves para determinar o bloco de comandos de uma função, condicional ou laço de repetição. O início de um bloco é definido pelo caractere ":".


In [5]:
x = 10

for i in range(20):
    # Início da repetição For
    x = x + 1
    if x%2 == 0:
        # Instrução se condição verdadeira
        x = x + 1
    else:
        # Instrução se a condição for falsa
        x = x + 2
    # Fim do bloco de repetição For
print x # Isso está fora do for! Boa Ideia!


51

(1c) Funções

As funções no Python podem receber quantas entradas necessárias e retornar múltiplas saídas. A declaração da função é precedida pela palavra-chave def.


In [8]:
def Soma(x,y):
    return x+y

def Mult(x,y):
    return x*y

def SomaMult(x,y):
    return x+y, x*y  # múltiplas saídas separadas por vírgula

print Soma(10,2), Mult(100,2), SomaMult(10,2)

# O retorno de múltiplas saídas podem ser atribuídas diretamente para múltiplas variáveis
w,z = SomaMult(100,2)
print w, z


12 200 (12, 20)
102 200

(1d) Tipos Especiais

Além dos tipos básicos de variáveis o Python possui os tipos lista (list), tupla (tuple) e dicionários (dict).

As listas e tuplas são agregadores de valores e podem agrupar valores não necessariamente dos mesmos tipos. A diferença entre os dois é que listas são mutáveis (podem ser alterados) e tuplas são imutáveis. Elas são indexadas a partir do índice 0.

Os dicionários são arranjos associativos que permite associar uma chave (de qualquer tipo) a um valor (também de qualquer tipo).


In [16]:
lista = [1, 2, True, "palavra"]
tupla = (1, 2, True, "palavra")
lista[1] = 3.0
print lista


[1, 3.0, True, 'palavra']

In [17]:
tupla[1] = 3.0 # Vai dar erro!


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-17-9ab81c67d3ce> in <module>()
      1 
----> 2 tupla[1] = 3.0 # Vai dar erro!

TypeError: 'tuple' object does not support item assignment

In [9]:
# range(n) gera uma lista de valores entre 0 e n-1
# len(lista) retorna o tamanho de uma lista
def DobraValores(lista):
    for i in range(len(lista)):
        lista[i] = lista[i]*2
    return lista

lista = [1,2,3,4]
lista2 = DobraValores(lista)

print lista, lista2  # As listas são passadas como referência para as funções


[2, 4, 6, 8] [2, 4, 6, 8]

In [12]:
dicionario = { "Ana":12, "Joao":13, "Jose":17 }  # declaração inicial do dicionário, pode ser {} para dic. vazio
print dicionario["Ana"]  # acesso ao elemento pela chave entre colchetes

dicionario["Maria"] = 11  # podemos alterar ou inserir um novo elemento

print dicionario

print "As chaves do dicionário são: ", dicionario.keys()
print "Os valores do dicionário são: ", dicionario.values()


12
{'Jose': 17, 'Maria': 11, 'Ana': 12, 'Joao': 13}
As chaves do dicionário são:  ['Jose', 'Maria', 'Ana', 'Joao']
Os valores do dicionário são:  [17, 11, 12, 13]

(1e) Iteradores

Para iterar por uma lista, tupla ou dicionário podemos utilizar a palavra-chave in. Em conjunto com a instrução for ela percorre cada elemento iterativamente. Essa palavra-chave também pode ser utilizada para verificar se um elemento está contido na lista.


In [14]:
lista = range(10)  # gera a lista [0,..,9]
print 8 in lista, 12 in lista

for x in lista:
    print x


True False
0
1
2
3
4
5
6
7
8
9

(1f) Geradores e List Comprehension

O Python permite uma sintaxe mais enxuta (e otimizada) para gerar uma nova lista de acordo com alguma regra específica:

[ funcao(x) for x in listaGeradora ]


In [2]:
# Jeito tradicional, mas não otimizado
listaOriginal = [1,2,3,4,5,6,7,8,9]
listaQuadrada = []
for x in listaOriginal:
    listaQuadrada.append(x*x)

print listaQuadrada

# Através do List Comprehension
listaQuadrada = [ x*x for x in listaOriginal ]
print listaQuadrada


[1, 4, 9, 16, 25, 36, 49, 64, 81]
[1, 4, 9, 16, 25, 36, 49, 64, 81]

Quando precisamos trabalhar com listas muito grandes, mas sem a necessidade de acessar os elementos aleatóriamente, podemos utilizar os geradores.

Um gerador define a instrução para gerar uma sequência, calculando cada elemento da sequência conforme requisitado.


In [5]:
listaQuadrada = ( x*x for x in listaOriginal )
print listaQuadrada # os elementos ainda não foram calculados

for x in listaQuadrada:
    print x # a cada iteração apenas o próximo elemento é calculado, a lista não existe na memória


<generator object <genexpr> at 0x1101c8320>
1
4
9
16
25
36
49
64
81

(1g) Arquivos

A leitura de arquivos é feita pelo comando open() que gera um apontador para arquivo, podemos utilizar o laço for para ler cada linha do arquivo iterativamente como uma string.


In [7]:
import os.path

caminho = os.path.join('Data','Aula01')  # garante o uso correto de / ou \\ para diretórios
arquivo = os.path.join(caminho,'exemplo.txt')

f = open(arquivo)
for linha in f:
    print linha
f.close()


Linha 1

Linha 2

Linha 3

FIM

Parte 2: Numpy

NumPy é uma biblioteca do Python para trabalhar com arrays. Essa biblioteca provê abstrações para utilizar arrays como vetores e matrizes. Ela é otimizada para ser rápida e eficiente em relação ao uso de memória. O tipo básico do NumPy é o ndarray, que é uma array multidimensional de tamanho fixo que contém elementos de um tipo apenas.

(2a) Multiplicação por Escalar

Para esse exercício, crie uma ndarray contendo os elementos [1, 2, 3] e multiplique essa array por 5. Use o comando np.array() para criar a array. Note que um dos possíveis parâmetros para essa função é ums lista do Python. A multiplicação escalar pode ser feita utilizando o operador *.

Note que se você criar uma array partindo de uma lista do Python, você obterá uma array unidimensional, que é equivalente a um vetor.


In [9]:
# Como convenção importaremos a biblioteca numpy como np
import numpy as np

In [10]:
# EXERCICIO
# Crie uma array numpy com os valores 1, 2, 3
arraySimples = [1,2,3]
# Faça o produto escalar multiplicando a array por 5
vezesCinco = np.multiply(arraySimples, 5)
print arraySimples
print vezesCinco


[1, 2, 3]
[ 5 10 15]

In [12]:
# TESTE do exercício (2a)
assert np.all(vezesCinco == [5, 10, 15]), 'valor incorreto para vezesCinco'
print "Correto!"


Correto!

(2b) Multiplicação elemento-a-elemento e produto interno

A multiplicação elemento-a-elemento é calculada como: $$ \mathbf{x} \odot \mathbf{y} = \begin{bmatrix} x_1 y_1 \\\ x_2 y_2 \\\ \vdots \\\ x_n y_n \end{bmatrix} $$

E o do produto interno de dois vetores de mesmo tamanho $ n $: $$ \mathbf{w} \cdot \mathbf{x} = \sum_{i=1}^n w_i x_i $$

Em alguns livros você também vê $ \mathbf{w} \cdot \mathbf{x} $ escrito como $ \mathbf{w}^\top \mathbf{x} $

O tipo Numpy Array suporta essas duas operações, ao utilizar o operador * para multiplicar dois vetores ou matrizes, ele executará a multiplicação elemento-a-elemento. Para realizar o produto interno você pode utilizar tanto a função np.dot() ou ndarray.dot(). Ex.: dados os vetores $x$ e $y$ pode realizar a operação como np.dot(x,y) ou x.dot(y).


In [15]:
# EXERCICIO
# A função np.arange(inicio,fim,passo)  cria uma lista iniciando em inicio, terminando antes do fim seguindo passo
u = np.arange(0, 5, .5)   # np.array([0,0.5,1.0,...,4.5])
v = np.arange(5, 10, .5)

elementoAelemento = u * v
prodInterno = np.dot(u, v)
print 'u: {0}'.format(u)
print 'v: {0}'.format(v)
print '\nelementoAelemento\n{0}'.format(elementoAelemento)
print '\nprodInterno\n{0}'.format(prodInterno)


u: [ 0.   0.5  1.   1.5  2.   2.5  3.   3.5  4.   4.5]
v: [ 5.   5.5  6.   6.5  7.   7.5  8.   8.5  9.   9.5]

elementoAelemento
[  0.     2.75   6.     9.75  14.    18.75  24.    29.75  36.    42.75]

prodInterno
183.75

In [16]:
# TESTE do exercício (2b)
assert np.all(elementoAelemento == [ 0., 2.75, 6., 9.75, 14., 18.75, 24., 29.75, 36., 42.75]), "Valores incorretos para elementoAelemento"
print "Primeiro teste OK"
assert prodInterno==183.75, "Valor incorreto para prodInterno"
print "Segundo teste OK"


Primeiro teste OK
Segundo teste OK

(2c) Multiplicação de Matriz

A multiplicação de matriz é definida por:

$$ [\mathbf{X} \mathbf{Y}]_{i,j} = \sum_{r=1}^n \mathbf{X}_{i,r} \mathbf{Y}_{r,j} $$

Note que o número de colunas da primeira matriz deve ser o mesmo do número de linhas da segunda matriz, representada por $ n $

No Numpy utilizamo np.matrix() quando queremos criar uma matriz a partir de listas do Python. Com esse tipo podemos utilizar o operador * para multiplicação de matrizes, np.multiply() para multiplicação elemento-a-elemento, np.matrix.transpose() ou .T para calcular a transposta e np.linalg.inv() para calcular a inversa de uma matriz quadrada.


In [43]:
# EXERCICIO
from numpy.linalg import pinv  # agora podemos utilizar o comando inv() sem preceder com np.linalg

# Criar uma matriz com listas de listas
A = np.matrix([[1,2,3,4],[5,6,7,8]])
print 'A:\n{0}'.format(A)

# Imprima a matriz transposta
print '\nA transposta:\n{0}'.format(np.matrix.transpose(A))

# Multiplique A por sua Transposta
AAt = np.dot(A, np.matrix.transpose(A))
print '\nAAt:\n{0}'.format(AAt)

# Inverta AAt com o comando inv()
AAtInv = pinv(AAt)
print '\nAAtInv:\n{0}'.format(AAtInv)

# Mostre que a matriz vezes sua inversa é a identidade
# .round(n) arredonda os valores para n casas decimais
print '\nAAtInv * AAt:\n{0}'.format((np.multiply(AAt,AAtInv)).round(4))


A:
[[1 2 3 4]
 [5 6 7 8]]

A transposta:
[[1 5]
 [2 6]
 [3 7]
 [4 8]]

AAt:
[[ 30  70]
 [ 70 174]]

AAtInv:
[[ 0.54375 -0.21875]
 [-0.21875  0.09375]]

AAtInv * AAt:
[[ 16.3125 -15.3125]
 [-15.3125  16.3125]]

In [44]:
# TESTE do exercício (2c)
assert np.all(AAt == np.matrix([[30, 70], [70, 174]])), "Valores incorretos para AAt"
print "Primeiro teste OK"
assert np.allclose(AAtInv, np.matrix([[0.54375, -0.21875], [-0.21875, 0.09375]])), "Valor incorreto para AAtInv"
print "Segundo teste OK"


Primeiro teste OK
Segundo teste OK

(2d) Slices

Nos vetores e matrizes do Numpy podemos selecionar sub-conjuntos de valores durante a indexação. Ex.:

v[:10] seleciona os 10 primeiros elementos

v[2:] seleciona os elementos da terceira posição em diante

v[-5:] retorna os 5 últimos elementos

v[:-5] retorna os elementos do começo até o ultimo-5

v[1:3] retorna os elementos 1 e 2


In [45]:
# EXERCICIO
atributos = np.array([1, 2, 3, 4])
print 'atributos:\n{0}'.format(atributos)

# Crie uma array com os 3 últimos elementos de atributos
ultTres = atributos[-3:]

print '\nÚltimos três:\n{0}'.format(ultTres)


atributos:
[1 2 3 4]

Últimos três:
[2 3 4]

In [46]:
# TEST do exercício (2d)
assert np.all(ultTres == [2, 3, 4]), "Valores incorretos para ultTres"
print "Teste OK"


Teste OK

Parte 3: Programação Funcional

(3a) Funções Anônimas (Lambda)

Uma função/expressão lambda é utilizada para definir funções simples com apenas uma instrução. Para isso basta usar a instrução lambda seguido da lista de parâmetros de entrada, precedidos por : e a expressão a ser executada. Por exempo, lambda x, y: x + y é uma função anônima que calcula a soma de dois valores.

Expressões lambda geram uma função quando interpretadas pelo Python. Elas são úteis quando precisamos aplicar uma função simples em diversos elementos de uma lista.

Para saber mais sobre Lambdas: Lambda Functions, Lambda Tutorial, and Python Functions.

No exercício abaixo crie uma função lambda que multiplique um valor por 10, atribua a variável designada.


In [5]:
# EXERCICIO
# Lembre-se que: "lambda x, y: x + y" cria uma função que adiciona dois valores
mult10 = lambda x : x*10
print mult10(5)

# Note that the function still shows its name as <lambda>
print '\n', mult10


50

<function <lambda> at 0x7f575016f6e0>

In [6]:
assert mult10(10)==100, "Função incorreta"
print "Teste OK"


Teste OK

As funções lambdas tem restrições em relação a expressão computada. Essa expressão não pode conter print ou incremento +=, por exemplo.

Além disso, os parâmetros de entrada podem ser de qualquer tipo, incluindo tuplas e listas.


In [8]:
import numpy as np
p1 = (1,3)
p2 = (3,7)

euclidiana2D = lambda (x0,y0), (x1,y1): np.sqrt(((x0-x1)**2) + ((y0-y1)**2))  # sqrt é a raíz quadrada
print euclidiana2D(p1,p2)


4.472135955

(3b) Lógica Funcional

No paradigma funcional trabalhamos com os conceitos de dados imutáveis, ou seja, não existe o conceito de variáveis, uma vez que um valor é designado a um nome, esse valor não pode mudar.


In [9]:
# Lógica não-funcional
a = 0
def inc():
    global a
    a = a + 1

# Lógica funcional
def incFn(a):
    return a+1

(3c) Funções de Alta Ordem

Desse modo, o uso de laços (for, while) é desencorajado e substituídos pela recursividade e funções de alta ordem. Uma função de alta ordem é uma função que recebe uma ou mais funções como parâmetro e retorna uma função.


In [12]:
# Função para somar 3 valores
def Soma3(a,b,c):
    return a+b+c

# Função que soma apenas dois valores
def Soma2(a,b):
    return a+b

# Soma 3 poderia ser criado a partir de Soma2:
Soma3Fn = lambda a,b,c: Soma2(Soma2(a,b),c)


<function <lambda> at 0x7f5732d475f0>

Esse tipo de função ajuda a criar um código declarativo, em que o próprio código auto-explica o que está sendo feito

Um exemplo interessante é a construção de uma função que retorna outra função.


In [14]:
# Cria uma função que calcula a eq. do segundo grau no formato ax^2 + bx + c
def Eq2grau(a,b,c):
    def f(x):
        return a*x**2 + b*x + c
    return f

f = Eq2grau(10,2,1)
print f(10)


1021

In [15]:
# EXERCICIO

# Escreva uma função Soma(x) que retorna uma função que recebe um valor y e soma ao x.
def Soma(x):
    def f(y):
        return x+y
    return f

Soma2 = lambda a,b: Soma(a)(b)
Soma3 = lambda a,b,c: Soma(Soma(a)(b))(c)

print Soma2(1,3), Soma3(1,2,3)


4 6

In [16]:
assert Soma3(1,2,3)==6, "Erro na função"
print "Ok"


Ok

(3d) Map, Reduce, Filter

Essas três funções são utilizadas para transformação de listas no paradigma funcional. Essas funções recebem como parâmetro uma função f e uma lista l.

Map: aplica a função em cada elemento da lista, gerando uma nova lista

Reduce: aplica a função cumulativamente em pares de elementos da lista, retornando um único valor agregado ao final

Filter: gera uma nova lista contendo os elementos da lista l em que a aplicação de f returna True

Para os próximos exercícios vamos utilizar a classe FuncionalW para criar uma sintaxe parecida com a que utilizaremos com o Spark.


In [17]:
class FuncionalW(object):
    def __init__(self, data):
        self.data = data
    def map(self, function):
        """Call `map` on the items in `data` using the provided `function`"""
        return FuncionalW(map(function, self.data))
    def reduce(self, function):
        """Call `reduce` on the items in `data` using the provided `function`"""
        return reduce(function, self.data)
    def filter(self, function):
        """Call `filter` on the items in `data` using the provided `function`"""
        return FuncionalW(filter(function, self.data))
    def __eq__(self, other):
        return (isinstance(other, self.__class__)
            and self.__dict__ == other.__dict__)
    def __getattr__(self, name):  return getattr(self.data, name)
    def __getitem__(self, k):  return self.data.__getitem__(k)
    def __repr__(self):  return 'FuncionalW({0})'.format(repr(self.data))
    def __str__(self):  return 'FuncionalW({0})'.format(str(self.data))

In [22]:
# Exemplo de Map

# Criaremos uma lista
lista = FuncionalW(range(10))

# Criar uma função a ser aplicada nessa lista
f = lambda x: x*x

# Programação Imperativa
resultado1 = FuncionalW([])
for x in lista:
    resultado1.append(f(x))
print "Resultado: {}".format(resultado1)    

# Funcional
print "Resultado usando Map: {}".format(lista.map(f))


Resultado: FuncionalW([0, 1, 4, 9, 16, 25, 36, 49, 64, 81])
Resultado usando Map: FuncionalW([0, 1, 4, 9, 16, 25, 36, 49, 64, 81])

In [23]:
# Exemplo de Reduce

# Criaremos uma lista
lista = FuncionalW(range(1,10))

# Criar uma função a ser aplicada nessa lista
f = lambda x,y: x*y

# Programação Imperativa
produtoria = 1
for x in lista:
    produtoria = f(produtoria,x)
print "Resultado: {}".format(produtoria)    

# Funcional
print "Resultado usando Reduce: {}".format(lista.reduce(f))


Resultado: 362880
Resultado usando Reduce: 362880

In [34]:
# EXERCICIO

dataset = FuncionalW(range(10))

# Multiplique cada elemento por 5
mapResult = dataset.map(lambda x : x*5)
# Filtre eliminando os elementos ímpares
# No Python "x % 2" é o resultado do resto da divisão de x por 2
filterResult = dataset.filter(lambda x: x % 2 == 0)
# Some os elementos
reduceResult = dataset.reduce(lambda x, y : x+y)

print 'mapResult: {0}'.format(mapResult)
print '\nfilterResult: {0}'.format(filterResult)
print '\nreduceResult: {0}'.format(reduceResult)


mapResult: FuncionalW([0, 5, 10, 15, 20, 25, 30, 35, 40, 45])

filterResult: FuncionalW([0, 2, 4, 6, 8])

reduceResult: 45

In [35]:
assert mapResult == FuncionalW([0, 5, 10, 15, 20, 25, 30, 35, 40, 45]),"Valor incorreto para mapResult"
print "Teste 1 OK"

assert filterResult == FuncionalW([0, 2, 4, 6, 8]), "Valor incorreto para filterResult"
print "Teste 2 OK"

assert reduceResult == 45, "Valor incorreto para reduceResult"
print "Teste 3 OK"


Teste 1 OK
Teste 2 OK
Teste 3 OK

Para reduzir o tamanho do código e facilitar a leitura, podemos compor as funções em sequência


In [37]:
dataset = FuncionalW(range(10))

Soma = (dataset
            .map(lambda x: x*5)
            .filter(lambda x: x%2==0)
            .reduce(lambda x,y: x+y)
            )
print Soma


100

In [43]:
# EXERCICIO

# split() divide a string em palavras
Texto = FuncionalW("Esse texto tem varias palavras cada linha tem palavras escritas Esse texto esta escrito".split())

# Vamos fazer uma contagem da palavra 'palavras' no texto

# Crie uma função lambda que recebe duas entradas e retorna se são iguais ou não
Igual = lambda x,y: x == y

# Crie uma função lambda que utiliza a função Igual para detectar se a entrada é igual a palavra 'palavras'
DetectaPalavra = lambda x: Igual(x,"palavras")

# 1) Filtre as palavras iguais a 'palavras'
# 2) Mapeie todos os elementos para o valor 1
# 3) Reduza para a somatória
contagem = (Texto
            .filter(DetectaPalavra)
            .map(lambda x: 1)
            .reduce(lambda x,y : x + y)
            )

print "Existem {} ocorrências de 'palavras'".format(contagem)


Existem 2 ocorrências de 'palavras'

In [ ]:


In [ ]: