quarta-feira, 30 de dezembro de 2009

Duplos... só no cinema!

Consideremos o problema de remover os elementos duplicados de uma sequência. Uma solução trivial será:
1.def remove_duplos_1(seq):
2.    """Retira elementos duplicados da sequência."""
3.    aux = []
4.    for i,elem in enumerate(seq):
5.        if elem not in seq[i+1:]:
6.            aux.append(elem)
7.    return aux

ou ainda:
1.def remove_duplos_2(seq):
2.    """Retira duplicados da sequência."""
3.    return [elem for i,elem in enumerate(seq) if elem not in seq[i+1:]]

Nestes dois casos a ideia é óbvia: pegar num elemento e ver se há mais à sua frente.

Tal como estão, estas soluções funcionam bem para listas. E se for outro tipo de sequências? Cadeias de caracteres, por exemplo:
1.def remove_duplos_3(seq):
2.    """Retira elementos duplicados da sequência."""
3.    aux = ''
4.    for i,elem in enumerate(seq):
5.        if elem not in seq[i+1:]:
6.            aux = aux + elem
7.    return aux

Mas se eu souber que existe um tipo de dados set, que implementa conjuntos em Python posso fazer melhor. Por exemplo, no caso das listas:
1.def remove_duplos_4(seq):
2.    return list(set(seq))


Os conjuntos são colecções não ordenadas, mutáveis e heterogéneas de (referências para) objectos. Existem vários métodos associados com os objectos do tipo set, por exemplo union, intesection e difference. Para saber mais é só consultar o manual de Python.

Claro que agora é fácil encontrar uma solução que dê para diferentes tipos de objectos:
01.def remove_duplos_5(seq):
02.    aux = set(seq)
03.    if isinstance(seq,str):
04.        return ''.join(aux)
05.    elif isinstance(seq,list):
06.        return list(aux)
07.    elif isinstance(seq,tuple):
08.        return tuple(aux)
09.    else:
10.        print 'TypeError'
11.        return seq

E pronto. Acabaram-se os duplos!

O saber remove montanhas

Consideremos o problema simples de retirar todas as ocorrências de um item de uma sequência.

Podemos começar com uma solução simples:
1.def remove_1(item,seq):
2.    """Retira todas as ocorrências do item da sequência."""
3.    aux = []
4.    for elem in seq:
5.        if elem != item:
6.            aux.append(elem)
7.    return aux

Mas que só funciona para listas e tuplos. E, neste último caso, transforma o tuplo numa lista. Claro que se apenas tivermos que manipular listas ainda podemos optar pela solução que usa listas por compreensão:
1.def remove_2(item,seq):
2.    """Seq é uma lista."""
3.    return [elem for elem in seq if elem != item]

Então e se for um cadeia de caracteres? Podemos usar a função pré-definida replace:
1.def remove_3(item, seq):
2.    """se for uma cadeia de caracteres."""
3.    return seq.replace(item,'')

Mas podemos juntar tudo num único programa? Claro que sim! Só precisamos de fazer um teste ao tipo do objecto, e para isso usamos o comando isinstance.
01.def remove_4(item, seq):
02.    """Lista ou tuplo ou cadeia de caracteres."""
03.    if isinstance(seq,str):
04.        return seq.replace(item,'')
05.    elif isinstance(seq,list):
06.        return [elem for elem in seq if elem != item]
07.    elif isinstance(seq,tuple):
08.        return tuple([elem for elem in seq if elem != item])
09.    else:
10.        print 'TypeError'
11.        return seq

E pronto, já está!

terça-feira, 29 de dezembro de 2009

Errar é humano...

Um aluno de IPRP estava com dificuldades num problema que saiu no exame de 2006 e pediu-me ajuda. O problema era o seguinte:

Suponha que tem uma cadeia de caracteres com um dado comprimento e a pretende dividir em n partes iguais. Implemente em Python o respectivo programa não se esquecendo de prever o caso de o comprimento da cadeia não ser múltiplo de n.

Enviou-me a sua tentativa de solução que não funcionava:
01.def divide(f, p):
02.    aux=[]
03.    a=len(f)
04.    lpp=(len(f)-1)/p
05.    dif=0
06.    x=lpp
07.    faux=''
08.    n=0
09.    while(n < p):
10.        faux=f[dif:lpp+1]
11.        aux.append(faux)
12.        dif=(dif+lpp)+1
13.        lpp=(lpp+x)       
14.        n=n+1
15.    return aux

Qual é a lógica desta solução? Bem andar a partir a frase em pedaços todos do mesmo comprimento. Calcula-se esse comprimento na linha 4. Usam-se duas marcas (dif e lpp) para determinar os limites de cada pedaço, que vão sendo guardados em aux, a capa passagem pelo ciclo while. A ideia faz sentido.


Vamos ver alguns dos seus problemas. Começo pelo código inútil:

a = len(f), não está lá a fazer nada, nunca sendo usado.
faux = '' : esta inicialização não é preciso para nada

Agora os erros:

