sexta-feira, 19 de dezembro de 2014

Teste #3 - TP 1

P1

Explicar com rigor o que se passou na listagem abaixo.
>>> lista = [[]] * 3
>>> lista
[[], [], []]
>>> lista[1].append('ai')
>>> lista
[['ai'], ['ai'], [‘ai']]
A primeira instrução vai criar uma lista com três elementos, todos eles uma lista vazia, que de é visualizada(linha 2). Aplicamos depois o método append para acrescentar à lista vazia na segunda posição a cadeia de caracteres 'ai'. O método append altera a lista na segunda posição sem alterar a sua localização. Como os três elementos iniciais partilham a mesma memória, isto é, todos apontam para o mesmo objecto (a lista vazia), alterar um deles significa alterar todos. Estamos perante uma das consequências das listas serem objectos mutáveis e haver partilha de memória.

P2

Um ficheiro com a seguinte informação por linha: ano e cotação. Qual a cotação máxima e o respectivo ano?
def cota_maxima(ficheiro):
    with open(ficheiro) as f_ent:
        maximo = 0
        ano = 0
        for linha in f_ent:
            dados = linha[:-1].split()
            ano_cur = int(dados[0])
            valor_cota = float(dados[1])
            if valor_cota > maximo:
                maximo = valor_cota
                ano = ano_cur
        return ano, maximo
Erro mais frequente: ler todas as linhas com o readlines e depois aplicar o método split. Só que o readlines devolve uma lista e o split não se aplica a listas.

P3

A classificação do campeonato está guardada num dicionário (chave = nome clube, valor = pontos). Conhecidos os resultados de uma jornada, actualizar a classificação. Os resultados da jornada é representado por uma lista de jogos e cada jogo é uma lista em que o primeiro elemento é o nome do clube 1, o segundo os golos marcado, o terceiro o nome do clube 2 e o quarto os golos marcados.

def actualiza_tabela(tabela, resultados):
    for resultado in resultados:
        if resultado[1] > resultado[3]:
            tabela[resultado[0]] += 3
        elif resultado[1] == resultado[3]:
            tabela[resultado[0]] += 1
            tabela[resultado[2]] += 1
        else:
            tabela[resultado[2]] += 3
    return tabela
Erro comum: usar dois ciclos em que o primeiro percorre o dicionário e o segundo repete quatro vezes a alteração de um jogo…

segunda-feira, 8 de dezembro de 2014

A minha lista telefónica

É tempo de desenvolver uma aplicação interactiva que permita manter actualizada a minha lista de contactos telefónicos. Para simplificar vamos admitir que a lista apenas vai conter o nome e o número. Em abstracto existem várias operações que vou pretender poder fazer: adicionar um número, remover um número, procurar um número, carregar a lista de contactos, salvar um lista de contactos.

Sendo uma lista de contactos telefónicos que quero manter permanentemente, é óbvio que ela vai estar guardada num ficheiro. Por outro lado, a actualização da lista vai obrigar a operações que me interessa que sejam feitas de forma simples e eficiente. Veremos que um modo de o conseguir é ter os contactos armazenados na memória como um dicionário, de chave igual ao nome e valor igual ao número de telefone.

. Este tipo de programas interactivos tem uma estrutura clássica, ou se quiserem, existe um padrão de desenho para o resolver. Fundamentalmente teremos um ciclo potencialmente infinito que em cada repetição imprime um menu de opções e depois, em função das nossas escolhas lança o respectivo programa. Mostremos então esse padrão.
def main():
    lista_tele = {}
    while True:
        mostra_menu()
        menu_escolhas = int(input("Escolha a sua opção (1-7): "))
        if menu_escolhas == 1:
            mostra_lista(lista_tele)
        elif menu_escolhas == 2:
            print("Acrescentar Nome e Número")
            nome = input("Nome: ")
            telefone = input("Número: ")
            acrescenta_numero(lista_tele, nome, telefone)
        elif menu_escolhas == 3:
            print("Retira Nome e Número")
            nome = input("Nome: ")
            retira_contacto(lista_tele, nome)
        elif menu_escolhas == 4:
            print("Procura Número")
            nome = input("Nome: ")
            print(procura_numero(lista_tele, nome))
        elif menu_escolhas == 5:
            nome_ficheiro = input("Ficheiro a carregar: ")
            carrega_lista(lista_tele, nome_ficheiro)
        elif menu_escolhas == 6:
            nome_ficheiro = input("Ficheiro a salvar: ")
            salva_lista(lista_tele, nome_ficheiro)
        elif menu_escolhas == 7:
            break
        else:
            mostra_menu()
     
    print("Fim")
    
def mostra_menu():
    pass

def mostra_lista(lista_tele):
    pass

def acrescenta_numero(lista_tele,nome,telefone):
    pass
    
def retira_contacto(lista_tele, nome):
    pass

def procura_numero(lista_tele,nome):
    pass

def carrega_lista(lista_tele, nome_ficheiro):
    pass

def salva_lista(lista_tele,nome_ficheiro):
    pass

if __name__ == '__main__':
    main()

Este programa já pode ser corrido, para testes primitivos, embora não faça nada ainda de relevante. Vamos então completar o que falta, começando pelo mais simples, a função que mostra o menu de opções.
def mostra_menu():
    print("1. Mostra Lista Telefónica")
    print("2. Acrescentar Entrada (Nome, Número)")
    print("3. Retirar Entrada (Nome, Número)")
    print("4. Procurar Número")
    print("5. Carregar Lista Telefónica")
    print("6. Salvar Lista Telefónica")
    print("7. Terminar")
    print()
Vamos agora passar às funções mais interessantes e que se baseiam nas escolhas já enunciadas: a lista está guardada num ficheiro externo e é manipulada internamente como um dicionário. Carregar a lista telefónica consiste então em ler um ficheiro e guardar essa informação num dicionário.
def carrega_lista(lista_tele, nome_ficheiro):
    with open(nome_ficheiro,'r') as f_ent:
        for contacto in f_ent:
            dados = contacto[:-1]
            nome, numero = dados.split()
            lista_tele[nome] = numero
        return lista_tele
Este programa pode ser testado de forma autónoma, bastando apenas existir previamente um pequeno ficheiro com uma lista telefónica. Por exemplo, com o ficheiro
ernesto   123
patricia  456
daniela 789
pedro 012
ao corremos o programa o resultado será:
{'pedro': '012', 'ernesto': '123', 'patricia': '456', 'daniela': '789'}
Notar que os nomes e os números são tratados como cadeia de caracteres. Vamos agora escrever o código da função inversa, ou seja, salvar uma lista telefónica interna guardada num dicionário num ficheiro.
def salva_lista(lista_tele,nome_ficheiro):
    with open(nome_ficheiro,'w') as f_saida:
        for chave,valor in lista_tele.items():
            contacto = chave + '  ' + valor + '\n'
            f_saida.write(contacto)
Não há muitas observações a fazer. Apenas notar que usamos espaços em branco para separar o nome do número e que cada contacto termina com a marca de fim de linha. Vamos agora implementar as funções que internamente manipulam a lista telefónica.
def acrescenta_numero(lista_tele,nome,telefone):
    lista_tele[nome] = telefone
    return lista_tele
    
def retira_contacto(lista_tele, nome):
    if nome in lista_tele:
        del lista_tele[nome]
        return lista_tele
    else:
        print('Contacto inexistente')

def procura_numero(lista_tele,nome):
    if nome in lista_tele:
        print(lista_tele[nome])
    else:
        print('Contacto inexistente’)
São programas muito simples. Apenas temos o cuidado de verificar que o contacto existe na lista. Fica por resolver a função que mostra a lista telefónica. Também não apresenta grande dificuldade. O único aspecto que requere a nossa atenção é que a visualização seja feita por ordem dos nomes.
def mostra_lista(lista_tele):
    for nome,numero in sorted(lista_tele.items()):
        print()
        print('Nome: %s\nNúmero: %s' % (nome,numero))
E pronto. Agora pode divertir-se. Mas lembre-se que se trata de uma solução simples e com algumas fragilidades. Em particular, não esta muito protegida contra erros na entrada de dados, ou não está previsto que uma pessoa possa ter mais do que um telefone. Pense nestas e noutras questões e melhore a solução apresentada.

sábado, 6 de dezembro de 2014

Do Problema Ao Programa

Programar é uma actividade complexa. Obriga-nos a dominar o domínio do problema (se eu não souber o que são números primos como posso escrever um programa para verificar se um número é primo?), obriga-nos a conhecer uma linguagem de programação (se eu precisar de ler informação guardada num ficheiro externo e não conhecer as instruções sobre ficheiros, como posso esperar resolver o problema?). Mas mesmo tendo estas duas competências, ainda é preciso outra, fundamental: como passar do problema ao programa, como dominar a complexidade inerente ao desenvolvimento de um programa? Vamos ver um dos modos de dominar essa complexidade, que consiste em decompor um problema em sub-problemas mais simples, algo que Descartes enunciou de modo claro.

Vamos supor que o nosso problema consiste em querer desenhar numa tela um caminho aleatório formado por segmentos de recta. Vamos supor ainda que nos pedem que fique guardado num ficheiro externo uma representação do caminho. Vamos então proceder por partes.
def caminho_alea(n):
    # Gerar segmentos do caminho
    # Guardar caminho
    # Ler caminho
    # Mostrar caminho
    pass

if __name__ == '__main__':
    caminho_alea(30)
O que nos diz esta solução? Bom , diz-nos que decidimos ter n segmentos, que vão ser gerados, depois guardados, a seguir lidos e finalmente desenhados. Ou seja: dividimos o nosso problema inicial em quatro sub-problemas. O curioso da questão é que este programa corre sem erros, embora não faça nada…

Como vamos resolver cada sub-problema? Por que ordem? Vamos começar por resolver o primeiro dos quatro. Vamos tornar mais específico o enunciado. Vamos admitir que um segmento é representado pelos seus pontos extremos. Vamos ainda concretizar os valores possíveis para dada um dos pontos. Como se trata de um caminho, vamos garantir a ligação entre os segmentos impondo que os segmentos consecutivos têm uma extremidade em comum. Isso tem como consequência que eu possa pensar em termos de pontos consecutivos e não em segmentos. Assim preciso de um programa que gere pontos, podendo cada um deles assumir valores num dado intervalo. Os pontos serão representados por uma lista com as coordenadas do ponto.
import random

def gera_pontos(n, inf, sup):
    pontos = []
    for i in range(n):
        pontos.append([random.randint(inf,sup), random.randint(inf,sup)])
    return pontos

def gera_pontos_2(n,inf,sup):
    return [[random.randint(inf,sup), random.randint(inf,sup)] for i in range(n)]
Como se pode ver, apresentamos duas soluções alternativas, uma primeira convencional e, uma segunda, que recorre a listas por compreensão. Em qualquer das soluções usamos listas como contentores onde são guardados os pontos. Os valores são gerados aleatoriamente recorrendo ao módulo random. Com este sub-problema resolvido, e que pode ser testado isoladamente, podemos avançar no nosso programa.
def caminho_alea(n, inf,sup):
    # Gerar segmentos do caminho
    pontos = gera_pontos(n, inf, sup)
    # Guardar caminho
    # Ler caminho
    # Mostrar caminho
Note-se que o número de parâmetros foi alterado e também o significado do primeiro. Agora não traduz o número de segmentos mas antes o número de pontos. Continuemos. Vamos guardar os pontos num ficheiro externo, um ponto por linha.
def guarda_caminho(ficheiro, dados):
    fich_dados = open(ficheiro,'w')
    for x,y in dados:
        linha = str(x) + '\t' + str(y) + '\n'
        fich_dados.write(linha)
    fich_dados.close()   
Esta solução não tem nada de especial: abrimos um ficheiro para escrita (criando-o caso não exista), e depois vamos percorrer a lista de pontos retirando-os um a um, fabricando a cadeia de caracteres que representa a linha e escrevendo essa linha no ficheiro. Notar que existe uma tabulação entre cada coordenada e que a linha termina com a marca de fim de linha. Este programa pode ser testado isoladamente! Por outro lado, sendo autónomo podemos usá-lo noutro tipo de aplicação. A modularidade compensa!! E de novo a nossa solução global pode ser refinada.
def caminho_alea(n, inf,sup, ficheiro):
    # Gerar segmentos do caminho
    pontos = gera_pontos(n, inf, sup)
    # Guardar caminho
    guarda_caminho(ficheiro,pontos)
    # Ler caminho
    # Mostrar caminho
Notar o aparecimento de mais um parâmetro, o nome do ficheiro. Resolvida esta questão vamos passar à operação inversa: obter as coordenadas a partir do ficheiro.
def ler_caminho(ficheiro):
    fich_dados = open(ficheiro,'r')
    valores = []
    for linha in fich_dados:
        ponto = linha[:-1].split('\t')
        x = int(ponto[0])
        y = int(ponto[1])
        valores.append([x,y])
    fich_dados.close() 
    return valores
O que se pode dizer sobre esta solução? Talvez chamar a atenção para o modo como se transformou uma cadeia de caracteres que representa uma linha do ficheiro, numa lista com dois inteiros. linha[:-1] retira a marca de fim de linha. split(’\t’) divide o que resta numa lista com duas cadeias de caracteres que representam as coordenadas. Como são cadeias de caracteres foi preciso converter cada uma num inteiro. E nova solução global.
def caminho_alea(n, inf,sup, ficheiro):
    # Gerar segmentos do caminho
    pontos = gera_pontos(n, inf, sup)
    # Guardar caminho
    guarda_caminho(ficheiro,pontos)
    # Ler caminho
    pontos = ler_caminho(ficheiro)
    # Mostrar caminho
Já só nos falta mostrar o caminho. Vamos fazê-lo recorrendo ao módulo matplotlib.
import matplotlib.pyplot as plt

def mostra_caminho(pontos):
    plt.title('Caminho')
    plt.xlabel('X')
    plt.ylabel('Y')
    lista_x = [ x for x,y in pontos]
    lista_y = [y for x,y in pontos]
    plt.plot(lista_x,lista_y)
    plt.show()   
Nesta solução, que uma vez mais podemos testar isoladamente, a única dificuldade residia no facto de termos que separar os pontos em duas listas, cada uma com os valores de cada coordenada. Para os que gostam de aventuras mais arrojadas, deixamos aqui uma alternativa à conversão dos dados que recorre ao iterador zip.
import matplotlib.pyplot as plt

def mostra_caminho(pontos):
    plt.title('Caminho')
    plt.xlabel('X')
    plt.ylabel('Y')
    coord = zip(*pontos)
    plt.plot(*coord)
    plt.show()  
Um dia explicaremos o porquê do asterisco... E agora vamos juntar tudo.
def caminho_alea(n, inf,sup, ficheiro):
    # Gerar segmentos do caminho
    pontos = gera_pontos(n, inf, sup)
    # Guardar caminho
    guarda_caminho(ficheiro,pontos)
    # Ler caminho
    pontos = ler_caminho(ficheiro)
    # Mostrar caminho
    mostra_caminho(pontos)
E pronto. Já está. Ou talvez não… Olhando para a solução acima é um pouco bizarro que tendo os pontos logo após o primeiro passo, vamos estar a lê-los a partir do ficheiro. Daí se possa simplificar o programa.
def caminho_alea(n, inf,sup, ficheiro):
    # Gerar segmentos do caminho
    pontos = gera_pontos(n, inf, sup)
    # Guardar caminho
    guarda_caminho(ficheiro,pontos)
    # Mostrar caminho
    mostra_caminho(pontos)
Agora sim: geramos, guardamos e mostramos! Moral da estória: dividir compensa!