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!

Sem comentários:

Enviar um comentário