lpp = (len(f) - 1) / p: não se deve retirar um ao comprimento, dado o modo como a divisão funciona.
faux[f[dif:lpp+1]: errado devido à soma de 1.
dif = dif + lpp +1: aqui talvez o erro maior! Em cada etapa dif deve avançar para o valor de lpp.

Posto tudo isto uma solução usando esta ideia seria (notem que uso x no lugar de lpp, por uma questão de clareza):
01.def divide_corrigido(f, p):
02.   aux=[]
03.   lpp=len(f)/p
04.   dif=0
05.   x = lpp
06.   n=0
07.   while(n < p):
08.       faux=f[dif:x]
09.       aux.append(faux)
10.       dif=x
11.       x = x + lpp
12.       n=n+1
13.   aux.append(f[dif:])
14.   return aux

Mas não estou satisfeito, pois podemos encontrar uma solução mais elegante, tendo por base o mesmo princípio: duas marcas que vão avançando a cada passagem pelo ciclo. Mas para tal podemos usar uma combinação de um ciclo for com a operação range:
01.def divide_ec(frase,n):
02.   """divide a frase em n pedaços iguais."""
03.   # número de caracteres em cada parte
04.   num_car = len(frase)/n
05.   # calcula e guarda em aux
06.   aux = []
07.   for i in range(0,len(frase),num_car):
08.       aux.append(frase[i:i+num_car])
09.   return aux

O trabalho de fazer avançar a marca correctamente é assegurado pelo modo de funcionar do range comn três argumentos. Concordarão que este programa é mais claro do que o proposto! E prevê o caso de a frase ter um comprimento que não é múltiplo de n!

Se gostarem de listas por compreensão podem ainda prescindir de aux.:
1.def divide_ec_2(frase,n):
2.   num_car = len(frase)/n
3.   return [frase[i:i+num_car] for i in range(0,len(frase),num_car)]


Mas voltemos ao enunciado, que nos fala em n partes iguais. Se corrermos o programa, verificamos que nos casos em que não é múltiplo existem n+1 partes! Como corrigir este aspecto? Uma solução será esticar a cadeia e passar a ter cadeias todas do mesmo tamanho, menos a última. Esta era, aliás, a interpretação correcta do enunciado...

1.def divide_ec_3(frase,n):
2. """divide uma sequência frase em n partes.
3. A última pode ser menor."""
4. parte=(len(frase) + n - 1)/n
5. aux = []
6. for i in range(0,n):
7.  aux.append(frase[parte*i:parte*(i+1)])
8. return aux


E até nos podemos dar ao luxo de ter uma solução recursiva!!
1.def divide_ec_4(frase,n):
2. if n == 0:
3.  return []
4. else:
5.  return [frase[:len(frase)/n]] + divide_ec_4(frase[len(frase)/n:],n-1)


E assim terminamos este exercício. aprendemos muito com os nossos erros ... e com os erros dos outros!!

quinta-feira, 24 de dezembro de 2009

Mini Teste # 3

Apresentamos de seguida um esboço da solução do Mini Teste #3.

Boas Festas!!

3.1

Parâmetros formais são os nomes dados aos argumentos das definições. Por exemplo, em:

1.def teste(x,y):
2.   return x + y

x e y são os parâmetros formais. Durante a chamada (uso) de uma definição esses nomes recebem a identidade dos objectos associados aos parâmetros reais.

3.2

O módulo random foi importado de dois modos distintos. No primeiro caso, o acesso ao conteúdo do módulo, por exemplo para usar um dos seus métodos, obriga a usar o nome do módulo como prefixo, como se pode ver na linha 2. No segundo caso, é feita apenas uma imprtação selectiva, sendo apenas importados alguns elementos do módulo, no exemplo da linha 4 apenas se importa o método choice. Nesta situação usamos directamente o nome do método sem o prefixar com o nome do módulo. Nas linhas 7 a 11 a diferença entre os dois métodos aparece na forma de erro pois tentámos usar um método, randint, que não tinha sido importado directamente, sem o prefixar com o nome do módulo.

3.3

O programa cria uma matriz identidade, isto é uma matriz em que todos os elementos são zero, menos os elementos na diagonal principal que são um.

3.4

1.def posicoes(texto):
2.    vogais ='aeiouAEIOU'
3.    dic_pos = {}
4.    for i,letra in enumerate(texto):
5.        if letra in vogais:
6.            dic_pos[letra] = dic_pos.get(letra,[]) + [i]
7.    return dic_pos

3.5

01.def cota(fich):
02.    f_in = open(fich,'r')
03.    # ler cabeçalho
04.    linha = f_in.readline()
05.    dados = []
06.    # percorre ficheiro linha a linha
07.    linha = f_in.readline()
08.    while linha != '': # EOF?
09.        linha = linha[:-1].split(',')
10.        dados.append(float(linha[4]) - float(linha[1]))
11.        linha = f_in.readline()
12.    dados.sort()
13.    print 'Melhor: ', dados[-1]
14.    print 'Pior: ', dados[0]
15.    f_in.close()

domingo, 20 de dezembro de 2009

Problema 10.6

O ordenamento por selecção tem uma lógica muito simples: admitindo que uma parte do vector já está definitivamente ordenada concentra-mo-nos na outra parte. Escolhemos o maior elemento (ou o menor se for por ordem decrescente) da parte ainda desordenada e coloca-mo-lo na sua posição definitiva. este processo é repetido tantas vezes quantas a dimensão do vector menos um.
1.def selec_dir(seq):
2. """Ordenamento por selecção directa"""
3. copia=seq[:]
4. for cont in range(len(seq)-1,0,-1):
5.  indice=copia.index(max(copia[:cont+1]))
6.  copia[cont],copia[indice]=copia[indice],copia[cont]
7. return copia

Nesta implementação o ordenamento é por ordem crescente. Veja-me como usamos a operação de range!

Problema 10.5

Porque analisamos o vector em toda a sua dimensão se a partir de um certo índice não houve mais trocas? Não precisamos, pois não haver trocas significa: vector ordenado daí para a frente! O algoritmo apresetnado resolve essa questão.
01.def bubblesort_3(seq):
02. """Ordenamento por bolhas.
03. Apenas ordena o subvector esquerdo onde houve trocas!
04. """
05. copia = seq[:]
06. indice= len(seq)-1
07. while indice > 0:
08.  cont = indice - 1
09.  indice = 0
10.  for i in range(cont+1):
11.   if copia[i] > copia[i+1]:
12.    copia[i],copia[i+1] = copia[i+1],copia[i]
13.    indice=i
14. return copia

Problema 10.4

Vamos melhorar o algoritmo de ordenamento por bolhas (bubble sort) evitandio que ele continue a efectuar comparações quando o vector já está ordenado. Como sabemos que o vector está ordenado? Simples: quando percorremos o vector sem haver trocas entre elementos!
01.def bubblesort_2(seq):
02. """Ordenamento por bolhas.
03. Pára mal fica ordenado = não há mais trocas!
04. """
05. copia = seq[:]
06. cont= len(seq)-1
07. troca = True
08. while troca:
09.  cont = cont -1
10.  troca=False
11.  for i in range(cont+1):
12.   if copia[i] > copia[i+1]:
13.    copia[i],copia[i+1] = copia[i+1],copia[i]
14.    troca=True
15. return copia

Então o que fizemos? Passamos a ter um ciclo condicional (while) controlado por uma variávewl booleana, que indica se houve ou não trocas na etapa anterior. Como temos que percorrer pelo menos uma vez o vector, inicializamos a variável booleana de controlo a True. No interior do ciclo começamos por assumir que não houve trocas. Percorremos o vector. Caso não haja nenhuma troca, ele mantém-se com o valor False e saímos do ciclo. Caso contrário, passa a verdadeira e retomamos o ciclo no seu início.

Problema 10.2

A dificuldade desta questão residia apenas na compreensão de que é possível passar uma função como argumento. Em Python, tudo são objectos e, por isso, as funções também são objectos, caracterizadas por identidade, valor e tipo. Por outro lado, o que se comunica a uma definição aquando da sua chamada ou é um objecto ou uma referência para um objecto. Na listagem abaixo ilustramos alguns destes aspectos.

01.>>> def maior(x,y):
02.... return  x > y
03.>>> maior(5,6)
04.False
05.>>> maior(6,5)
06.True
07.>>> type(maior)
08.<type 'function'="">
09.>>> id(maior)
10.13432752
11.>>> maior
12.<function maior at 0xccf7b0>
13.>>> menor = maior
14.>>> menor(6,5)
15.True
16.>>>
17.</type>

Regressando ao nosso problema. O desafio é evitar duplicar o código, transmitindo na chamada a referência para a operação de comparação.

01.def insertion_both(seq, metodo):
02. """ Como um jogador de cartas"""
03. copia=seq[:]
04. for cont in range(1,len(seq)):
05.  elem=copia[cont]
06.  indice=cont - 1
07.  while (metodo(elem,copia[indice])) and (indice >= 0):
08.   copia[indice + 1] = copia[indice]
09.   indice = indice - 1
10.  copia[indice + 1] = elem
11. return copia
12. 
13.def maior(x,y):
14. return (x > y)
15. 
16.def menor(x,y):
17. return (x < y)

E assim se resolve de forma simples e elegante, um problema aparentemente difícil! Deixo ao leitor o cuidado de perceber porque é que foi preciso criar as funções maior e menor que apenas fazem o que > e < fazem!

segunda-feira, 14 de dezembro de 2009

Problema 9.10

A questão dos agrupamentos vazios obriga a algum trabalho de alteração do código. Mas o que importa mais é definir a estratégia para lidar com esses agrupamentos. Uma solução simples seria deixar que existam.Mas não é isso que queremos! Então o que fazer? Bom, uma solução razoável será escolher o pior elemento de todo o conjunto de pontos. E qual é o pior? Começamos por calcular o pior dentro de cada agrupamento: o ponto mais distante ao respectivo centróide. De entre estes pontos vamos optar, uma vez mais, pelo mais distante. Esse elemento é retirado do seu agrupamento e é colocado no agrupamento vazio, passando a ser o seu centróide.

No código abaixo é fácil identificar as novas operações e a parte do código que foi alterada.
001.import math
002.import random
003.import cTurtle
004.import operator
005. 
006.def dist_euclidiana(p1,p2):
007.    return math.sqrt(sum([(p[1] - p[0])**2 for p in zip(p1,p2)]))
008. 
009.def centroide(pontos):
010.    """
011.    Calcula o centróide de um conjunto de pontos n-dimensionais.
012.    Entrada = uma lista de pontos.
013.    Assume a existência de pelo menos um ponto!!
014.    """
015.    cent = [sum(ponto)/len(ponto) for ponto in zip(*pontos)]
016.    return tuple(cent)
017. 
018.def cria_dicio_dados(ficheiro):
019.    """
020.    Lê as coordenadas dos pontos a partir de um ficheiro.
021.    Incorpora num dicionário.
022.    """
023.    f_in = open(ficheiro,'r')
024.    dicio_dados = dict()
025.     
026.    chave = 0
027.    linha = f_in.readline()
028.    while linha != '': # EOF?
029.        chave = chave + 1
030.        tuplo = tuple([int(valor) for valor in linha[:-1].split()])
031.        dicio_dados[chave] = tuplo
032.        linha = f_in.readline()
033.    return dicio_dados
034. 
035.# por localização
036.def cria_dicio_dados_terramoto(ficheiro):
037.    """
038.    Extrai os dados do ficheiro e constrói o dicionário.
039.    """
040.    f_in = open(ficheiro,'r')
041.    dicio_dados = dict()
042.     
043.    chave = 0
044.    linha = f_in.readline()
045.    while linha != '': # EOF?
046.        chave = chave + 1
047.        latitude = float(linha[:-1].split()[3])
048.        longitude = float(linha[:-1].split()[4])
049.        dicio_dados[chave] = (longitude,latitude)
050.        linha = f_in.readline()
051.    return dicio_dados
052. 
053.def cria_centroides(k,dicio_dados):
054.    """
055.    Devolve uma lista com k pontos distintos como centróides.
056.    Apenas com intensidade...
057.    """
058.    centroides = []
059.    conta_centroides = 0
060.    chaves_centroides = []
061.    while conta_centroides < k:
062.        chave_alea = random.randint(1,len(dicio_dados))
063.        if chave_alea not in chaves_centroides:
064.            centroides.append(dicio_dados[chave_alea])
065.            chaves_centroides.append(chave_alea)
066.            conta_centroides = conta_centroides + 1
067.    return centroides
068. 
069.#-- alterado por causa dos vazios
070.def cria_agrupamentos(k,centroides,dicio_dados,repete):
071.    """
072.    k = número de agrupamentos
073.    centroides = lista de centroides inicial
074.    dicio_dados = os dados organizados num dicionário {1:(...), 2: (...),...}
075.    repete = número de passagens
076.    Os dados podem ser n-dimensionais. Cada ponto é um tuplo.
077.    """
078.    for passo in range(repete):
079.        print "----- Passagem:\t {0}\t-----".format(passo)
080.        # cria contentor para os agrupamentos
081.        agrupamentos = []
082.        for conta in range(k):
083.            agrupamentos.append([])
084.             
085.        # Determina agrupamento para cada dado
086.        for chave in dicio_dados:
087.            distancias = []
088.            for indice_agrupamento in range(k):
089.                dist = dist_euclidiana(dicio_dados[chave],centroides[indice_agrupamento])
090.                distancias.append(dist)
091.            menor = min(distancias)
092.            indice = distancias.index(menor)
093.             
094.            agrupamentos[indice].append(chave)
095. 
096.        # Actualiza centróides
097.        for indice in range(k):
098.            agrupa = [dicio_dados[chave] for chave in agrupamentos[indice]]
099.            centroides[indice] = centroide(agrupa)
100.             
101.        # agrupamentos vazios: o elemento de um agrupamento não vazio de
102.        # maior distância é movido
103.        if agrupamento_vazio(agrupamentos):
104.            agrupamentos = consolida_agrupamentos(centroides,agrupamentos,dicio_dados)
105.         
106.        # mostra resultado
107.        mostra_agrupamentos(agrupamentos,dicio_dados)
108.         
109.    return agrupamentos
110. 
111.def mostra_agrupamentos(agrupamentos,dicio_dados):
112.    """
113.    Mostra os agrupamentos.
114.    """
115.    for indice,agrupamento in enumerate(agrupamentos):
116.        print "AGRUPAMENTO %d: " % (indice + 1)
117.        for chave in agrupamento:
118.            print dicio_dados[chave],
119.        print
120. 
121.def agrupamento_vazio(agrupa):
122.    """ Algum agrupamento vazio."""
123.    return any([len(elem) == 0 for elem in agrupa])
124. 
125.def indices_agrupa_vazios(agrupa):
126.    """ lista de índices dos agrupamentos vazios."""
127.    return [indice for indice in range(len(agrupa)) if len(agrupa[indice]) == 0]
128. 
129.def indice_max_dist(centroides, agrupamentos, dicio_dados):
130.    """ Determina o agrupamento e o índice do elemento mais distante do centroide
131.    considerando todos os agrupamentos.
132.    """
133.    max_dist = []
134.    for indice,agrupa in enumerate(agrupamentos):
135.        if len(agrupa) > 0:
136.            lista_dist = []
137.            for elemento in agrupa:
138.                lista_dist.append((elemento,dist_euclidiana(centroides[indice], dicio_dados[elemento])))
139.            lista_dist.sort(key=operator.itemgetter(1), reverse = True)
140.            maior = (indice,lista_dist[0][0])
141.            max_dist.append(maior)
142.    max_dist.sort(key=operator.itemgetter(1),reverse=True)
143.    return max_dist[0] # -- (indice agrupamento, indice elemento)
144.             
145.         
146. 
147.def consolida_agrupamentos(centroides, agrupamentos,dicio_dados):
148.    """ Devolve agrupamentos sem elementos vazios."""
149.    indice_vazios = indices_agrupa_vazios(agrupamentos)
150. 
151.    while indice_vazios:
152. 
153.        # qual o elemento à distância máxima do seu centróide?
154.        # qual o máximo dos máximos?
155.        maximo = indice_max_dist(centroides,agrupamentos,dicio_dados)
156.        # coloca o máximo dos máximos num agrupamento vazio
157.        agrupamentos[indice_vazios[0]].append(maximo[1])
158.        # retira do agrupamento origem
159.        agrupamentos[maximo[0]].remove(maximo[1])
160.        indice_vazios = indice_vazios[1:]
161.    return agrupamentos
162.     
163. 
164.def main(ficheiro,numero_grupos):
165.    """
166.    Faz a análise de agrupamentos segundo K-médias.
167.    """
168.    dicio_dados = cria_dicio_dados(ficheiro)
169.    centroides = cria_centroides(numero_grupos, dicio_dados)
170.    agrupamentos = cria_agrupamentos(numero_grupos,centroides, dicio_dados,3)

Problema 9.8

Este problema apenas acrescenta a necessidade de visualizar os dados. Para isso vamos usar o módulo cTurtle. O código é o seguinte:
01.def visualiza_terramotos(ficheiro, num_agrupa):
02.    dicio_dados = cria_dicio_dados_terramoto(ficheiro)
03.    centroides_terramoto = cria_centroides(num_agrupa,dicio_dados)
04.    agrupamentos = cria_agrupamentos(num_agrupa,centroides_terramoto,dicio_dados,10)
05.     
06.    # prepara o cenário ...
07.    tartaruga = cTurtle.Turtle()
08.    tartaruga.bgpic('/tempo/imagens/worldmap.gif')
09.    tartaruga.screensize(448,266)
10.    tartaruga.setWorldCoordinates(-180,-90, 180,90)
11.    tartaruga.hideturtle()
12.    tartaruga.up()
13.     
14.    # cores aleatorias
15.    tartaruga.colormode(255)
16.    lista_cores = []
17.    while len(lista_cores) < num_agrupa:
18.        r = random.randint(0,255)
19.        g = random.randint(0,255)
20.        b = random.randint(0,255)
21.        cor = (r,g,b)
22.        minimo = dist_euclid_lista(cor, lista_cores)
23.        if (cor not in lista_cores) and (minimo > 255):
24.            lista_cores.append(cor)
25.  
26.    # mostra os dados
27.    for indice_agrupa in range(num_agrupa):
28.        tartaruga.color(lista_cores[indice_agrupa])
29.        for chave in agrupamentos[indice_agrupa]:
30.            longitude = dicio_dados[chave][0]
31.            latitude = dicio_dados[chave][1]
32.            tartaruga.goto(longitude,latitude)
33.            tartaruga.dot()
34.    tartaruga.exitOnClick()

Começamos por obter os agrupamentos com os programas já desenvolvidos anteriormente (linhas 3 a 5 ). Criamos de seguida o objecto tartaruga (linha 8), colocamos em background a imagem (linha 9), e ajustamos o tamanho da janela ao tamanho real da imagem (linha 10). Redefinimos as coordenadas do mundo para ficarem iguais aos do problema (linha 11, latitude entre -90 e 90 e longitude entre -180 e 180), escondemos (linha 12) e levantamos a tartaruga (linha 13). Escolhemos aleatoriamente as cores dos pontos com que vamos marcar o mapa (linhas 15 a 25), mas garantido que são suficientemente distintas (linhas 23 a 25). Depois é só mostrar os pontos (linhas 27 a 34), e abandonar no final (linha 35)!
A figura abaixo mostra um exemplo resultado da execução do programa com os dados do tremor de terra e para 4 agrupamentos.


Problema 9.6

Para este problema só temos que definir qual é a informação relevante. Se queremos fazer o agrupamento por posição, temos que ir ao ficheiro de dados obter a latitude e a longitude da ocorrência do terramoto.
O ficheiro tem uma organização como se ilustra:


2.8 2006/10/19 02:02:10 62.391 -149.751 15.0 CENTRAL ALASKA

2.5 2006/10/19 00:31:15 20.119 -156.213 1.5 MAUI REGION, HAWAII

5.0 2006/10/18 21:15:51 4.823 -82.592 37.3 SOUTH OF PANAMA

2.6 2006/10/18 21:12:25 59.934 -147.904 30.0 GULF OF ALASKA

3.4 2006/10/18 20:59:21 36.540 -89.640 7.7 SOUTHEASTERN MISSOURI

2.7 2006/10/18 20:11:22 61.023 -151.418 60.0 SOUTHERN ALASKA


A latitude é o quarto elemento e a longitude o quinto (não se esqueça que as sequências em Python começam em zero). Daí o código:
01.def cria_dicio_dados_terramoto(ficheiro):
02.    """
03.    Extrai os dados do ficheiro e constrói o dicionário.
04.    """
05.    f_in = open(ficheiro,'r')
06.    dicio_dados = dict()
07.     
08.    chave = 0
09.    linha = f_in.readline()
10.    while linha != '': # EOF?
11.        chave = chave + 1
12.        latitude = float(linha[:-1].split()[3])
13.        longitude = float(linha[:-1].split()[4])
14.        dicio_dados[chave] = (longitude,latitude)
15.        linha = f_in.readline()
16.    f_in.close()
17.    return dicio_dados

É semelhante ao anterior dado para as notas, dispensado por isso mais comentários.

domingo, 13 de dezembro de 2009

k-Médias

O algoritmo k-médias é um algoritmo de agrupamento conceptual. De um modo simples, dado um conjunto de objectos, descritos por atributos, como os dividir em k grupos por forma que os objectos de um dado grupo são maximanente semelhantes entre si e minimamente semelhantes em relação aos objectos dos outros grupos.


Computacionalmente, o algoritmo K-Médias desdobra-se nos seguintes passos:


(1) Escolhe o número, K, de agrupamentos
(2) Escolhe, de modo aleatório, K pontos para servirem de centróides iniciais de cada um dos K agrupamentos
(3) Repetir os seguintes passos:

--> (3a) Afectar cada ponto ao agrupamento de cujo centróide se encontrar mais perto

-->(3b) Recalcular os centróides

(4) Mostrar os K agrupamentos


Posto isto, o código:

001.import math
002.import random
003.import cTurtle
004. 
005.def dist_euclidiana(p1,p2):
006.    """
007.    Distância euclidiana entre dois pontos. Os pontos são tuplos n-dimensionais.
008.    """
009.    soma =0
010.    for i in range(len(p1)):
011.        soma = soma + (p2[i] - p1[i])**2
012.    distancia = math.sqrt(soma)
013.    return distancia
014.     
015.def centroide(pontos):
016.    """
017.    Calcula o centróide de um conjunto de pontos n-dimensionais.
018.    Entrada = uma lista de pontos. Saida = tuplo
019.    Assume a existência de pelo menos um ponto!!
020.    """
021.    cent = [sum(ponto)/len(ponto) for ponto in zip(*pontos)]
022.    return tuple(cent)
023. 
024.def cria_dicio_dados(ficheiro):
025.    """
026.    Lê as coordenadas dos pontos a partir de um ficheiro.
027.    Incorpora num dicionário.
028.    """
029.    f_in = open(ficheiro,'r')
030.    dicio_dados = dict()
031.     
032.    chave = 0
033.    linha = f_in.readline()
034.    while linha != '': # EOF?
035.        chave = chave + 1
036.        tuplo = tuple([int(valor) for valor in linha[:-1].split()])
037.        dicio_dados[chave] = tuplo
038.        linha = f_in.readline()
039.    f_in.close()
040.    return dicio_dados
041. 
042.def cria_centroides(k,dicio_dados):
043.    """
044.    Devolve uma lista com k pontos distintos como centróides.
045.    """
046.    centroides = []
047.    chaves_centroides = []
048.    for i in range(k):
049.        chave_alea = random.randint(1,len(dicio_dados))
050.        while chave_alea  in chaves_centroides:
051.            chave_alea = random.randint(1,len(dicio_dados))
052.        centroides.append(dicio_dados[chave_alea])
053.        chaves_centroides.append(chave_alea)
054.    return centroides
055. 
056.def cria_agrupamentos(k,centroides,dicio_dados,repete):
057.    """
058.    k = número de agrupamentos
059.    centroides = lista de centroides inicial
060.    dicio_dados = os dados organizados num dicionário {1:(...), 2: (...),...}
061.    repete = número de passagens
062.    Os dados podem ser n-dimensionais. Cada ponto é um tuplo.
063.    """
064.    for passo in range(repete):
065.        print "----- Passagem:\t {0}\t-----".format(passo)
066.        # Cria contentor para os agrupamentos
067.        agrupamentos = []
068.        for conta in range(k):
069.            agrupamentos.append([])
070.             
071.        # Determina agrupamento para cada dado
072.        for chave in dicio_dados:
073.            distancias = []
074.            for indice_agrupamento in range(k):
075.                dist = dist_euclidiana(dicio_dados[chave],centroides[indice_agrupamento])
076.                distancias.append(dist)
077.            menor = min(distancias)
078.            indice = distancias.index(menor)
079.             
080.            agrupamentos[indice].append(chave)
081. 
082.        # Actualiza centróides
083.        for indice in range(k):
084.            agrupa = [dicio_dados[chave] for chave in agrupamentos[indice]]
085.            centroides[indice] = centroide(agrupa)
086.        # Mostra resultado
087.        mostra_agrupamentos(agrupamentos,dicio_dados)
088.         
089.    return agrupamentos
090. 
091.def mostra_agrupamentos(agrupamentos,dicio_dados):
092.    """
093.    Mostra os agrupamentos.
094.    """
095.    for indice,agrupamento in enumerate(agrupamentos):
096.        print "AGRUPAMENTO %d: " % (indice + 1)
097.        for chave in agrupamento:
098.            print dicio_dados[chave],
099.        print
100. 
101.def main(ficheiro,numero_grupos):
102.    """
103.    Faz a análise de agrupamentos segundo K-médias.
104.    """
105.    dicio_dados = cria_dicio_dados(ficheiro)
106.    centroides = cria_centroides(numero_grupos, dicio_dados)
107.    agrupamentos = cria_agrupamentos(numero_grupos,centroides, dicio_dados,3)
108. 
109. 
110.if __name__ =='__main__':
111.    main('/tempo/data/notasmt1.txt',5) # no seu caso o local do ficheiro pode ser diferente!!!


O leitor deve estar atento à representação dos objectos! Em particular:

(a) os agrupamentos são guardados numa lista, sendo que cada agrupamento é descrito pela lista das chaves dos objectos;

(b) os centróides são guardados numa lista e descritos pelos atributos dos objectos;

(c) os objectos estão guardados num dicionário, sendo a chaves inteiros e os valores os atributos do objecto.
(d) durante a computação é usada uma lista com as distâncias de um objecto a cada um dos centróides.

Problema 9.3

Este problema envolve a criação da lista de centróides a partir do número de agrupamentos pretendidos, e do dicionário de dados. Este dicionário está organizado de modo que cada chave é um inteiro e o valor o tuplo com as coordenadas do ponto. Os membros da lista devem ser escolhidos aleatoriamente e não pode haver repetições.
Uma solução simplista será:
01.import random
02. 
03.def cria_centroides(k,dicio_dados):
04.    “”” Devolve lista de k centroides escolhidos aleatoriamente.”””
05.   centroides = []
06.   chaves_centroides = []
07.   conta_centroides = 0
08.   while centroides < k:
09.      chave_alea = random.randint(1,len(dicio_dados))
10.      if chave_alea not in chaves_centroides:
11.         centroides.append(dicio_dados[chave_alea])
12.         chaves_centroides.append(chave_alea)
13.         conta_centroides = conta_centroides + 1
14.    return centroides

Existem variantes. Apresentamos uma, que se nos afigura mais elegante.
01.import random
02. 
03.def cria_centroides_b(k,dicio_dados):
04.    “”” Devolve lista de k centroides escolhidos aleatoriamente.”””
05.   centroides = []
06.   chaves_centroides = []
07.   for i in range(k):
08.      chave_alea = random.randint(1,len(dicio_dados))
09.      while chave_alea in chaves_centroides:
10.         chave_alea = random.randint(1,len(dicio_dados))
11.       centroides.append(dicio_dados[chave_alea])
12.       chaves_centroides.append(chave_alea)
13.    return centroides

Veja-se como o ciclo for trata do problema da contagem e como asseguramos a não repetição com um ciclo while no interior do ciclo for.

sábado, 12 de dezembro de 2009

Problema 9.2

Face à explicação dada para o problema 9.1, apresentamos sem mais uma solução para esta questão. Veja-se como resolvemos a questão de passar de uma lista para um conjunto de pontos: basta usar o operador * no parâmetro formal, e voltar a usar em zip. No primeiro caso empacotamos todos os pontos num tuplo; no segundo caso, desempacotamos esses mesmos pontos para poderem ser usados pelo zip!
1.def centroide_b(*pontos):
2.    """
3.    Calcula o centróide de um conjunto de pontos n-dimensionais.
4.    entrada = número variável de pontos.
5.    Assume a existência de pelo menos um ponto!!
6.    """
7.    cent = [sum(ponto)/len(ponto) for ponto in zip(*pontos)]
8.    return tuple(cent)

Problema 9.1

Em post anterior falei de listas por compreensão. Vejamos como elas nos ajudam a resolver esta questão. Recordo: trata-se de calcular o centróide de uma lista de pontos n-dimensionais.
1.def centroide_a(pontos):
2.    """
3.    Calcula o centróide de um conjunto de pontos n-dimensionais.
4.    Entrada = uma lista de pontos.
5.    Assume a existência de pelo menos um ponto!!
6.    """
7.    cent = [sum(ponto)/len(ponto) for ponto in zip(*pontos)]
8.    return tuple(cent)

Como fizemos? Precisamos somar por componente (coordenada) o valor, nessa componente, de cada ponto. Depois basta dividir pelo número de pontos. Que representação estamos a usar? Uma lista de tuplos. Por exemplo, para o caso a 3 dimensões podíamos ter algo como:
1.[(x1,y1,z1), (x2,y2,z2), (x3,y3,z3), (x4,y4,z4)]

Para a componente x, por exemplo, teríamos que fazer (x1 + x2 + x3 + x4)/4). Mas como passar da lista de pontos para uma lista em que temos as componentes de cada ponto juntas? Usamos a operação pré-definida zip. Com um detalhe: no argumento colocamos à cabeça o operador *, para que se possa operar correctamente. A diferença entre usar, ou não, * é ilustrada no exemplo seguinte:
1.>>> ll = [(1,2,3), (4,5,6)]
2.>>> zip(ll)
3.[((1, 2, 3),), ((4, 5, 6),)]
4.>>> zip(*ll)
5.[(1, 4), (2, 5), (3, 6)]
6.>>>

Repare-se que ao usar listas por compreensão, o nosso resultado é uma lista. Por isso na instrução return temos que forçar a passagem a tuplo.

Listas por Compreensão

Em Python existe uma construção que ajuda a tornar os programas mais curtos sem perda de legibilidade. Trata-se das listas por compreensão.

Suponhamos que temos uma lista de listas e que queremos formar uma nova lista, contendo o último elemento de cada uma das listas que fazem parte da lista inicial. Um modo de resolver seria:
1.def ultimos(arg):
2.    """ lista dos últimos..."""
3.    ult = []
4.    for lista in arg:
5.        ult.append(lista[-1])
6.    return ult

Percorremos a lista elemento a elemento. De cada um deles obtemos o seu último elemento e acrescentamos a um contentor (outra lista) que antes do ciclo criámos como lista vazia.

Usando agora listas por compreensão para resolver o mesmo problema:
1.def ultimos_comp(arg):
2.    """ Lista dos últimos."""
3.    return [ lista[-1] for lista in arg]

A sintaxe da formulação mais simples das listas por compreensão é a seguinte:
1.[ elemento for elemento in objecto]

Mas suponhamos, por exemplo, que no exemplo anterior apenas queremos os últimos elementos, caso sejam ímpares? Como fazer? Acrescenta-se um if!
1.def ultimos_comp_impares(arg):
2.    """ Lista dos últimos."""
3.    return [ lista[-1] for lista in arg if (lista[-1] % 2) == 1]

Admitamos que a nossa lista de listas representa uma imagem. Cada um dos seus elementos é um tuplo com três elementos entre 0 e 255. Como podemos obter a passagem da imagem para uma outra em que apenas mantemos o valor do canal vermelho e os outros dois ficam a 0? Usamos dois ciclos for!
1.def escala_cinzentos(imagem):
2.    """ Converte um imagem para a escala de cinzentos."""
3.    return [ [ (coluna[0],0,0 ) for coluna in linha]  for linha in imagem]

Notar que manter a estrutura da matriz, o ciclo mais à esquerda está envolto entre [ e ].

Como se pode ver é possível fazer muitas coisas com listas por compreensão. sempre que tiver uma oportunidade, use-as!

quinta-feira, 10 de dezembro de 2009

Matrizes, ciclos imbricados e impressões

Todos estamos habituados a lidar com informação cujo acesso se faz por indicação das suas coordenadas num espaço a n dimensões. Por exemplo, quando lidados com imagens, cada pixel é referenciado dessa forma, por duas coordenadas. Os matemáticos arranjaram um modo de organizar essa informação através de matrizes. Os cálculos complexos que temos que fazer com matrizes podem ser facilmente passados para o computador. Como aqui já referimos, a linguagem Python tem módulos que permitem lidar com matrizes (Scipy, por exemplo). Mas vamos supor que tal não existe. Um modo natural de representar uma matriz seria por recurso a uma lista de listas. Cada lista interior representa uma linha da matriz. Por exemplo:

1.matriz = [[1,2,3],[4,5,6],[7,8,9]]

Vamos ver como podemos visualizar os elementos de uma matriz quadrada qualquer. A questão essencial que temos que ter em conta é que, havendo duas dimensões, temos que criar dois ciclos, um dentro do outro,em que cada um deles responsável por percorrer de modo ordenado cada uma das dimensões. Vejamos o caso mais simples, ou seja, mostrar todos os elementos.
01.def mostra_por_linhas(matriz):
02. “””Indexação pelo conteúdo.”””
03. for linha in matriz:
04.  for coluna in linha:
05.   print %3d% coluna,
06.  print
07. 
08. 
09.def mostra_por_linhas(matriz):
10. “””Indexação pela posição.”””
11. for pos_linha in range(len(matriz)):
12.  for pos_coluna in range(len(matriz[0])):
13.   print %3d% matriz[pos_linha][pos_coluna],
14.  print

O que têm estes dois programas de diferente? O simples facto de, no primeiro, percorremos a matriz usando o seu conteúdo, enquanto que, no segundo, usamos as posições. E de comum, que pontos devem ser salientados??? Essencialmente o modo como fazemos a impressão: o comando print usa uma marca de formatação (%3d) e termina com uma vírgula. É este último facto que permite colocar os elementos de uma linha todos ao lado uns dos outros. O segundo print sem argumento serve apenas para mudar de linha.

Admitamos agora que queremos mostrar a matriz por colunas e não por linhas.
1.def mostra_por_colunas(matriz):
2.    """ AKO transposta. Indexação pelo conteúdo."""
3.    for pos_coluna in range(len(matriz[0])):
4.        for pos_linha in range(len(matriz)):
5.            print "%3d" % matriz[pos_linha][pos_coluna],
6.        print

Bastou trocar a ordem: o primeiro ciclo trata das colunas, enquanto o segundo ciclo (mais interior) as linhas!

E se forem as matrizes triangulares superior e inferior??
01.def mostra_tri_sup(matriz):
02.    """Matriz triangular superior.Indexação pela posição."""
03.    for pos_linha in range(len(matriz)):
04.        print ' '* 3 * pos_linha, # 3 por causa do %2d
05.        for pos_coluna in range(pos_linha,len(matriz[0])):
06.            print "%2d" % matriz[pos_linha][pos_coluna],
07.        print
08. 
09.def mostra_tri_inf(matriz):
10.    """Matriz triangular superior.Indexação pela posição."""
11.    for pos_linha in range(len(matriz)):
12.        for pos_coluna in range(0,pos_linha+1):
13.            print "%2d" % matriz[pos_linha][pos_coluna],
14.        print

Atente-se como tratamos de mostrar de modo conveniente. uma vez mais o comando print é essencial para esse objectivo. Terminamos com a matriz diagonal.
1.def mostra_diag_principal(matriz):
2.    """ Mostra diagonal principal."""
3.    for pos_linha in range(len(matriz)):
4.        print ' '* 3 * pos_linha,
5.        for pos_coluna in range(0,pos_linha+1):
6.            if pos_linha == pos_coluna:
7.                print "%2d" % matriz[pos_linha][pos_coluna]

Deixamos ao leitor a tarefa de testar estes programas. Fica também como exercício resolver o problema não de visualizar mas de criar.

segunda-feira, 7 de dezembro de 2009

Problema 8.13

Para animar o nosso programa uma possibilidade simples é usar o módulo cTurtle. Vamos definir o nosso boneco de acordo com as coordenadas dadas na figura.



O boneco estará separado em 6 partes: cabeça, tronco, braços e pernas. Isto coloca um limite de 6 tentativas falhadas. Claro que podemos alterar. Por exemplo, é preciso falhar 2 vezes seguidas para mais uma parte do corpo ficar pendurada. De qualquer maneira o que temos que introduzir no nosso código é uma chamada à operação de desenho cada vez que falhamos. Se é a nossa i tentativa falhada, então desenhamos a parte i do corpo. Eis o código.
01.import cTurtle
02. 
03.def desenha_enforcado(i):
04.    cTurtle.up()
05.    if(i==0):
06.        cTurtle.goto(60,-90)
07.        cTurtle.setheading(180)
08.        cTurtle.down()
09.        cTurtle.forward(130)
10.        cTurtle.right(90)
11.        cTurtle.forward(160)
12.        cTurtle.right(90)
13.        cTurtle.forward(70)
14.        cTurtle.right(90)
15.        cTurtle.forward(20)
16.        cTurtle.up()
17.        cTurtle.goto(-70,50)
18.        cTurtle.down()
19.        cTurtle.goto(-50,70)
20.    elif(i==1):
21.        cTurtle.goto(-20,30)
22.        cTurtle.down()
23.        cTurtle.circle(20)
24.    elif(i==2):
25.        cTurtle.goto(0,10)
26.        cTurtle.down()
27.        cTurtle.goto(0,-30)
28.    elif(i==3):
29.        cTurtle.goto(0,0)
30.        cTurtle.down()
31.        cTurtle.goto(-20,-10)
32.    elif(i==4):
33.        cTurtle.goto(0,0)
34.        cTurtle.down()
35.        cTurtle.goto(20,-10)
36.    elif(i==5):
37.        cTurtle.goto(0,-30)
38.        cTurtle.down()
39.        cTurtle.goto(-20,-70)
40.        cTurtle.goto(-30,-70)
41.    elif(i==6):
42.        cTurtle.goto(0,-30)
43.        cTurtle.down()
44.        cTurtle.goto(20,-70)
45.        cTurtle.goto(30,-70)
46.    cTurtle.hideturtle()

Problema 8.11

A tudo o que já foi feito anteriormente junta-se agora a introdução do conceito de nível. O nível de um jogador (iniciado, normal e perito, vai determinar a complexidade da palavra a adivinhar e o número de tentativas a que tem direito. No nosso caso, escolhemos o seguinte:

(1) iniciado: palavras até 4 caracteres, tentativas igual à soma de 6 com o comprimento da palavra;
(2) normal: palavras até 8 caracteres, tentativas igual à soma de 6 com o número de caracteres diferentes na palavra;
(3) perito: palavra com mais do que 8 caracteres, tentativas igual à soma de 6 com 0.8 * número de letras diferentes.

Posto isto apresentemos as definições que tratam da escolha da palavra (do ficheiro onde estão as palavra do nível escolhido), e do número de tentativas permitido.

01.def escolhe_nivel():   
02.    """ Escolhe o nível. Implicações para as palavras e o número de tentativas."""
03.    print
04.    print "Níveis possíveis: Iniciado, Normal, Perito."
05.    nivel = raw_input('Que nível [I/N/P]? ')
06.    if nivel == 'I':
07.        return '/tempo/data/pal_iniciado.txt', 'I'
08.    elif nivel == 'N':
09.        return '/tempo/data/pal_normal.txt', 'N'
10.    else:
11.        return '/tempo/data/pal_perito.txt', 'P'
12.     
13.def define_tentativas(dicio_pal, nivel):
14.    diferentes = len(dicio_pal)
15.    comprimento = sum([ len(indices) for indices in dicio_pal.values()])
16.    if nivel == 'I':
17.        return 6 + comprimento
18.    elif nivel == 'N':
19.        return 6 + diferentes
20.    else:
21.        return int(6 + 0.8 * diferentes)

No programa que joga precisamos introduzir a parte em que se define o nome do ficheiro onde vamos buscar a palavra, e o número de tentativas. O resto é mais do mesmo. O leitor deve consultar as soluções aos problemas anteriores.
01.def hang811():
02.    while True:       
03.        # Definição do nível
04.        ficheiro,nivel= escolhe_nivel()
05.        # --- palavra secreta
06.        palavras = open(ficheiro).read().split()
07.        secreta = list(random.choice(palavras))
08.        dicio = seq_to_dict(secreta)
09.        # --- parâmetros
10.        tentativas = define_tentativas(dicio,nivel)
11.        acertou = False
12.        estado = cria_estado(list('_'* len(secreta)), [],tentativas)
13.        # resto igual
14.        .......

Problema 8.10

É-nos pedido uma alteração ao programa de base (problema 8.7), por forma a ser possível ter informação estatística. Vamos discutir o princípio, e não vamos estar preocupados com definir informação detalhada do jogador e seu desempenho. Assim ,o que vamos querer sobretudo é que a informação sobreviva, mesmo depois de o jogador ter terminado de jogar. Isso significa ter a informação armazenada externamente, logo significa usar ficheiros. No entanto, pode ser conveniente internamente ao programa usar outra organização. Como vamos ter vários jogadores, cada um deles com informação estatística associada, o uso de dicionários surge como a forma mais natural de guardar a informação. Para tudo isto ser possível teremos que definir duas operações de interface: uma que converte do ficheiro para o dicionário e, uma segunda, que faz o inverso (nota para o programador savvy: também pode usar o módulo pickle e, neste caso não é preciso preocupar-se com as conversões!). São essas definições que apresentamos de seguida.
01.def fich_to_dict(ficheiro):
02.    """Transforma os dados de um ficheiro para um dicionário."""
03.    linhas = ficheiro.readlines()
04.    dicio = {}
05.    for linha in linhas:
06.        nome,jogos,vitorias = linha.split()
07.        dicio.update({nome:[int(jogos),int(vitorias)]})
08.    return dicio
09. 
10.def dict_to_fich(dicio, ficheiro):
11.    """Transforma os dados de um dicionário para um ficheiro."""
12.    f_out= open(ficheiro,'w')
13.    for chave,valor in dicio.items():
14.        linha = chave + '\t\t' + str(dicio[chave][0]) + '\t' + str(dicio[chave][1]) + '\n'
15.        f_out.write(linha)
16.    f_out.close()
17.    return dicio   

Por outro lado, a parte inicial do programa também deve ser alterada, de modo a identificar o jogador e saber como actualizar os dados no final de cada jogo. Vejamos como.
01.def hang810a(fich_dados):
02.    # Mensagem inicial
03.    print 'Bem Vindo ao Jogo do Enforcado!!!'
04.    print 'Vamos jogar!'
05.    # recupera informação estatística
06.    f_in = open(fich_dados,'r')
07.    dicio_jogadores = fich_to_dict(f_in)
08.    f_in.close()
09.    # identificação do jogador
10.    nome = raw_input('O seu nome por favor: ')
11.    if nome in dicio_jogadores:
12.        print 'Olá de novo!'
13.        print 'O seu desempenho actual é:'
14.        print 'Jogos efectuados: %d \nVitórias: %d' % (dicio_jogadores[nome][0], dicio_jogadores[nome][1])
15.    else:
16.        print 'Bem Vindo pela primeira vez!'
17.        dicio_jogadores[nome]=[0,0]
18.         
19.    # jogar
20.    jogar = True
21.    while jogar:
22.        hang810(nome,dicio_jogadores)
23.        jogar = continuar(jogar,dicio_jogadores, fich_dados)
24.         
25.             
26.def continuar(jogar,dicio_jogadores,fich_dados):
27.    # Jogar mais??
28.    mais = raw_input('mais? [S/N]: ')
29.    while mais not in ['S','N', 's','n','sim','não']:
30.        mais = raw_input('A resposta tem que ser [S/N]. A sua  resposta: ')
31.    if mais in ['N','n','não']:
32.        # Actualiza estatística
33.        dict_to_fich(dicio_jogadores,fich_dados)
34.        print 'Adeus, até à vista...'
35.        jogar = False
36.    return jogar    

Como se pode ver o programa depois de fazer a inicialização necessária, chama o programa que efectivamente joga. Aqui tivemos que fazer as alterações que envolvem a actualização da parte estatística no final de cada jogo. Por outro lado, mantivemos a parte do programa que permite jogar mais do que uma vez.

01.def hang810(nome,dicio_jogadores):
02. 
03.    # --- palavra secreta
04.    palavras = open('/tempo/data/palavras.txt','r').read().split()
05.    secreta = list(random.choice(palavras))
06.    dicio = seq_to_dict(secreta)
07. 
08.    # --- parâmetros
09.    tentativas = len(secreta)
10.    acertou = False
11.    estado = cria_estado(list('_'* len(secreta)), [],tentativas)
12. 
13.    # Começa o jogo
14.    for tentativa in range(tentativas):
15.        # Estado actual
16.        mostra_estado(estado)
17.        # joga
18.        letra = adivinha(estado)
19.        # analisa resposta
20.        if letra in dicio:
21.            # --- Acertou na letra!
22.            indices = dicio[letra]
23.            pal_utilizador = get_palavra(estado)
24.            for ind in indices:
25.                pal_utilizador[ind] = letra
26.            estado= set_palavra(estado,pal_utilizador)
27.            # --- Acertou na palavra??
28.            if fim(secreta,pal_utilizador):
29.                acertou = True
30.                mensagem_sim(secreta)
31.                break
32.        # Actualiza estado
33.        estado = actualiza_estado(estado, estado['palavra'],letra, get_tentativas(estado) - 1)
34.    # actualiza estatística
35.    dicio_jogadores[nome][0] = dicio_jogadores[nome][0] + 1
36.    if acertou:
37.        dicio_jogadores[nome][1] = dicio_jogadores[nome][1] + 1
38.    # mensagem final
39.    mensagem_fim(acertou,secreta)

Falta apenas incluir as definições auxiliares introduzidas nas versões anteriores.

domingo, 6 de dezembro de 2009

Objectos com Classe!!

Temos estado a discutir como a definição de uma estrutura de dados adequada pode ser benéfica para a qualidade da programação. A estrutura de dados tem duas componentes: o objecto propriamente dito, devidamente representado, e um conjunto de operações sobre o objecto, operações que dependem da representação escolhida. Dizemos que as operações escondem o objecto do exterior, ou, dito de outro modo, o objecto fica encapsulado pelas operações. Normalmente o programador deve-se preocupar em definir estrutura de dados que sejam eficientes computacionalmente, passando, se for esse o caso, a só ter que se preocupar com as funcionalidades oferecidas pela estrutura. O leitor mais atento já ligou este aspecto ao que se passa com os tipos de dados das linguagens de programação: nós operamos com números inteiros, por exemplo, sem cuidar de saber como é que estes inteiros estão organizados. Apenas nos interessam as funcionalidades. Alguém teve o trabalho de definir esses tipos para nós. Mas será que nós podemos definir novos tipos de dados com as mesmas características de abstracção? Já vimos que sim! Mas podemos fazer, diferente, para melhor? Uma vez mais a resposta é sim, e liga-se ao conceito de objecto tal como é usado em programação orientada aos objectos: o objecto é um todo englobando as duas componentes acima referidas.


Neste paradigma de programação o conceito de classe é central. De um modo simples, uma classe é um modelo abstracto de objectos. Traduz as características comuns a todos os objectos da classe.Na vida comum nós falamos da classe dos mamíferos, sendo o meu cão ‘Bobi’ e eu objectos dessa classe. Dizemos que os objectos são instâncias da classe. Repetindo: os objectos formam um todo com as suas características (estado) definidas por meio de atributos (por exemplo, palavra, letras usadas, tentativas) e as operações (por exemplo, add_letra_pal) determinando o seu comportamento.


Em Python todos os tipos de dados são implementados como classes. Vamos tentar fazer o mesmo com o conceito de estado apresentado no problema 8.9.
01.# classe Estado
02.class Estado(object):
03.     
04.    # Construtor
05.    def __init__(self,palavra =[],usadas=[],tentativas=0):
06.        """ Cria estado 'vazio'."""
07.        self.palavra = list(palavra)
08.        self.usadas = list(usadas)
09.        self.tentativas = tentativas
10.     
11.    # Acessores
12.    def get_estado(self):
13.        # Devolve estado
14.        return (self.palavra, self.usadas, self.tentativas)
15. 
16.    def get_pal(self):
17.        return  self.palavra
18. 
19.    def get_letras(self):
20.        return self.usadas
21. 
22.    def get_tentativas(self):
23.        return  self.tentativas
24. 
25.    # Modificadores
26.    def set_estado(self, palavra,letras,tentativas):
27.        # Altera estado
28.        self.palavra = list(palavra)
29.        self.usadas = list(letras)
30.        self.tentativas = tentativas
31. 
32. 
33.    def set_palavra(self,pal):
34.        self.palavra = list(pal)
35. 
36. 
37.    def set_usadas(self,letras):
38.        self.usadas = list(letras)
39. 
40. 
41.    def set_tentativas(self,tenta):
42.        if tenta >= 0:
43.            self.tentativas = tenta
44.        else:
45.            print 'O valor não pode ser negativo. Foi indicado %d.' % tenta  
46. 
47. 
48.    def add_letra_palavra(self, letra,l_pos):
49.        """ Junta a letra em todas as posições não ocupadas indicadas em l_pos."""
50.        for ind in l_pos:
51.            if self.palavra[ind] == '_':
52.                self.palavra[ind] = letra
53.            else:
54.                print 'Posição %d já ocupada. Estado inalterado!' % ind
55. 
56. 
57.    def add_letra_usadas(self, letra):
58.        """ Junta a letra às letras usadas."""
59.        if not letra in self.usadas:
60.            self.usadas.append(letra)
61.        else:
62.            print '%s já existente. Estado inalterado!' % letra
63. 
64.    def add_tentativas(self,tenta):
65.        """ Modifica o valor das tentivas em tenta."""
66.        novo_valor = self.tentativas + tenta
67.        if novo_valor >= 0:
68.            self.tentativas = novo_valor
69.        else:
70.            print 'O valor não pode ser negativo. o Resultado foi %d.' % novo_valor   
71. 
72. 
73.    # Auxiliares
74.    def __str__(self):
75.        palavra = ' '.join(self.palavra)
76.        usadas =  ', '.join(self.usadas)
77.        tentativas = self.tentativas
78.        return 'Palavra: %s\nUsadas: %s\nTentativas: %d' % (palavra,usadas,tentativas)
79. 
80.if __name__ == '__main__':
81.    estado = Estado()
82.    estado.set_palavra('__n_n_')
83.    estado.add_letra_usadas('b')
84.    estado.set_tentativas(7)
85.    print estado
86.    estado.add_letra_usadas('b')
87.    estado.set_tentativas(-4)
88.    estado.add_letra_palavra('a',[1,2,3,5])
89.    estado.add_letra_usadas('a')
90.    print estado


Carregue o código e execute-o. Sem entrar em muitos detalhes, o que podemos dizer por comparação ao que fizemos anteriormente?

(1) Aparece uma nova construção denominada classe;

(2) As definições anteriormente apresetnadas estão agora no interior da classe;

(3) O construtor tem agora o nome __init__ (trata-se de um método especial que permite ao utilizador fugir ao construtor por defeito);

(4) Já não temos um dicionários mas sim atributos do objecto definidos no interior do método __init__;

(5) O nome da estrutura (estado) é substituído pelo nome genérico self;

(6) Os modificadores alteram os atributos e não devolvem por return;

(7) A chamada dos métodos da classe usa a notação por ponto (dot notation);

(8) Na chamada dos métodos não se usa o parâmetro self


Muito mais poderia ser dito, mas ficamos por aqui. O leitor interessado pode alterar o código do enforcado para usar a nova classe.

Estruturas de Dados e Abstracção

Regressemos à ideia de estado avançada no problema do Enforcado. Dissemos que o estado era o que andava a ser comunicado entre o programa e o utilizador. Dissemos também que a implementação seria feita com um dicionário. Existem algumas questões interessantes que se colocam. Uma primeira, prende-se com de saber como definir uma camada de abstracção entre a estrutura de dados estado e o resto do mundo. Uma segunda, é a de saber como reagir a uma modificação na implementação do estado, por exemplo, passando de dicionário para lista.


Para responder a estas questões vamos começar por definir uma estrutura de dados, a que chamaremos estado, e que, como referimos será implementada como um dicionário.
01.# estado como dicionário
02.# {'palavra':..., 'usadas':..., 'tentativas':...}
03.# palavra e usadas são listas de caracteres. tentativas é um inteiro não negativo
04. 
05.# Construtor
06.def cria_estado():
07.    """ Cria estado 'vazio'."""
08.    estado= dict(palavra=[],usadas=[],tentativas=0)
09.    return estado
10.     
11.# Acessores
12.def get_estado(estado):
13.    # Devolve estado
14.    pal = ' '.join(estado['palavra'])
15.    letras = ', '.join(estado['usadas'])
16.    tenta = estado['tentativas']
17.    return (pal, letra,tenta)
18. 
19.def get_pal(estado):
20.    return  ' '.join(estado['palavra'])
21. 
22.def get_letras(estado):
23.    return  ' '.join(estado['usadas'])
24. 
25.def get_tentativas(estado):
26.    return  estado['tentativas']
27. 
28.# Modificadores
29.def set_estado(estado, palavra,letras,tentativas):
30.    # Altera estado
31.    estado['palavra'] = list(palavra)
32.    estado['usadas'] = list(letras)
33.    estado['tentativas'] = tentativas
34.    return estado
35. 
36.def set_pal(estado,pal):
37.    estado['palavra'] = list(pal)
38.    return estado
39. 
40.def set_letras(estado,letras):
41.    estado['usadas'] = list(letras)
42.    return estado
43. 
44.def set_tentativas(estado,tenta):
45.    if tenta >= 0:
46.        estado['tentativas'] = tenta
47.    else:
48.        print 'O valor não pode ser negativo. Foi indicado %d.' % tenta  
49.    return estado
50. 
51.def add_letra_pal(estado, letra,l_pos):
52.    """ Junta a letra em todas as posições não ocupadas indicadas em l_pos."""
53.    for ind in l_pos:
54.        if estado['palavra'][ind] == '_':
55.            estado['palavra'][ind] = letra
56.        else:
57.            print 'Posição %d já ocupada. Estado inalterado!' % ind
58.    return estado
59. 
60.def add_letra_letras(estado, letra):
61.    """ Junta a letra às letras usadas."""
62.    if not letra in estado['usadas']:
63.        estado['usadas'].append(letra)
64.    else:
65.        print '%s já existente. Estado inalterado!' % letra
66.    return estado
67. 
68.def add_tentativas(estado,tenta):
69.    """ Modifica o valor das tentivas em tenta."""
70.    novo_valor = estado['tentativas'] + tenta
71.    if novo_valor >= 0:
72.        estado['tentativas'] = novo_valor
73.    else:
74.        print 'O valor não pode ser negativo. o Resultado foi %d.' % novo_valor   
75.    return estado
76. 
77.# Auxiliares
78.def mostra_estado(estado):
79.    # Mostra palavra
80.    print 'Palavra Actual: ',' '.join(estado['palavra'])
81.    print
82.    # Mostra letras usadas
83.    print 'Letras já usadas: ',', '.join(estado['usadas'])
84.    print
85.    # Mostra tentativas restantes
86.    print 'Ainda tem as tentativas: ', estado['tentativas']
87.    print

Acabamos de mostrar como uma estrutura de dados se define através de grupos de operações. Um (ou mais) construtor, acessores (para o todo ou a parte dos elementos da estrutura), modificadores (do todo ou da parte da estrutura) e, ainda operações auxiliares. Algumas dessas operações são simétricas: uma consulta (get), a outra altera (set ou add). Com base nelas o nosso código para o enforcado pode ser reescrito.

01.def hang89():
02.    # inicialização
03.    # --- palavra secreta
04.    palavras = open('/tempo/data/palavras.txt').read().split()
05.    secreta = list(random.choice(palavras))
06.    dicio = seq_to_dict(secreta)
07.    # --- parâmetros
08.    TAMANHO = len(secreta)
09.    LIMITE = limite(TAMANHO)
10.    estado = cria_estado()
11.    estado = set_estado(estado,'_'* TAMANHO,'',LIMITE)
12.    acertou = False
13.    # Entra no jogo
14.    for tentativa in range(LIMITE):
15.        # estado actual
16.        mostra_estado(estado)
17.        # joga
18.        letra = adivinha(estado['usadas'])
19.        # analisa resposta
20.        if letra in dicio:
21.            # --- Acertou na letra!
22.            indices = dicio[letra]
23.            estado = add_letra_pal(estado,letra,indices)
24.            # --- Acertou na palavra??
25.            if fim(secreta,estado['palavra']):
26.                acertou = True
27.                mensagem_sim(secreta)
28.                break
29.        # actualiza estado
30.        esatdo = add_letra_letras(estado,letra)
31.        estado = add_tentativas(estado, -1)
32.    # mensagem final
33.    mensagem_fim(acertou,secreta)

A pergunta natural que se coloca é a de saber o que ganhámos com estas alterações. A resposta liga-se às duas questões iniciais. Imaginemos que decidimos altera a representação de um dicionário para uma lista. Como proceder? Nesta abordagem basta alterar as operações sobre a estrutura de dados estado. Não precisamos alterar nada no código do programa principal! Programar por camadas de abstracção tem elevados ganhos de produtividade.

Problema 8.9

No jogo do Enforcado existe um conceito importante que é o conceito de estado, conceito esse que traduz a situação do jogo num dado momento. Jogar significa provocar uma mudança de estado. Um outro aspecto neste jogo é o da comunicação entre o programa e o utilizador. Para que essa comunicação seja efectiva deve ser implementado um bom interface entre ambos. É o que vamos mostrar usando precisamente a noção de estado. Isso vai-nos levar a alterar o programa hang87(), mostrando que a nossa análise inicial não foi a melhor. Concentrar tudo no estado vai ser benéfico, permitindo mais tarde alterar o programa de modo mais fácil, caso a especificação seja alterada.


Para já decidimos incluir no estado a palavra que o utilizador está a construir, as letras já tentadas e o número de tentativas de que ainda dispõe. Não é difícil aceitar que a melhor estrutura para guardar esta informação será um dicionário. As operações sobre o estado serão as operações de consulta e de alteração de componentes. Apresentamos a nova versão de hang87.
01.def hang89():
02.    # inicialização
03.    # --- palavra secreta
04.    palavras = open('/tempo/data/palavras.txt').read().split()
05.    secreta = list(random.choice(palavras))
06.    dicio = seq_to_dict(secreta)
07.    # --- parâmetros
08.    TAMANHO = len(secreta)
09.    LIMITE = limite(TAMANHO)
10. 
11.    estado = {'palavra':list('_'* TAMANHO),'usadas':[],'tentativas':LIMITE}
12.    acertou = False
13.    # Entra no jogo
14.    for tentativa in range(LIMITE):
15.        # estado actual
16.        mostra_estado(estado)
17.        # joga
18.        letra = adivinha(estado['usadas'])
19.        # analisa resposta
20.        if letra in dicio:
21.            # --- Acertou na letra!
22.            indices = dicio[letra]
23.            for ind in indices:
24.                estado['palavra'][ind] = letra
25.            # --- Acertou na palavra??
26.            if fim(secreta,estado['palavra']):
27.                acertou = True
28.                mensagem_sim(secreta)
29.                break
30.        # actualiza estado
31.        estado['usadas'] = estado['usadas'] + [letra]
32.        estado['tentativas'] = estado['tentativas'] - 1
33.    # mensagem final
34.    mensagem_fim(acertou,secreta)

Problema 8.8

Num post anterior apresentámos a solução para o problema 8.7. Chamámos a função principal hang87. Com base nisso vamos mostrar a solução para este problema.

01.def hang88():
02.    print 'Vamos jogar!'
03.    jogar = True
04.    while jogar:
05.        hang87()
06.        mais= raw_input('Jogar mais? [S/N]: ')
07.        if mais == 'N':
08.            print 'Adeus, até à vista...'
09.            jogar = False

Como se pode ver não alterámos o nosso programa principal. Apenas definimos um programa envolvente (os ingleses chamam-lhe wrapper). Claro que esta solução é muito simplista, e pressupõe que tudo o que não for N significa que se quer continuar a jogar. Mas podemos melhorar a solução.
01.def hang88b():
02.    print 'Vamos jogar!'
03.    jogar = True
04.    while jogar:
05.        jogar = continuar(jogar)
06.             
07.def continuar(jogar):
08.    mais = raw_input('mais? [S/N]: ')
09.    while mais not in ['S','N', 's','n','sim','não']:
10.        mais = raw_input('A resposta tem que ser [S/N]. A sua  resposta: ')
11.    if mais in ['N','n','não']:
12.            print 'Adeus, até à vista...'
13.            jogar = False
14.    return jogar


Deixamos ao leitor a tarefa de fazer à sua maneira. A moral desta solução era apenas a de que não foi preciso alterar o nosso programa principal.


Ao longo da vida de um programador a tarefa mais comum é ter que manter um programa, seja devido a erros seja devido a alterações na especificação. Quanto melhor tiver sido construído o nosso programa, mais fácil serão estas tarefas!