segunda-feira, 28 de outubro de 2013

A causa das coisas (II)

Nunca paramos de admirar os feitos desportivos dos grandes desportistas, seja no futebol, no golfe, no xadrez ou em qualquer outra actividade. A que se deve o seu tão elevado desempenho? Uns dirão que é o resultado da genética, ou seja uma predisposição natural para serem excelentes no que fazem. Seguramente que uma “boa” genética ajuda. Mas... se os ouvirmos falar, eles referem em primeiro lugar uma coisa mais simples: treino, muito treino.

No mundo da Ciência e da Tecnologia as coisas não são muito diferentes. A demonstração do último Teorema de Fermat (http://en.wikipedia.org/wiki/Fermat's_Last_Theorem) por Andrew Wiles no final do século passado mostra um cientista que durante cerca de 8 anos se dedicou à sua demonstração.Uma tarefa que muitos (todos, menos ele?) julgavam impossível. E conseguiu. Steve Jobs foi conhecido pela forma como nunca desistia de tornar possível um novo artefacto, que muitos (todos, menos ele?) julgavam impossível, fosse ele um iPhone ou um Mac Book Air. E conseguiu.

Dedicação, trabalho árduo são as chaves para o sucesso. Há muitos anos, quando assistia em Paris a uma conferência por um dos pais fundadores da Inteligência Artificial, Marvin Minsky, ele começou a palestra dizendo: “Somos todos Einsteins”. Não é verdade, todos o sabemos. Mas o que é verdade, como disse um dia Lou Reed, ontem falecido, independentemente do nosso ponto de partida, “se praticares uma coisa, uma, outra e outra vez, é suposto ires melhorando.”

Programar não é uma actividade contemplativa.

Take a walk on the wild side!

domingo, 27 de outubro de 2013

Olhar e ver (III)

Já por várias vezes referi a importância de pegar num problema complexo e dominar essa complexidade dividindo o problema inicial em sub-problemas mais simples e/ou começando por resolver um problema semelhante mas mais simples que depois se adapta para resolver a questão inicial.
Isto é importante, claro, mas de nada serve se não tivermos conhecimentos sobre o domínio do problema (por exemplo, pode obrigar a usar conhecimentos de trigonometria).
Mas podemos saber muito de técnicas de resolução de problemas, ter conhecimentos sobre o domínio do problema e mesmo assim não resolver o problema porque não conhecemos a linguagem de programação.


Assim para programar bem são precisas, pelo menos três coisas: dominar a linguagem de programação, ter conhecimentos sobre o domínio do problema e dominar a arte de resolução de problemas. Faltando uma delas e o resultado nunca será bom, se é que se chega a algum resultado...


Neste post vou falar sobre como o modo de decompor um problema em sub-problemas pode ter várias soluções e se liga aos outros aspectos. Suponhamos que queremos desenhar a figura seguinte:
Numa primeira abordagem admitamos que o que eu vejo são segmentos de recta ligados entre si. Nesta perspectiva é fundamental ter uma primitiva que me permita desenhar segmentos de recta conhecidos os seus pontos extremos. Não é um problema difícil:
def linha(x_1,y_1, x_2,y_2):
    """ Desenha uma linha entre dois pontos."""
    # posiciona-se
    turtle.pu()
    turtle.goto(x_1,y_1)
    turtle.pd()
    # Desenha
    turtle.goto(x_2,y_2)
Precisei de usar conhecimentos básicos do modo turtle. Agora só necessito de identificar os oito pontos do desenho e desenhar os doze segmentos que unem alguns entre si.
Feito isso o resto é trivial do ponto de vista da programação. Mas identificar os pontos pode não ser simples se não tivermos alguma conhecimento de geometria. Mas apresentemos primeiro o código.
def bloco(posx, posy,lado_1, lado_2):
    """ Desenha um bloco geométrico."""
    # Auxiliares
    passo_x = lado_2 * math.cos(math.pi/6)
    passo_y = lado_2 * math.sin(math.pi/6)
    # Desenho
    linha(posx,posy, posx+lado_1, posy)
    linha(posx+lado_1, posy, posx+ lado_1, posy + lado_2)
    linha(posx+ lado_1, posy + lado_2,posx, posy + lado_2)
    linha(posx, posy + lado_2, posx, posy)
    
   
    linha(posx + passo_x,posy + passo_y, posx + passo_x+lado_1, posy + posy+passo_y)
    linha(posx+passo_x+lado_1, posy+posy+passo_y, posx+passo_x+ lado_1, posy+posy+passo_y + lado_2)
    linha(posx+passo_x+ lado_1, posy+posy+passo_y + lado_2,posx+passo_x, posy+posy+passo_y + lado_2)
    linha(posx+passo_x, posy+posy+passo_y + lado_2, posx+passo_x, posy+posy+passo_y)
    
    linha(posx, posy, posx+passo_x, posy + passo_y)
    linha(posx, posy+lado_2, posx+passo_x , posy+lado_2+passo_y)
    linha(posx + lado_1, posy, posx+lado_1+passo_x , posy+passo_y)
    linha(posx + lado_1, posy + lado_2, posx+lado_1+passo_x , posy+lado_2+passo_y)
    
    turtle.hideturtle()
A questão central, pelo menos para mim, foi a de definir o que designei por passo_x e passo_y que me obriga a saber um pouco de trigonometria. Temos que convir que o programa além de extenso não é muito legível. Talvez se possa fazer melhor no que diz respeito a este segundo aspecto, passando do conceito de coordenadas para o conceito de ponto. Fazendo isso chegamos a uma nova solução.
def linha_b(p_1, p_2):
    """ Desenha uma linha entre dois pontos."""
    # posiciona-se
    turtle.pu()
    turtle.goto(p_1[0],p_1[1])
    turtle.pd()
    # Desenha
    turtle.goto(p_2[0],p_2[1])
    
def bloco_b(posx, posy,lado_1, lado_2, angulo):
    """ Desenha um bloco geométrico."""
    # Auxiliares
    passo_x = lado_2 * math.cos(angulo)
    passo_y = lado_2 * math.sin(angulo)
    # Pontos
    p_1 = (posx, posy)
    p_2 = (posx + lado_1, posy)
    p_3 = (posx + lado_1 + passo_x,posy + passo_y)
    p_4 = (posx + passo_x, posy+passo_y)
    p_5 = (posx, posy+lado_2)
    p_6 = (posx + lado_1, posy + lado_2)
    p_7 = (posx + lado_1 + passo_x, posy + lado_2 + passo_y)
    p_8 = (posx + passo_x, posy + lado_2 + passo_y)
    
    # Desenho
    linha_b(p_1, p_2)
    linha_b(p_2, p_3)
    linha_b(p_3, p_4)
    linha_b(p_4, p_1)
    
  
    linha_b(p_5, p_6)
    linha_b(p_6, p_7)
    linha_b(p_7, p_8)
    linha_b(p_8, p_5)
    
    linha_b(p_1, p_5)
    linha_b(p_2, p_6)
    linha_b(p_4, p_8)
    linha_b(p_3, p_7)
    
    
    turtle.hideturtle()
Penso que estamos de acordo de que se trata de uma solução mais limpa. Notar que aproveitámos as oportunidade para acrescentar como parâmetro formal o ângulo que determina o valor dos passos (ver figura).
Mas voltemos a olhar para a figura e admitamos que afinal o que vemos são dois rectângulos ligados por segmentos. Vamos de novo construir as respectivas primitivas.
def rectangulo(posx, posy, lado_1, lado_2):
    """ Desenha um rectangulo com o canto inferior esquerdo em (posx, posy)."""
    # Posiciona-se
    turtle.pu()
    turtle.goto(posx,posy)
    turtle.pd()
    # desenha
    for i in range(2):
        turtle.fd(lado_1)
        turtle.left(90)
        turtle.fd(lado_2)
        turtle.left(90)
    turtle.hideturtle()
    
def linha(x_1,y_1, x_2,y_2):
    """ Desenha uma linha entre dois pontos."""
    # posiciona-se
    turtle.pu()
    turtle.goto(x_1,y_1)
    turtle.pd()
    # Desenha
    turtle.goto(x_2,y_2)
Agora, de novo, a questão que resta resolver é a gestão dos pontos.
def main_2(posx, posy, lado_1, lado_2):
    """ Desenha o bloco."""
    # auxiliares
    canto_2_x = lado_2 * math.cos(math.pi/6)
    canto_2_y = lado_2 * math.sin(math.pi/6)
    # rectangulos
    rectangulo(posx, posy, lado_1, lado_2)
    rectangulo(canto_2_x, canto_2_y, lado_1, lado_2)
    # linhas
    linha(posx, posy, canto_2_x,canto_2_y)
    linha(posx, posy+lado_2, canto_2_x,canto_2_y + lado_2)
    linha(posx + lado_1, posy, canto_2_x + lado_1,canto_2_y)
    linha(posx + lado_1, posy+lado_2, canto_2_x + lado_1,canto_2_y + lado_2)
E cá está muito claro: dois rectângulos e quatro segmentos. Mas como somos esquisitos,um dia olhando de novo para a figura achámos que afinal tudo o que víamos eram degenerações de losangos (degeneração no mesmo sentido de que um rectângulo é um quadrado degenerado...).

Vamos então resolver a questão de desenhar a dita forma.
def quase_losango(posx, posy, lado_1, lado_2, angulo, orientacao):
    # Posiciona-se
    turtle.pu()
    turtle.goto(posx,posy)
    turtle.pd()
    turtle.setheading(orientacao)
    # desenha 
    for i in range(2):
        turtle.fd(lado_1)
        turtle.left(angulo)
    
        turtle.fd(lado_2)
        turtle.left(180-angulo)
    turtle.hideturtle()
Isto resolvido, vamos ter o desenho de quatro quase-losangos (na realidade dois são mesmo losangos...).

def main_3(posx, posy, lado_1, lado_2):
    """ Desenha o bloco."""
    # auxiliares
    canto_2_x = lado_2 * math.cos(math.pi/6)
    canto_2_y = lado_2 * math.sin(math.pi/6)
    # quase losangos :pela ordem maior-frente, menor-esquerdo, menor-direito, maior-trás
    quase_losango(posx, posy, lado_1, lado_2, 30,0)
    quase_losango(posx, posy, lado_2, lado_2, 60,30)
    quase_losango(posx+lado_1, posy, lado_2, lado_2, 60,30)
    quase_losango(posx, posy+lado_2, lado_1, lado_2, 30,0)
Veja como tudo se simplificou. Mas não se esqueça de importar os módulos turtle e math! Agora pode dedicar-se a embelezar a solução. Por exemplo, colorindo-a...

Moral da História: para diferentes modos de olhar e ver, diferentes soluções. Todas são iguais (no produto final), mas há umas mais iguais do que outras. É muito importante aprender a ver e isso treina-se. Pode começar a treinar com as duas figuras abaixo. O que vê?

sábado, 26 de outubro de 2013

Acrónimos

Numa das aulas foi pedido um programa para resolver o problema de gerar um acrónimo a partir de uma cadeia de caracteres. Por exemplo, “Ernesto Jorge Fernandes Costa” deve dar como resultado “EJFC”. Os acrónimos devem ser sempre em maiúsculas. No caso de “random access memory” o resultado deverá ser “RAM”.
Alguns resolveram o problema de um modo simples: percorrer a cadeia à procura do espaço em branco, que separa as palavras, para escolher o caractere seguinte. Vamos ver o código.
def acronimo_alunos(cadeia):
    """Extrai o acrónimo da cadeia."""
    cadeia = cadeia.upper()
    acro = cadeia[0]
    for i in range(1, len(cadeia)-1):
        if cadeia[i] == ' ':
            acro = acro + cadeia[i+1]
    return acro
O programa começa por converter toda a cadeia para maiúsculas. De seguida começa por formar o acrónimo com o primeiro caractere da cadeia que, por definição, será sempre o primeiro caractere da primeira palavra. Dentro do ciclo procura o espaço em branco e vai buscar o caractere seguinte para juntar ao acrónimo. Testando com os dois exemplos acima dá o resultado esperado. Perfeito! Perfeito??
E se existirem caracteres brancos no início ou no final? E se existir mais do que um espaço em branco a separar as palavras? Se testar verificará que a sua solução cai por terra. Enfim, cai por terra no caso de ser possível acontecer o descrito. Vamos admitir que sim e procurar uma nova solução.
def acronimo(cadeia):
    """
    Constrói o acrónimo a partir de uma frase 
    representada por uma cadeia de caracteres.
    """
    acro = ''
    inicio = True
    for car in cadeia:
        if car == ' ':
            inicio = True
        elif inicio == True:
            acro += car.upper()
            inicio = False
    return acro
O que fizemos? Algo de muito simples: usamos um indicador booleano (os ingleses dizem uma flag - bandeira) para nos dizer quando estamos à procura do início de uma palavra. Isso é verdade enquanto não encontrarmos um caractere diferente do branco! Quando isso acontece juntamos o caractere depois de o converter para maiúsculas associando-o ao nome acro que funciona como um acumulador. Experimente agora e verá que funciona para todas as situações.
Existem outras soluções para o problema. A primeira que vamos mostrar introduz implicitamente conceitos que ainda não demos (no caso objectos do tipo lista, ako de tuplos mas mutáveis).
def acronimo(cadeia):
    acro = ''
    nova_cadeia = cadeia.strip().split()
    for pal in nova_cadeia:
        acro += pal[0].upper()
    return acro
Nesta solução começamos por tirar os espaços em branco nas extremidades esquerda e direita da cadeia (método strip) e dividimos o que fica na sequência das palavras que a constituem (método split). Agora, no ciclo for, apenas temos que ir buscar o primeiro caractere de cada palavra, passá-lo a maiúsculas e juntar tudo. Simples, não é?
A última solução que vamos apresentar utiliza o conceito de ciclo variável (ciclo while) que será discutido numa das próximas aulas.
def acronimo(frase):
    """ Forma um acronimo a partir da frase."""
    frase = frase.upper().strip()
    comprimento = len(frase)
    acron = ''
    posicao = 0
    while posicao < comprimento:
        acron = acron + frase[posicao].upper()
        while (posicao < comprimento) and (frase[posicao] != ' '):
            posicao = posicao + 1           
        while (posicao < comprimento) and (frase[posicao] == ' '):
            posicao = posicao + 1
    return acron 
Começamos por passar tudo para maiúsculas e retirar os espaços em branco nas extremidades. De seguida vamos percorrer posição a posição a cadeia. Sabemos que o primeiro caractere da cadeia sem os espaços na extremidade tem que fazer parte do acrónimo. Depois passamos por todos os caracteres significativos até aparecer um espaço em branco (primeiro ciclo while interior). De seguida passamos por cima dos espaços em branco (segundo ciclo while interior) e voltamos ao início do ciclo while principal. Se ainda existirem caracteres ele terá que ir de novo para o acrónimo pelo que o ciclo se repete.

Moral da História: (1) Nem sempre o que parece é! (2) Vários são os caminhos que nos levam a Roma!

segunda-feira, 21 de outubro de 2013

Erros, equívocos e outras singularidades (I)

Depois de corrigir testes e/ou exames tenho por hábito, com mais calma, olhar para as vossas respostas na procura dos problemas que ainda subsistem. Todos aprendem com os erros, mesmo com os erros dos outros. No caso dos docentes isso serve para procurar melhorar o modo como ensinam. Vou tentar referir aqui no blogue algumas das situações que nos podem ajudar a ser melhores programadores. Começo com o problema do teste da TP9 que pedia para calcular a Distância de Hamming entre duas cadeias de caracteres, isto é, o número de posições em que os caracteres das duas cadeias são diferentes. Vejamos uma das soluções que apresentaram. Trata-se de uma solução muito próxima da melhor solução.
def dist_ham(cad_1, cad_2):
    res = 0
    indice = 0
    if len(cad_1) > len(cad_2):
        return False
    for car in cad_1:
        if car != cad_2[indice]:
            res = res + 1
        indice = indice + 1
    return res
Este programa funciona para cadeias de igual comprimento. Mas o que acontece se a segunda cadeia for maior do que a primeira? Com esta solução, entra no ciclo na mesma quando me parece que a ideia era a de que não fosse assim: @ autor@ queria dizer que se fossem diferentes então devia dar False. Então, uma primeira correcção seria:
def dist_ham(cad_1, cad_2):
    res = 0
    indice = 0
    if len(cad_1) != len(cad_2):
        return False
    for car in cad_1:
        if car != cad_2[indice]:
            res = res + 1
        indice = indice + 1
    return res
Claro que, o que o enunciado pedia implicitamente era para tratar também esse caso, considerando que os caracteres em excesso também deviam ser contados como diferentes. Mas vamos esquecer esse detalhe e olhar para outro aspecto do código menos conseguido.

Nas aulas foi referido que num ciclo for as sequências podem percorridas por posição/índice ou por conteúdo. A opção depende do problema. No exemplo acima estão a ser usadas ambas! É evidente que temos que comparar caracteres e esse facto pode levar a pensar em percorrer as cadeias por conteúdo. Mas temos que comparar os caracteres de cada cadeia numa dada posição. E isso obriga a cada momento qual a posição dos caracteres que queremos comparar. Neste caso, é esta situação que deve prevalecer. Daí a nova versão:
def dist_ham(cad_1, cad_2):
    res = 0

    if len(cad_1) != len(cad_2):
        return False
    for indice in range(len(cad_1)):
        if cad_1[indice] != cad_2[indice]:
            res = res + 1
    return res
Uma vez mais, a solução acima não responde satisfatoriamente no caso das cadeias de comprimento diferente. Em post anterior já mostrei como se podia resolver o problema, mesmo quando as cadeias têm comprimentos diferentes.
Conclusão maior: não é boa prática misturar duas formas de percorrer uma sequência. Mas se tal for mesmo necessário há um modo Pythoniano de o fazer recorrendo à função enumerate. Exemplo:
>>> cadeia = 'abcdef'
>>> for indice, valor in enumerate(cadeia): # <-- enumerate!!
... print('indice %d:\tcaractere= %s' % (indice, valor))
...
indice 0: caractere=  a
indice 1: caractere= b
indice 2: caractere= c
indice 3: caractere= d
indice 4: caractere= e
indice 5: caractere= f
That’s it!

domingo, 20 de outubro de 2013

A Causa das Coisas (I)

Programar não é um acto isolado. Programamos muitas vezes em equipa e o que fazemos é para ser usado por nós e por quem necessitar. Agora que sabemos que em Python existem módulos que estendem a linguagem dando-nos novas possibilidades e que foram desenvolvidos por alguém por esse mundo fora, sabemos que é assim: os programas são feitos por muitos para serem usados por muitos. Esse facto tem consequência importantes para o modo como desenvolvemos e disponibilizamos o código. Ele deve estar correcto e ser eficiente. Claro. Também deve ser elegante, fácil de manter e adaptar. Seguramente. Mas, não menos importante, deve ser possível integrar o código noutros programas sem problemas.
Suponhamos que queremos desenvolver um programa para calcular a raiz quadrada de um número bem preciso, por exemplo 5. Sabemos que em Python isso não é problema pois alguém resolveu essa questão para nós, graças ao método sqrt do módulo math. Mas vamos admitir que não é assim. Vamos então à procura de um algoritmo. Fazemos uma pesquisa na internet recorrendo ao Google, e lá nos aparece o Método de Newton. Para calcular o valor aproximado da raíz basta iterar a fórmula:
x(n+1) = 1/2 * (x(n) + a/x(n))
onde x denota a raiz do número a. Ligamos o computador, usamos o nosso IDE preferido para Python, e escrevemos o programa.
x = 2.0

for i in range(10):
    x = 1/2 * (x + 5/x)   
print(x)
Executado o programa lá nos aparece o lindo valor de : 2.23606797749979, que compara excepcionalmente bem com o que se obtém usando math.sqrt(5). Mas se o número for 20 em vez de 5? Não há problema, alteramos ligeiramente o código.
x = 4.0

for i in range(10):
    x = 1/2 * (x + 20/x)    
print(x)
Uma vez mais o resultado é excelente. Mas, depois de pensarmos um bocado chegamos sem problema à conclusão que o melhor é escrever um pedaço de código único que possa ser utilizado para todas as situações e que minimize as alterações que são necessárias introduzir. Vamos a isso.
a = eval(input('Qual o número?  '))
x = a/2

for i in range(10):
    x = 1/2 * (x + a/x)    
print(x)
Agora cada vez que o código é executado pede ao utilizador o número. Podemos agora dormir descansados: sempre que for preciso nós calcularmos a raiz quadrada de um número é só executar este código. E como não somos egoístas, quando um amigo nosso teve o mesmo problema não tivemos dúvida em lhe passar o ficheiro raiz2.py com o código, dizendo-lhe que só precisava ou de mandar correr o ficheiro ou de importar o código, dependendo do que queria fazer.
Python 3.2.3 (default, Sep  5 2012, 20:52:27) 
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.1.00)]
Type "help", "copyright", "credits" or "license" for more information.
>>> import raiz2
Qual o número?  15
3.872983346207417
>>>
Como se vê, mal importou o módulo apareceu a pergunta, o nosso amigo lá colocou o número e aparece resplandecente o resultado. Perfeito!
Mas uns dias depois o amigo aparece-lhe de novo, muito triste. Queria usar o seu programa do cálculo da raíz quadrada como auxiliar de outro programa para calcular as raízes de um polinómio do segundo grau, e não via como. Ele até sabe a fórmula resolvente:
raiz_1 = (-b + raiz2(b**2 - 4*a*c))/ 2*a
raiz_2 = (-b - raiz2(b**2 - 4*a*c))/ 2*a
E fez mesmo um programa:
import raiz2

a = eval(input('Coeficiente de grau 2: '))
b = eval(input('Coeficiente de grau 1: '))
c = eval(input('Coeficiente de grau 0: '))

raiz_1 = (-b + raiz2(b**2 - 4*a*c))/ 2*a
raiz_2 = (-b - raiz2(b**2 - 4*a*c))/ 2*a

print(raiz_1, raiz_2)
Mas quando o programa é executado, ele começa logo por me pedir o número cuja raiz quero saber, só depois pede os coeficientes e no fim ainda me dá um erro que não se entende lá muito bem.
Python 3.2.3 (default, Sep  5 2012, 20:52:27) 
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.1.00)]
Type "help", "copyright", "credits" or "license" for more information.
[evaluate poli2.py]
>>> Qual o número?  13
3.6055512754639896
Coeficiente de grau 2: 2
Coeficiente de grau 1: 3
Coeficiente de grau 0: 4
Traceback (most recent call last):
  File "/Volumes/Work/__Aulas/_____Aulas 2013_2014/IPRP/Blogue_iprp/causas_das_coisas/poli2.py", line 9, in 
builtins.TypeError: 'module' object is not callable
Isto não faz sentido! É que o valor da raíz é função do valor dos coeficientes. Conhecido estes o valor cuja raíz queremos calcular fica determinado. Não somos nós que devemos entrar o valor ou que tem que fazer as contas! E colocar a importação depois de pedir os coeficientes também não resolve, porque o programa vai continuar a pedir o valor cuja raíz se pretende calcular quando isso, já vimos, não faz sentido.
E ainda há o erro! Mas tu percebes a razão do erro pois lembras-te bem como se pode usar o código (objectos e definições) que se encontra num módulo: tens que colocar o nome do módulo, seguido de um ponto, seguido do nome do objecto ou da definição. Mas neste teu caso como proceder se não há um nome associado ao código que calcula a raiz? Então vamos dar um nome, salvar tudo e voltar a executar. Vejamos as alterações ao teu programa:
def raiz_quadrada():
    a = eval(input('Qual o número?  '))
    x = a/2
    
    for i in range(10):
        x = 1/2 * (x + a/x)    
    print(x)
    

raiz_quadrada()
Tens agora a definição do algoritmo para o cálculo da raíz quadrada (def), seguida do seu uso (raiz_quadrada()). Testas e funciona. Passas de novo ao teu amigo,ele altera o seu código:
import raiz2


a = eval(input('Coeficiente de grau 2: '))
b = eval(input('Coeficiente de grau 1: '))
c = eval(input('Coeficiente de grau 0: '))

raiz_1 = (-b + raiz2.raiz_quadrada(b**2 - 4*a*c))/ 2*a
raiz_2 = (-b - raiz2.raiz_quadrada(b**2 - 4*a*c))/ 2*a

print(raiz_1, raiz_2)
e executa:
Python 3.2.3 (default, Sep  5 2012, 20:52:27) 
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.1.00)]
Type "help", "copyright", "credits" or "license" for more information.
[evaluate poli2.py]
>>>Qual o número?  17
4.123105625617661
Coeficiente de grau 2: 3
Coeficiente de grau 1: 4
Coeficiente de grau 0: 5
Traceback (most recent call last):
  File "/Volumes/Work/__Aulas/_____Aulas 2013_2014/IPRP/Blogue_iprp/causas_das_coisas/poli2.py", line 10, in 
builtins.TypeError: raiz_quadrada() takes no arguments (1 given)
Parece que está tudo na mesma. Começa por pedir um número para a raíz, e não devia, e depois de introduzires os coeficientes aparece outra vez uma mensagem de erro, embora diferente da anterior. Para o primeiro problema tens solução. Lembras-te de te dizerem que todos os ficheiros com código Python e que terminam com a extensão .py podem ser executados ou importados. Se não quisermos que durante a importação uma parte do código seja executada temos que a colocar essa parte dentro da instrução condicional:
if__name__ == ‘__main__’:
   *código_aqui*
Vamos alterar então de novo o código de raiz2.py:
def raiz_quadrada():
    a = eval(input('Qual o número?  '))
    x = a/2
    
    for i in range(10):
        x = 1/2 * (x + a/x)    
    print(x)
    
if __name__ == '__main__':
    print(raiz_quadrada())
E toca a executar o programa para as raizes do polinómio. Verificamos que o primeiro erro desapareceu mas o segundo mantém-se. Então qual é a questão?
Por um lado sabemos que o valor da raiz a calcular é função dos coeficientes e por isso é determinado internamente pelo programa e não por nós através de uma instrução de input. Por outro lado, quando o resultado é calculado não o queremos imprimir mas antes usar dentro de uma expressão mais geral (i.e., (-b + raiz2.raiz_quadrada(b**2 - 4*a*c))/ 2*a)). Por isso o que está mal com a nossa solução é o modo como raiz quadrada recebe o valor e o comunica. Quando não nos interessa a interacção com o utilizador humano o que temos a fazer é usar um parâmetro formal, para a entrada do dado, e return, para devolver o resultado, em vez de input e de print respectivamente. Vamos de novo alterar o código.
def raiz_quadrada(a):
    """Calcula a raiz quadrada aproximada de a, suposto um número positivo."""
    x = a/2
    for i in range(10):
        x = 1/2 * (x + a/x)    
    return x
    
if __name__ == '__main__':
    # Para testar
    num = eval(input('O número sff: '))
    print(raiz_quadrada(num))
Parece em condições de ser usado agora. Vejamos.
Python 3.2.3 (default, Sep  5 2012, 20:52:27) 
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.1.00)]
Type "help", "copyright", "credits" or "license" for more information.
[evaluate poli2.py]
Coeficiente de grau 2: 2
Coeficiente de grau 1: 16
Coeficiente de grau 0: 4
raiz 1 -1.033370
raiz_2 -30.966630
Fantástico!

Mas se olharmos para o código do programa para o cálculo do polinómio, não estamos a incorrer no mesmo erro? Isto é, se alguém quiser usar este código para calcular as raízes de um polinómio não vai ter o mesmo problema? Claro que sim! Então vamos resolver isto do mesmo modo.
import raiz2

def polinomio_2_grau(a,b,c):
    """ Calcula as raízes de um polinómio do segundo grau. Funciona para raízes não complexas."""
    raiz_1 = (-b + raiz2.raiz_quadrada(b**2 - 4*a*c))/ 2*a
    raiz_2 = (-b - raiz2.raiz_quadrada(b**2 - 4*a*c))/ 2*a
    return  (raiz_1, raiz_2)

if __name__ == '__main__':
    # Para testar
    a = eval(input('Coeficiente de grau 2: '))
    b = eval(input('Coeficiente de grau 1: '))
    c = eval(input('Coeficiente de grau 0: '))
    r_1, r_2 = polinomio_2_grau(a,b,c)
    print('raiz 1: %f\nraiz 2: %f' % (r_1, r_2))

Em conclusão. Existem (para já, pois veremos que há ainda outros) dois modos de introduzir dados (parâmetro formais ou instrução input) e dois modos de obter resultados (return ou instrução print). O que usamos depende do tipo do problema e do modo como o código tem que ser integrado, ou não com outro código. Um dia falaremos com mais detalhe sobre os programas em que a interacção com o utilizador humano é muito grande (jogos, por exemplo), e como devemos proceder. 

sábado, 19 de outubro de 2013

Problemas 3.15, 3.16 e 3.17

Se percebemos a solução para o Problema 3.12 então estes também não oferecem grande dificuldade. Comecemos pelo ... primeiro.
import turtle

def adn_tartaruga(tartaruga, adn):
    """ Simula o comportamento da tartaruga ditado pelo seu ADN."""
    for car in adn:
        if car == 'f':
            tartaruga.fd(50)
        elif car == 't':
            tartaruga.bk(50)
        elif car == 'd':
            tartaruga.rt(45)
        else:
            tartaruga.lt(45)

if __name__ == ‘__main__’:
    tarta = turtle.Turtle()
    adn_1 = ‘ffttedtftedf’
    adn_tartaruga(tarta,adn_1)
    turtle.exitonclick()
 
O que é que a solução tem de especial? Muito pouco. Os valores do movimento e das rotações são fixos. Por outro lado, usamos o construtor do módulo turtle para definir uma tartaruga concreta em vez de usar a tartaruga genérica. Finalmente, o ADN da tartaruga foi por nós definido “à mão” . A primeira variante, Problema 3.16, pede-nos que os valores do movimento e das rotações não sejam fixos mas possam variar dentro de valores razoáveis.
import random

def adn_tartaruga_alea(tartaruga, adn):
    """ Simula o comportamento da tartaruga ditado pelo seu ADN."""
    for car in adn:
        lado = random.randint(20,100)
        angulo = random.randint(10,180)
        if car == 'f':
            tartaruga.fd(lado)
        elif car == 't':
            tartaruga.bk(lado)
        elif car == 'd':
            tartaruga.rt(angulo)
        else:
            tartaruga.lt(angulo)
Como se observa a adaptação às novas condições foi fácil. Finalmente, no problema 3.17, apenas nos é pedido que o ADN da tartaruga seja ele próprio gerado aleatoriamente. Isso foi o que aprendemos a fazer com a solução ao Problema 3.12.
import random

def adn_tartaruga_total(tartaruga, passos):
    """ Simula o comportamento da tartaruga em função do seu ADN. O dito
    ADN é gerado aleatoriamente."""
    adn =''
    for i in range(passos):
        adn = adn + random.choice('fted')
    adn_tartaruga_alea(tartaruga,adn)


def adn_tartaruga_alea(tartaruga, adn):
    """ Simula o comportamento da tartaruga ditado pelo seu ADN."""
    for car in adn:
        lado = random.randint(20,100)
        angulo = random.randint(10,180)
        if car == 'f':
            tartaruga.fd(lado)
        elif car == 't':
            tartaruga.bk(lado)
        elif car == 'd':
            tartaruga.rt(angulo)
        else:
            tartaruga.lt(angulo)

That's it!

Problema 3.10

Vamos resolver o problema 3.10 do livro que nos pede para gerar uma cadeia de ADN. O único argumento/parâmetro formal é o tamanho da sequência pretendido. Trata-se de um exercício muito fácil, mas que nos pode dar alguns ensinamentos.
A ideia da solução passa por repetir tantas vezes quantas o tamanho da sequência a escolha aleatória de uma das bases presentes na cadeia de ADN, de entre as quatro possíveis (Adenina (A), Citosina (C), Guanina (G) e Timina (T). Os resultados vão sendo acumulados. Daí a solução.
import random

def gera_adn(tam):
    """Gera uma cadeia de ADN de tamanho tam. Padrão ciclo-acumulador"""
    adn =''
    for i in range(tam):
        base = random.choice('TACG')
        adn = adn + base
    return adn
Baseia-se esta solução no padrão ciclo (neste caso ciclo for) e acumulador (o nome adn onde vamos acumulando os resultados).
Suponhamos agora uma situação semelhante. Alguém anda perdido numa cidade, perfeitamente geométrica, e pede ajuda para se deslocar a um dado local da cidade. Recebe ajuda na forma de uma sequência de indicações do tipo Avança (A), Recua (R), Vira à Esquerda (E) ou Vira à Direita (D). Admitamos que o que se pretende é um programa que gere sequências válidas de indicações, de tamanho variável. O leitor não estranhará, depois de uma breve reflexão, que a solução proposta não lhe ofereça grandes dúvidas.
mport random
import random
def gera_comandos(tam):
    """
    Gera uma sequências de indicações de movimento numa cidade de tamanho tam.
    Padrão ciclo-acumulador
    """
    seq_comandos =''
    for i in range(tam):
        comando = random.choice('ARED')
        seq_comandos = seq_comandos + comando
    return seq_comandos
Olhando para estas soluções verificamos que o que as distingue é apenas os caracteres possíveis nos dois casos. Podemos então abstrair esse detalhe, e construir uma solução genérica que sirva para gerar qualquer sequência de caracteres, conhecido o alfabeto dos caracteres possíveis. Vamos a isso.
import random

def gera_comandos(tamanho, alfabeto):
    """ Gera uma sequência de comandos com elementos retirados aleatoriamente do alfabeto."""
    comandos = ''
    for i in range(tamanho):
        comandos = comandos + random.choice(alfabeto)
    return comandos
Mais uma vez verificamos que generalizar significa transformar uma constante (no caso a cadeia de ADN ou de indicações) num nome que aparece como parâmetro formal. Mas será que as versões concretas devem ser deitadas fora? Não necessariamente. Suponha que tem indicações sobre a probabilidade de ocorrência de um dos eventos. Como incorporar isso no programa. Vamos ver para o caso da indicação do percurso. Se a pessoa for competente então é provável que dê mais indicações de Avançar do que de Recuar e que privilegie também uma das indicações de Virar. Vamos ver como podemos incorporar essa indicação no código.
import random

def gera_comandos(n):
    """Gera n comandos aleatoriamente. Alguns movimentos são mais prováveis do que outros."""
    comandos = ''
    for i in range(n):
        if random.choice([0,0,0,1]) == 0:
            comandos += random.choice(['A','A', 'A','A','R'])
        else:
            comandos += random.choice(['E',’E’, 'D'])
    return comandos
Neste exemplo, avançar e recuar globalmente têm 75% de probabilidades de ocorrer, enquanto virar tem apenas 25%. Dentro de cada caso também há diferenças: avançar tem 80% de probabilidade de ocorrer e virar à esquerda 66%.

sexta-feira, 18 de outubro de 2013

Teste 1 - TP9

Pergunta 1 O espaço dos objectos é o local da memória onde estão guardados os objectos. Em particular, os atributos valor e tipo. O espaço dos nomes é o local da memória onde estão armazenados os nomes que foram associados a objectos através de uma atribuição explícita (caso da instrução de atribuição =) ou implícita (caso de uma definição (def) ou da importação de um módulo (import). Visualmente temos a seguinte situação para cada um dos três casos referidos.
Pergunta 2 Olhando para a cara o que vemos? Que é composta a partir de quadrados e rectângulos coloridos. E os quadrados são um caso particular de rectângulos. Então só precisamos de uma função auxiliar.
def rect_cor(posx,posy,lado1,lado2,cor):
    # Atributos
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    turtle.fillcolor(cor)
    turtle.begin_fill()
    for i in range(4):
        if i%2 == 0:
            turtle.forward(lado1)
        else:
            turtle.forward(lado2)
        turtle.left(90)
    turtle.end_fill()
    turtle.hideturtle()
Com esta implementação podemos colocar o rectângulo em qualquer posição e com qualquer cor!

A partir do momento em que temos esta possibilidade o resto é apenas calcular as posições e definir a cor de cada rectângulo ou quadrado. Numa solução simples, e mais não era pedido, podemos assumir que a cara fica sempre na mesma posição, no caso da nossa implementação no ponto (0,0). Notar que este ponto corresponde ao canto inferior esquerdo da cara.
def cara():
    # cabeça
    rect_cor(0,0,120,120,'white')
    # olhos
    rect_cor(30,85,20,20,'blue')
    rect_cor(70,85,20,20,'blue')
    # nariz
    rect_cor(55,30,10,35,'red')
    # boca
    rect_cor(50,15,20,10,'yellow')
    # orelhas
    rect_cor(-10,60,10,30,'red')
    rect_cor(120,60,10,30,'red')
Pergunta 3 Pedem-nos para comparar duas cadeias de caracteres posição a posição e determinar em quantas os respectivos caracteres são diferentes. Chama-se a isto a Distância de Hamming. Vamos esquecer para já o problema de tamanhos diferentes e assumimos que têm o mesmo comprimento. Uma solução simples é percorrer as duas cadeias por posição e comparar.
def hamming_basico(cadeia_1, cadeia_2):
    """Calcula a distância de Hamming de duas cadeias."""
    distancia = 0
    for i in range(tamanho_menor):
        if cadeia_1[i] != cadeia_2[i]:
            distancia = distancia + 1
    return distancia   
Ter feito isto já valia bastante. Agora o problema do tamanho. Por definição os caracteres em excesso da cadeia maior são diferentes dos da outra (que não existem!). Logo só temos que somar a dirença. Por outro lado temos que ter o cuidado de percorrer as duas cadeias um número de vezes igual ao tamanho da de menor comprimento. Logo, usando apenas funções mencionadas nas aulas:
def hamming(cadeia_1, cadeia_2):
    """Calcula a distância de Hamming de duas cadeias."""
    tamanho_menor = min(len(cadeia_1), len(cadeia_2))
    distancia = 0
    for i in range(tamanho_menor):
        if cadeia_1[i] != cadeia_2[i]:
            distancia = distancia + 1
    tamanho_maior = max(len(cadeia_1), len(cadeia_2))
    distancia = distancia + (tamanho_maior - tamanho_menor)
    return distancia
Em alternativa podíamos ainda fazer:
def hamming_b(cadeia_1, cadeia_2):
    """Calcula a distância de Hamming de duas cadeias."""
    tamanho_menor = min(len(cadeia_1), len(cadeia_2))
    distancia = 0
    for i in range(tamanho_menor):
        if cadeia_1[i] != cadeia_2[i]:
            distancia = distancia + 1
    distancia = distancia + abs(len(cadeia_1)- len(cadeia_2))
    return distancia
A função abs dá-nos o módulo do número dado como argumento: abs(-5) e abs(5) são ambos iguais a 5. Este exemplo e a sua solução ilustram o poder das abstracção que permite a escrita de programas modulares e ainda a técnica de encontrar primeiro uma solução para um problema mais simples e depois completar a solução.

Teste 1 - TP3

Pergunta 1 Uma cadeia de caracteres é uma sequência (tem ordem), homogénea (cada elemento é um caracter) e imutável (não é possível alterar o seu valor). Pergunta 2 Estamos perante mais um caso de olhar e ver. E o que vemos? Uma flor formada por pétalas. E o que são as pétalas: rectângulos coloridos. Precisamos então um programa para desenhar rectângulos. Depende do valor de cada lado e da cor. Mas também vemos que os quadrados têm uma posição. Daí o programa.
def rect_cor(posx,posy,lado1,lado2,cor):
    # Atributos
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    turtle.fillcolor(cor)
    turtle.begin_fill()
    for i in range(4):
        if i%2 == 0:
            turtle.forward(lado1)
        else:
            turtle.forward(lado2)
        turtle.left(90)
    turtle.end_fill()
    turtle.hideturtle()
Pensemos agora na flor. O que vemos são várias pétalas, todas da mesma cor, que dão uma volta completa de 360 graus. Se usar como parâmetro o número de pétalas então vou repetir a acção de desenhar uma pétala tendo o cuidado de ir actualizando a orientação. Mas de quanto? Para que dê uma volta completa com as pétalas igualmente espaçadas ... 360 a dividir pelo número de pétalas!
def flor(num_petalas, posx,posy,lado1,lado2,cor):
    """Desenha uma flor cujas pétalas são rectângulos coloridos."""
    for i in range(num_petalas):
        rect_cor(posx,posy,lado1,lado2,cor)
        turtle.right(360/num_petalas)
Problema resolvido. Antes de passar ao ultimo problema vamos ver se conseguimos fazer melhor. afinal uma pétala rectangular não é muito realista. E que tal esta?
def petala(posx, posy, raio, angulo,cor):
    """ Desenho de uma pétala."""
    # Atributos
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    turtle.fillcolor(cor)
    turtle.begin_fill()
    # Desenha
    turtle.circle(raio,angulo)
    turtle.setheading(turtle.heading() + 180 - angulo)
    turtle.circle(raio,angulo)
    turtle.setheading(turtle.heading() + 180 - angulo)
    # Termina
    turtle.end_fill()
    turtle.hideturtle()
A tarefa mais complicada na solução acima foi a gestão da orientação da tartaruga. Na solução apresentada garantimos que a orientação inicial e final coincidem. E agora o programa principal.
def flor_geral(num_petalas,posx,posy, raio, angulo,cor):
    """Desenha uma flor."""
    for i in range(num_petalas):
        petala(posx,posy,raio, angulo,cor)
        turtle.right(360/num_petalas)    
Muito semelhante ao anterior. Executando o código para valores aceitáveis podemos desenhar lindas flores.
Pergunta 3 Temos uma cadeia de caracteres que pode conter ‘a’s ‘b’s e ‘c’s. Mas só queremos contar os dois primeiros e saber se aparecem em igual número. Como podemos contar o número de vezes que um caracter aparece numa cadeia? Percorrendo a cadeia, caractere a caractere, e somando um, sempre que for igual ao caractere de referência:
def conta_car(caractere, cadeia):
    """ Número de ocorrências de caractere em cadeia."""
    conta = 0
    for car in cadeia:
        if car == caractere:
            conta = conta +1
    return conta
Agora o nosso problema original é trivial: contamos para um caso, contamos para o outro e comparamos.
def igual(car_1, car_2,cadeia):
    conta_1 = conta_car(car_1, cadeia)
    conta_2 = conta_car(car_2, cadeia)
    return conta_1 == conta_2
Quem conhecer os métodos que existem para as cadeias de caracteres acha que perdemos tempo a implementar a função de contagem conta_car. Não precisamos dela: bastava fazer apenas
def igual_c1_c2(car_1,car_2,cadeia):
    conta_1 = cadeia.count(car_1)
    conta_2 = cadeia.count(car_2)
    return conta_1 == conta_2
Trata-se de uma solução genérica e elegante. Mas também era aceitável uma solução para o caso específico de ‘a’s e de ‘b’s e sem modularizar. Por exemplo:
def igual_a_b(cadeia):
    """
    Numa cadeia em que existem 'a', 'b' e 'c' 
    verifica se o número de 'a's é igual ao número de 'b's.
    """
    conta_a = 0
    conta_b = 0
    for car in cadeia:
        if car == 'a':
            conta_a = conta_a + 1
        elif car == 'b':
            conta_b = conta_b + 1
    return conta_a == conta_b
Qual das versões é melhor? Antes de responder imagine uma nova situação: em vez de apenas dois caracteres eram 5. Que solução preferia agora?

quinta-feira, 17 de outubro de 2013

Exercícios 3.12, 3.13, 3.14 (ako of Pi) do livro

O capítulo 3 do livro da cadeira tem vários problemas envolvendo cadeias de caracteres. Vamos apresentar a solução para alguns, todos eles muito semelhantes e que permitem exercitar a operação de fatiamento (slicing) e o iterável range. O primeiro (Exercício 3.12) pede-nos para dada uma cadeia e um inteiro positivo mostrar todas as sub-cadeias da cadeia de comprimento igual ao número.
def sub_cadeias(pal, n):
    """
    Todas as sub-cadeias de comprimento n.
    """
    for i in range(len(pal) - n + 1):
        print(pal[i:i + n])
Este exemplo mostra como podemos aceder a um pedaço de uma cadeia usando a operação cadeia[x:y]. Recordar que o último índice não conta. Assim, se tivermos, por exemplo cadeia[2:5] acedemos à sequência formada pelos caracteres nas posições 2,3 e 4. Na solução acima,atente no valor do índice mais elevado. Tal acontece devido ao facto das sub-cadeias terem que ter comprimento n. Antes de passarmos aos exemplos seguintes pensemos numa variante deste problema: queremos dividir a cadeia em pedaços de tamanho n.
def divide_cadeia(pal,n):
    """ divide a cadeia em pedações de tamanho n."""
    for i in range(0,len(pal) - n + 1,n):
        print(pal[i:i+n])
        
if __name__ == '__main__':
    divide_cadeia('Monty Python',3)
Como se pode ver não precisámos de muita mudança. Bastou alterar o controlo do ciclo passando a usar o iterável range com três argumentos. range(n_1, n_2, n_3) gera a sequência de inteiros a começar em n_1 (inclusivé), a terminar em n_2 (exclusivé) saltando de n_3 em n_3 elementos. Mas o que acontece se o número de caracteres não fôr divisível por n? Bom, com esta solução os caracteres do último pedaço, mais pequeno perdem-se... Mas quisermos mesmo ver também o pedaço mais pequeno?
def divide_cadeia_b(pal,n):
    """ divide a cadeia em pedações de tamanho n."""
    for i in range(0,len(pal) - n + 1,n):
        print(pal[i:i+n])        
    resto = len(pal)%n
    if resto:
        print(pal[-resto:])
Como se pode ver primeiro tentamos saber se é divisível (linha 5). Se não for (teste da linha 6), imprimimos o resto da palavra (linha 7). Fácil, certo?

O segundo, exercício 3.13, pede-nos todos os prefixos de uma cadeia de caracteres.
def prefixos(pal):
    """
    Mostra os prefixos da palavra.
    """
    for i in range(len(pal)):
        print(pal[:i+1])
Usamos [:i+1] porque se pretendem os prefixos que começam no início (:) e porque a própria palavra é prefixo de si própria (i+1).

Por fim, o exercício 3.14, destina-se a escrever um programa que mostre todos os sufixos de umas cadeia de caracteres.
def sufixos(pal):
    """
    Mostra os sufixos da palavra.
    """
    for i in range(len(pal),-1,-1):
        print(pal[i:])
O que temos a destacar aqui é o recurso ao iterável range com três argumentos. No caso, porque pretendemos os sufixos, caminhamos do fim da cadeia (len(pal)) para o início. Usamos -1 como condição de fim porque já sabemos que o último não é considerado, logo termina no elemento na posição 0! O salto é de -1 em -1 por razões óbvias.

quarta-feira, 16 de outubro de 2013

Olhar e ver (II) ou um farol sem faroleiro...

Acontecem coisas fantásticas nos testes como, por exemplo ter que desenhar um lindo farol como o da figura.
Quando olhamos para o problema diante de nós ele parece muito complexo. Então para melhor dominar a complexidade vamos dividir o nosso problema em sub-problemas e vamos começar por esquecer alguns detalhes. Depois, com calma, havemos de chegar ao resultado pretendido, ou mesmo superar o que era pedido! Então, ao olhar para o desenho, o que vemos? Um farol formado por duas componentes: uma torre e uma luz no cimo da torre. Elas não são completamente independentes pois a posição da torre condiciona a posição da luz. Mas vamos esquecer isso tudo para já e pensar apenas na decomposição.
""" Farol."""

import turtle

def farol():
    # torre
    # luz
    pass

if __name__ == '__main__':
    pass
É bizarro mas este programa até pode ser executado... embora não faça nada. Vamos agora concentrarmo-nos no desenho da torre. Uma vez mais, quando olhamos o que vemos? Elementos com a forma de quadrados uns em cima dos outros. E que mais? Bom, o tamanho do quadrado diminui à medida que se sobe e as cores vão alternando entre o vermelho e o branco. Significa isto que se tivermos código que nos permita desenhar um quadrado, na posição da tartaruga, com um dado determinado e uma certa cor podemos ter a nova vida facilitada. Vamos então a isso e fixemos a cor vermelha.
    """ Desenha um quadrado vermelho de um certo tamanho numa dada posição."""
    # Prepara 
    turtle.fillcolor(‘red’)
    turtle.begin_fill()
    # Desenha quadrado
    for i in range(4):
        turtle.forward(tamanho)
        turtle.left(90) 
    turtle.end_fill()
Este código funciona um pouco como um bloco construtor. Vamos usá-lo então para desenhar a torre. Não nos custa perceber que vamos repetir um certo número de vezes o desenho do quadrado. Admitamos para já que nos esquecemos das imposições do problema e nos concentramos apenas na altura.
import turtle

def farol(altura,lado):
    # torre
    for i in range(altura):
        # Desenha quadrado vermelho
        turtle.fillcolor('red')
        turtle.begin_fill()
        for i in range(4):
            turtle.forward(lado)
            turtle.left(90)
        turtle.end_fill()
    # luz
    pass

if __name__ == '__main__':
    farol(3,50)
    turtle.exitonclick()
Como se vê começam a aparecer os parâmetros formais (altura, tamanho do lado). Usando o bloco construtor embebido num ciclo for mandamos desenhar a torre. Mas se corrermos o código o resultado é decepcionante. Em vez de uma torre um único quadrado. Porquê? É óbvio que nos esquecemos de dizer que a posição dos sucessivos quadrado vai mudando. Como? Bom em relação ao eixo dos xx (horizontal) é igual, mudando apenas a coordenada dos yy por um valor igual ao lado. Lembre-se: estamos para já a esquecer que o lado varia em comprimento em função da altura!!! Alteremos então o código.
import turtle

def farol(altura,lado, posx,posy):
    # torre
    vai_para(posx, posy)
    for i in range(altura):
        # quadrado colorido
        turtle.fillcolor('red')
        turtle.begin_fill()
        for i in range(4):
            turtle.forward(lado)
            turtle.left(90)
        turtle.end_fill()
        vai_para(turtle.xcor(), turtle.ycor() + lado)
    # luz
    pass

def vai_para(posx,posy):
    """ Vai para (posx, posy) sem deixar rasto."""
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()

if __name__ == '__main__':
    farol(5,50,0,0)
    turtle.exitonclick()
        
E o novo resultado: Notar que introduzimos um sub-programa (vai_para) para colocar a tartaruga na posição desejada sem deixar rasto enquanto se movimenta. O que pode fazer uma simples alteração do código!!! Só é pena é ser tudo da mesma cor... Então é tempo de pensar um pouco nisso. Numeremos os quadrados como fazemos com as sequências: o da base é o 0, o seguinte o 1 e por aí adiante. Se queremos as cores alternadas então fixemos o vermelho para as posições pares e o branco para as ímpares. Modifiquemos o código por forma a testar se a posição é par ou é ímpar.
def farol(altura,lado, posx,posy):
    # torre
    vai_para(posx, posy)
    for i in range(altura):
        # quadrado colorido
        if i % 2 == 0: # <-- par?
            turtle.fillcolor('red')
        else:
            turtle.fillcolor('white')
        turtle.begin_fill()
        for i in range(4):
            turtle.forward(lado)
            turtle.left(90)
        turtle.end_fill()
        vai_para(turtle.xcor(), turtle.ycor() + lado)
    # luz
    pass
Façamos o boneco de novo.
Fantástico, não é? Agora ... faça-se luz. Vamos escrever código para desenhar um semi-círculo amarelo com um dado raio, no topo da torre e acrescentar ao programa.
def farol(altura,lado, posx,posy):
    # torre
    vai_para(posx, posy)
    for i in range(altura):
        # quadrado colorido
        if i % 2 == 0:
            turtle.fillcolor('red')
        else:
            turtle.fillcolor('white')
        turtle.begin_fill()
        for i in range(4):
            turtle.forward(lado)
            turtle.left(90)
        turtle.end_fill()
        vai_para(turtle.xcor(), turtle.ycor() + lado)
    # luz
    turtle.forward(lado)
    turtle.setheading(90) # <--- Atenção!
    turtle.fillcolor('yellow')
    turtle.begin_fill()
    turtle.circle(lado/2,180)
    turtle.end_fill()
Toca a executar.
Notar como foi resolvido o problema da orientação do semi-círculo. Quase pronto. Falta o detalhe dos quadrados a decrescer de um certo valor, a que chamaremos decremento. Pensemos um pouco. Se cada vez que subimos um degrau o lado decresce de um valor (decremento) e os quadrados têm que estar centrados, onde devemos colocar a tartaruga a cada etapa? E como mudamos o valor do lado? Em que zona do código fazemos as alterações? Não é difícil perceber que só temos que fazer avançar o valor da coordenada xx e diminuir o tamanho do lado. Eis a nova solução.
def farol(altura,lado, posx,posy, decremento):
    # torre
    vai_para(posx, posy)
    for i in range(altura):
        # quadrado colorido
        if i % 2 == 0:
            turtle.fillcolor('red')
        else:
            turtle.fillcolor('white')
        turtle.begin_fill()
        for i in range(4):
            turtle.forward(lado)
            turtle.left(90)
        turtle.end_fill()
        vai_para(turtle.xcor() + decremento/2, turtle.ycor() + lado) # <-- Eureka!
        lado = lado - decremento # <--- Eureka (2)
        
    # luz
    turtle.forward(lado)
    turtle.setheading(90)
    turtle.fillcolor('yellow')
    turtle.begin_fill()
    turtle.circle(lado/2,180)
    turtle.end_fill()
    turtle.hideturtle()
 
E já está!!! Fácil, não é?? Notar o aparecimento do novo parâmetro decremento e a instrução para esconder a tartaruga no final. O boneco é igual ao primeiro que mostrámos...
Vamos à moral da história. Um problema relativamente difícil pode tornar-se mais simples dividindo-o em sub-problemas mais simples e abordando as especificações em sequência. Muitas vezes (sempre?) quando acabamos de resolver um problema olhamos para o código e pensamos: posso melhorar o código (elegância, legibilidade,...), posso usá-lo noutros problemas? Uma modificação simples que podemos fazer é alterar o código para que as cores não sejam fixas. Uma espécie de farol UCB.
import turtle

def farol(altura,lado, posx,posy, decremento, cor_1,cor_2,cor_3):
    # torre
    vai_para(posx, posy)
    for i in range(altura):
        # quadrado colorido
        if i % 2 == 0:
            turtle.fillcolor(cor_1)
        else:
            turtle.fillcolor(cor_2)
        turtle.begin_fill()
        for i in range(4):
            turtle.forward(lado)
            turtle.left(90)
        turtle.end_fill()
        vai_para(turtle.xcor() + decremento/2, turtle.ycor() + lado)
        lado = lado - decremento
        
    # luz
    turtle.forward(lado)
    turtle.setheading(90)
    turtle.fillcolor(cor_3)
    turtle.begin_fill()
    turtle.circle(lado/2,180)
    turtle.end_fill()
    turtle.hideturtle()

def vai_para(posx,posy):
    """ Vai para (posx, posy) sem deixar rasto."""
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()

if __name__ == '__main__':
    farol(5,100,0,0,10,'blue', 'green', 'orange')
    turtle.exitonclick()

Ei-lo:
That’s all folks!!

Estar em voga...

Pretendemos retirar as vogais que aparecem num dado texto. Vimos nas aulas uma forma simples de o fazer:
def elimina_vogais(texto):
    """ Elimina as vogais do texto."""
    vogais ='aeiouAEIOU'
    novo_texto = ''
    for car in texto:
        if car not in vogais:
            novo_texto = novo_texto + car
    return novo_texto
Como sempre em programação existem alternativas de solução. Algumas requerem que se conheça bem a linguagem que estamos a usar, no nosso caso Python. Isso permite-nos chegar a uma solução extremamente simples:

def elimina_vogais(texto):
    """ Retira as vogais numa cadeia, substituindo-as por espaços em branco."""
    vogais = 'aeiou'
    for car in vogais:
        texto =  texto.replace(ch,' ') 
    return texto

Como se pode ver agora usamos as vogais para conduzir o processo de eliminação. Isto evita ter que andar num longo percurso do texto, caracter a caracter, e fazer um processo de selecção com um if como na primeira solução. Note-se também que enquanto no primeiro caso usamos uma variável auxiliar (novo_texto), no segundo caso não. Esta solução é fácil de tornar mais geral. Por exemplo, se quisermos um programa que elimine um subconjunto de caracteres quaisquer basta alterar uma instrução e uns pequenos ajustes, ou, melhor ainda, passar esses caracteres através de um parâmetro formal.

def elimina_caracteres(texto, caracteres):
    """ Retira os caracteres  numa cadeia, substituindo-as por espaços em branco."""
    for car in caracteres:
        texto = texto.replace(ch,' ') 
    return texto
Dentro desta ideia de generalizar podemos tentar com a primeira solução. Exemplo.
def elimina_caracteres_d(texto, funcao):
    """ 
    Elimina os caracteres que satisfazem uma condição dada pela funcao. 
    A funcao aplica-se a um caractere dando um resultado booleano: é um filtro!
    """
    novo_texto = ''
    for car in texto:
        if not funcao(car):
            novo_texto = novo_texto + car
    return novo_texto


def f(car):
    return (44 <= ord(car) <= 75)

def vogue(car):
    return car in 'aeiouAEIOU'
Agora usamos uma função genérica que se aplica a caracteres dando um resultado booleano. No exemplo apresentamos duas funções possíveis. Como vê a sua imaginação é o limite.

terça-feira, 15 de outubro de 2013

Dominar a cor

Numa das últimas aulas resolvemos o problema de desenhar quatro quadrados concêntricos, com o bordo com uma cor pré-determinada.

import turtle

def quadrado(posx, posy,lado,cor):
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    turtle.pencolor(cor)
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.hideturtle()     
        
def quadrados(posx, posy,lado,incremento):
    quadrado(posx,posy,lado,'red')
    quadrado(posx-incremento/2,posy - incremento/2,lado+incremento,'blue')
    quadrado(-20,-20,60,'green')
    quadrado(-30,-30,80,'black')
        
if __name__ == '__main__':
    quadrados()
    turtle.exitonclick()
Trata-se de uma solução simples e modular que tem no seu centro a capacidade de desenhar quadrados numa dada posição, com um dado lado e cor. Não é muito interessante como solução. Em particular estamos a chamar em sequência a função quadrado tendo o cuidado de calcular os seus argumentos. E se forem dez e não quatro. Vamos ver como podemos escolher as cores, guardando-as num tuplo para posterior escolha. Apresentamos dois casos. As cores são escolhidas por ordem e quase se esgotam voltamos ao início. Ou as são escolhidas aleatoriamente de entre as possíveis.
import turtle
import random

def quadrado(posx,posy,angulo,lado, cor):
    # Prepara
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    turtle.setheading(angulo)
    turtle.pencolor(cor)
    # Desenha
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.hideturtle()
    
def quad_concent(num, lado, incremento, posx,posy, angulo):
    # Prepara
    turtle.penup()
    turtle.goto(posx, posy)
    turtle.pendown()
    turtle. setheading(angulo)
    # Cores
    cores = ('red','green','blue','yellow','pink','orange','black')
    for i in range(num):
        cor = cores[i%len(cores)] # <-- Percebe para que serve?
        quadrado(posx,posy,angulo,lado,cor)
        posx = posx - incremento/2
        posy = posy - incremento/2
        lado = lado + incremento
  

    
def quad_concent_b(num, lado, incremento, posx,posy, angulo):
    # Prepara
    turtle.penup()
    turtle.goto(posx, posy)
    turtle.pendown()
    turtle. setheading(angulo)
    # Cores
    cores = ('red','green','blue','yellow','pink','orange','black')
    for i in range(num):
        indice = random.randint(0,len(cores)-1)
        cor = cores[indice]
        quadrado(posx,posy,angulo,lado,cor)
        posx = posx - incremento/2
        posy = posy - incremento/2
        lado = lado + incremento
        
        
        
if __name__ == '__main__':
    #quad_concent(10,30,15,0,0,0)
    quad_concent_b(10,30,15,0,0,0)
    turtle.exitonclick()
O código ilustra as duas possibilidades. Experimente você mesmo não se esquecendo de explorar as diferentes alternativas.

segunda-feira, 14 de outubro de 2013

Importa-se de repetir?

Importa-se de repetir? Aprendemos nas aulas que quando temos um conjunto de instruções que se repete um certo número de vezes recorremos a uma instrução de controlo repetitiva. Mas concretamente, temos vindo a exercitar para estas situações o uso do ciclo for. Nas sua versão mais simples toma a forma de:
for _nome> in _iterável:
   _instruções
A semântica desta construção é fácil de perceber. Enquanto existirem elementos em iterável vai buscar um e associa-o ao nome. De seguida, executa as instruções num ambiente com a associação nome elemento definida. Vejamos exemplos concretos.
>>> for i in range(3):
...     print(i)
... 
0
1
2
>>> for i in range(len('abc')):
...     print('abc'[i])
... 
a
b
c
>>> for car in 'abc':
...     print(car)
... 
a
b
c
>>> 
No primeiro e segundo exemplos o nome i funciona na prática como um contador que toma os sucessivos valores de 0,1 e 2. No terceiro exemplo, vamos extraindo um a um os elementos da cadeia de caracteres, primeiro ‘a’, depois ‘b’ e por último ‘c’, que vão ficando associados ao nome car. Este exemplo mostra ainda as duas formas de percorrer os elementos de uma cadeia de caracteres. O facto de estarmos a lidar com sequências faz com que os elementos sejam extraídos pela ordem em que se encontram na sequência.

Nota que pode ser saltada. Existem outros objectos iteráveis de que falaremos ao longo do curso, como os listas, os dicionários ou os conjuntos que também podem ser usados na parte do cabeçalho dos ciclos. No caso dos dicionários e dos conjuntos, porque não existe nenhuma ordem apenas podemos garantir que cada vez que se percorre o ciclo há um elemento que é extraído do objecto. É isso que define um objecto como sendo iterável: a possibilidade de ir fornecendo um a um os seus elementos.

Como já dissemos os ciclos são usados na resolução de qualquer problema minimamente complexo. Nestes casos os ciclos são usados num contexto mais vasto que faz emergir um padrão de utilização: ir construindo a solução final do nosso problema a partir de uma solução inicial que vai progredindo para a solução desejada cada vez que o ciclo é executado. Vamos aos exemplos.

Suponhamos que não sabemos que existe uma operação pré-definida para determinar o elemento máximo de uma cadeia de caracteres pelo que vamos implementar o respectivo programa. É desde logo claro que vamos ter que percorrer toda a cadeia. Por outro lado, estando interessados no elemento maior interessa-nos fazer a procura pelo conteúdo da cadeia. Assim sendo vamos ter um esqueleto de solução com o seguinte aspecto:

def maximo(cadeia):
      # inicialização
      for car in cadeia:
 # Transformação
      return elem_max
Mas falta o essencial. Vamos pensar. Admitamos que num dado momento da consulta da nossa cadeia sabemos qual é o maior elemento de entre os analisados. O que fazer então? Bom, consultamos o primeiro elemento dos ainda não analisados (lembre-se há uma ordem nas cadeias de caracteres!) e procuramos saber se é maior do que o máximo actual. Em função da resposta actualizamos, ou não, o máximo. A figura ilustra a situação.
 Então podemos melhorar a nossa solução básica:
def maximo(cadeia):
    # inicialização
    for car in cadeia:
        # elem_max é o maior dos elementos já vistos
        if elem_max < car:
            elem_max = car
    return elem_max
Uma das características do ciclo é que existe um propriedade que é sempre verdadeira quando vamos iniciar as instruções dentro do ciclo. Chama-se um invariante ... de ciclo. No nosso exemplo o invariante é: elem_max é o maior dos elementos já vistos! A inicialização do ciclo é feita por forma a tornar logo na primeira vez verdadeiro o invariante e daí a solução:
def maximo(cadeia):
    # inicialização
    elem_max = cadeia[0]
    for car in cadeia:
        # elem_max é o maior dos elementos já vistos
        if elem_max < car:
            elem_max = car
    return elem_max
Analisemos agora a solução. Em primeiro lugar só funciona se a cadeia tiver elementos, caso contrário dá erro. Por outro lado, tem uma ineficiência pois o primeiro elemento é analisado dentro do ciclo quando é desnecessário. Estas duas observações levam à solução final seguinte.
def maximo(cadeia):
    """Determina o máximo de uma cadeia de caracteres não vazia."""
    # inicialização
    elem_max = cadeia[0]
    for car in cadeia[1:]:
        # elem_max é o maior dos elementos já vistos
        if elem_max < car:
            elem_max = car
    return elem_max
Para consolidar a ideia passemos a um problema diferente: dada uma cadeia de caracteres fabricar a cadeia inversa. Uma vez mais vamos ter que percorrer toda a cadeia e também aqui são os elementos que nos interessam. De novo vamos partir de uma solução parcial que aproximamos passo a passo do resultado. Vamos pensar de novo indutivamente, procurando um invariante de ciclo e uma transformação que nos aproxime progressivamente da solução pretendida. Façamos o desenho que traduz a nossa estratégia. Baseia-se na ideia simples de que já temos uma cadeia que contém os caracteres invertidos de uma parte inicial da cadeia de que partimos.
Obtida uma solução parcial vamos buscar o próximo elemento à cadeia original, o primeiro ainda não invertido, que é colocado na sua posição de destino. Como podemos inicializar a solução parcial antes de entrar no ciclo? Basta considerar a cadeia vazia. E qual o invariante do ciclo? Na etapa de ordem N a solução parcial contem os primeiros (N-1) caracteres por ordem inversa. E agora o código.
def inverte(cadeia):
    """Inverte uma cadeia de carateres."""
    # Inicialização
    solucao = ''
    for car in cadeia:
        # Na etapa N solucao tem os primeiros (N-1) caracteres da cadeia por ordem inversa."""
        solucao = car + solucao
    return solucao
Comentário. Esta solução funciona mesmo com cadeias vazias.

Terceiro exemplo: Determinar se um caractere ocorre numa cadeia. Devolve o índice do elemento na cadeia caso exista, -1 caso não ocorra. Vamos fazer agora ao contrário, apresentando desde já a solução.
def procura(car, cadeia):
    """Procura car em cadeia. Devolve  o índice do caracter na cadeia, -1 se não encontrou."""
    enc = -1
    for i in range(len(cadeia)):
        if car == cadeia[i]:
            enc = i
    return enc
Algumas notas. Como pretendemos o índice, percorremos a cadeia por posição e não por conteúdo. Funciona mesmo no caso de cadeias vazias. No caso de existir mais do que uma ocorrência devolve o índice da última. Vermos ao longo do curso como se poderia resolver a questão de sair do ciclo mal encontre uma ocorrência do caracter. Questão: qual será o invariante? Pense um pouco, vai ver que chega à conclusão: no início da etapa i, o nome enc está associado ao índice ao índice a última ocorrência do car na sub-cadeia formada pelos primeiros (i-i) caracteres.

Resumindo. Existe um modo de resolver problemas que envolve ciclos e pensamento indutivo. Os ciclos têm três partes: inicialização, controlo e transformação. O pensamento indutivo significa que assumimos uma solução parcial que cada vez que o ciclo é percorrido se aproxima da solução pretendida. Descobrir o invariante do ciclo significa (quase) resolver o problema. A inicialização é a definição de valores que tornam o invariante verdadeiro à entrada do ciclo. Agora divirta-se!

domingo, 13 de outubro de 2013

De que falamos quando falamos de... (2)

Quando escrevemos um programa em Python, seja num vulgar editor de texto seja recorrendo ao editor de um ambiente integrado de desenvolvimento como o Wingware, foi-vos dito para colocar o código
if __name__ == ‘__main__’:
a separar as nossas definições da parte em que as usamos. Por exemplo:
import turtle

def  poligono(posx,posy,orientacao,lado, num_lados):
    """ Desenha um poligono de num_lados ."""
    # Prepara
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    turtle.setheading(orientacao)
    # Desenha
    for i in range(num_lados):
        turtle.forward(lado)
        turtle.left(360/num_lados)
    turtle.hideturtle()

if __name__ == ‘__main__’:
    poligono(-50,-50,0,20,8)
    turtle.exitonclick()
Será que é obrigatório fazer assim? Não, claro que não. Já vimos que podemos não usar a construção condicional if no final e o nosso programa corre na mesma. Então porque usamos?? Vamos tentar explicar. Sabemos que a linguagem Python pode ser estendida, adicionando novos comandos guardados em módulos. Esses módulos, para podermos usar as constantes e comando que nele estão definidos, têm que ser importados. Os módulos são tecnicamente objectos e, como todos os objectos têm um conjunto de atributos:
>>> import math
>>> id(math)
4528111056
>>> math

>>> type(math)
< class 'module' >
>>>
Isso mesmo, têm identidade, valor e tipo. Mas têm outros atributos igualmente que podem ser identificados consultando o módulo.
>>> dir(math)
['__doc__', '__file__', '__name__', '__package__', 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'copysign', 'cos', 'cosh', 'degrees', 'e', 'erf', 'erfc', 'exp', 'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'hypot', 'isfinite', 'isinf', 'isnan', 'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'modf', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'trunc']
>>>
Um dos atributos é o nome (__name__). Quando importamos um módulo é criado um espaço de nomes e __name__ fica associado a um objecto do tipo cadeia de caracteres que define o nome do módulo, após a importação. E podemos saber qual é consultando como habitualmente qual o valor do objecto associado ao nome.
>>> math.__name__
'math'
>>> 
Quando importamos o nome do módulo é igual ao nome do ficheiro que implementa o módulo depois de retirada a extensão do ficheiro. Regressemos agora aos nosso ficheiros com código Python. São criados com a extensão ‘.py’. Também eles têm a natureza de ... módulos. Podem por isso ser importados, e têm os mesmos atributos:
>>> import poli
>>> id(poli)
4546041256
>>> poli

>>> type(poli)
< class 'module' >
>>> dir(poli)
['__builtins__', '__cached__', '__doc__', '__file__', '__name__', '__package__', 'poligono', 'turtle']
>>> poli.__name__
'poli'
>>>
Uma vez mais um ficheiro com código Python escrito por nós quando importado tem um nome igual ao nome do ficheiro depois de retirada a extensão. Olhando para o código do ficheiro indicado acima, o leitor atento terá verificado que a chamada à definição polígono e ao método exitonclick() não ocorreram. E porquê? Por que serem executadas é preciso que o teste.
if __name__ == ‘__main__’:
seja verdadeiro, o que não acontece pois, como acabámos de ver, quando um módulo/ficheiro é importado o seu nome é igual ao do módulo! Vejamos agora o que acontece quando em vez de importar executamos o módulo/ficheiro.
Agora sim, aparece o polígono ! Conclusão. O recurso à separação do nosso ficheiro em duas partes, uma onde definimos o código e a outra onde executamos condicionalmente algumas instruções, permite que um ficheiro possa ter dois comportamentos diferentes, quando importamos ou quando mandamos correr/executar o ficheiro. Mas porque é que podemos querer este duplo comportamento? Bem, se o nosso programa for apenas para ser executado por nós e nunca importado não precisamos. Mas se por acaso nós, ou alguém, quiser usar algumas das definições que estão no nosso ficheiro para fins diversos dos nossos então precisamos de usar a instrução condicional a separar as duas partes: definição e uso. Refira-se ainda que muitos programadores que desenvolvem módulos complexos para Python usam esta facilidade para colocar a seguir ao if um conjunto de chamadas às definições que ilustram o que fazem as diferentes instruções contidas na primeira parte do módulo. Se ainda tem duvidas e quer testar com um código minimalista então crie um ficheiro com o seguinte conteúdo.
""" name_main.py."""

if __name__ == '__main__':
 print('Execução')
else:
 print('Importação')
salve-o com um nome apropriado (por exemplo name_main.py), e teste em duas situações: importe o módulo e execute o programa.

sábado, 12 de outubro de 2013

Devagar se vai ao longe

O módulo turtle é um velho conhecido nosso. Permite fazer desenhos e gráficos. Vamos supor que queríamos reproduzir o sinal de local perigoso devido a radioactividade.
Vamos transformar esta questão em mais um exercício para exemplificar como se pode dominar a complexidade. Primeiro, dividimos o problema em dois sub-problemas: (1) desenhar o fundo (um quadrado amarelo com um bordo espesso) e (2) ... desenhar o resto.
"""Devagar se vai ao longe: radioactividade."""
import turtle

def radioactividade():
    # Desenha fundo
    fundo()
    # Desenha frente
    frente()

def fundo():
    pass

def frente():
    pass
    
if __name__ == '__main__':
    radioactividade()
    turtle.exitonclick()
Pode parecer estranho, mas este programa já pode ser executado, embora não faça (quase) nada. Com este modelo fica claro que apenas tomámos a decisão de dividir o problema em dois. Nada mais. Tentemos resolver agora a primeira questão. Sabemos desenhar quadrados em função do comprimento do lado.
"""Devagar se vai ao longe: radioactividade."""
import turtle

def radioactividade(lado):
    # Desenha fundo
    fundo(lado)
    # Desenha frente
    frente()

def fundo(lado):
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.hideturtle()

def frente():
    pass
    
if __name__ == '__main__':
    radioactividade(100)
    turtle.exitonclick()
Ao concretizar introduzimos uma variável (lado) que terá que ser argumento do programa principal (radioactividade) e do fundo.Vamos completar a especificação tornando possível definir a cor e a espessura do bordo (não nos vamos preocupar para já nem com o posicionamento do quadrado nem com a orientação.)
"""Devagar se vai ao longe: radioactividade."""
import turtle

def radioactividade(lado):
    # Desenha fundo
    fundo(lado,'yellow',4)
    # Desenha frente
    frente()

def fundo(lado, cor, espessura):
    # Atributos
    turtle.width(espessura)
    turtle.fillcolor(cor)
    turtle.begin_fill()
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.end_fill()
    turtle.hideturtle()

def frente():
    pass
    
if __name__ == '__main__':
    radioactividade(100)
    turtle.exitonclick()
A solução acima cumpre a primeira parte. Se mandarmos correr o programa o que aparece é:
Notemos de passagem que a função fundo permite desenhar um fundo quadrado com qualquer cor e qualquer espessura. No entanto, preferimos não tornar essas opções possíveis no nosso problema inicial pelo que a cor e a espessura não são parâmetros da função radioactividade. Agora é a vez do segundo sub-problema. Vamos basear-nos no princípio ilustrado em post anterior: olhar e ver! E vemos a existência de um sector circular repetido três vezes e de uma circunferência. Daí que...
"""Devagar se vai ao longe: radioactividade."""
import turtle

def radioactividade(lado):
    # Desenha fundo
    fundo(lado,'yellow',4)
    # Desenha frente
    frente()

def fundo(lado, cor, espessura):
    # Atributos
    turtle.width(espessura)
    turtle.fillcolor(cor)
    turtle.begin_fill()
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.end_fill()
    turtle.hideturtle()

def frente():
    # Sectores
    sector()
    sector()
    sector()
    # Circunferência
    circunferencia()

def sector():
    pass

def circunferencia():
    pass

    
if __name__ == '__main__':
    radioactividade(100)
    turtle.exitonclick()
É claro que não avançámos muito pois estão ainda muitas coisas por decidir: posição, orientação,cor de fundo, amplitude e raio dos sectores, e também a posição, cor de fundo, espessura do bordo, cor do bordo e raio da circunferência. Vamos tratar dessas questões mas, para variar, vamos começar pela circunferência. Como se vai ver vamos ter que fazer ligeiras alterações no código anterior, uma vez mais determinado pelo facto de que os sub-problemas não são independentes. Em particular, o posicionamento da circunferência depende do posicionamento do fundo. Vamos então generalizar, incluindo a posição como argumento do problema.
"""Devagar se vai ao longe: radioactividade."""
import turtle

def radioactividade(posx, posy,lado):
    # Desenha fundo
    fundo(posx, posy,lado,'yellow',4)
    # Desenha frente
    frente(lado)

def fundo(lado, cor, espessura):
    # Atributos
    turtle.width(espessura)
    turtle.fillcolor(cor)
    turtle.begin_fill()
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.end_fill()
    turtle.hideturtle()

def frente(posx, posy, lado):
    # Sectores
    sector()
    sector()
    sector()
    # Circunferência
    circunferencia(posx+lado/2,t posy+(lado/2) - (lado/10), lado/10, 'black','yellow',2)

def sector():
    pass

def circunferencia(posx,posy,raio,cor_fundo,cor_bordo, espessura):
    # Posiciona
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    # Define atributos
    turtle.pencolor(cor_bordo)
    turtle.fillcolor(cor_fundo)
    turtle.width(espessura)
    # Desenha
    turtle.begin_fill()
    turtle.circle(raio)
    turtle.end_fill()

    
if __name__ == '__main__':
    radioactividade(400)
    turtle.exitonclick()
Para além do que já foi referido tivemos que ter em atenção aspectos como o facto de o raio ter Devemos também ter em conta que, se queremos o centro da circunferência no ponto (x,y), temos que ter a tartaruga afastada dessa posição, numa das coordenadas, de um valor igual ao raio. Qual das coordenadas e como afastamos depende da orientação da tartaruga. Do ponto de vista da tartaruga o centro está sempre à sua esquerda! Executemos para ver, um pouco como São Tomé.
Nada mau. Já faltou mais... Chegou a vez dos sectores. Aqui se queremos ter uma única definição que possa ser aplicada aos três sectores a única diferença significativa será a orientação. Claro que uma vez mais temos que pensar no valor da posição, do raio, da amplitude e da cor de fundo. Embora sejam os mesmos nos três casos, vamos implementar uma solução geral que possa eventualmente ser usada noutro tipo de problemas ( se pensar bem vai ver que encontra uma outra aplicação para o código geral ...).Vejamos então a nossa solução.
"""Devagar se vai ao longe: radioactividade."""
import turtle

def radioactividade(posx,posy,lado):
    # Desenha fundo
    fundo(posx, posy,lado,'yellow',4)
    # Desenha frente
    frente(posx,posy,lado)

def fundo(posx,posy,lado, cor, espessura):
    # Define posicao
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    # Atributos
    turtle.width(espessura)
    turtle.fillcolor(cor)
    turtle.begin_fill()
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.end_fill()
    turtle.hideturtle()

def frente(posx,posy,lado):
    # Sectores
    sector(posx+lado/2,posy+lado/2,0,0.4*lado,60,'black')
    sector(posx+lado/2,posy+lado/2,120,0.4*lado,60,'black')
    sector(posx+lado/2,posy+lado/2,240,0.4*lado,60,'black')
    # Circunferência
    circunferencia(posx+lado/2,posy+(lado/2) - (lado/10), lado/10, 'black','yellow',2)

def sector(posx,posy,orientacao,raio, angulo, cor_fundo):
    # Atributos
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    turtle.setheading(orientacao)
    turtle.fillcolor(cor_fundo)
    turtle.begin_fill()
    # Desenha
    turtle.forward(raio)
    turtle.left(90)
    turtle.circle(raio, angulo)
    turtle.left(90)
    turtle.forward(raio)
    turtle.left(180-angulo)
    turtle.end_fill()
    turtle.hideturtle()

def circunferencia(posx,posy,raio,cor_fundo,cor_bordo, espessura):
    # Posiciona
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    # Define atributos
    turtle.pencolor(cor_bordo)
    turtle.fillcolor(cor_fundo)
    turtle.width(espessura)
    # Desenha
    turtle.begin_fill()
    turtle.circle(raio)
    turtle.end_fill()
    turtle.hideturtle()

    
if __name__ == '__main__':
    radioactividade(-100,-100,400)
    turtle.exitonclick()
Como se vê na solução apresentada, os sectores estão centrados e têm um raio de tamanho igual a 10% do lado. No entanto se executarmos este código verificamos que agora a circunferência não está bem posicionada. A razão está no facto de os problemas sector e circunferência ... não serem independentes. O desenho dos sectores altera a orientação final da tartaruga! A solução mais simples e cómoda é tornar a orientação um parâmetro da circunferência.
"""Devagar se vai ao longe: radioactividade."""
import turtle

def radioactividade(posx,posy,lado):
    # Desenha fundo
    fundo(posx, posy,lado,'yellow',4)
    # Desenha frente
    frente(posx,posy,lado)

def fundo(posx,posy,lado, cor, espessura):
    # Define posicao
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    # Atributos
    turtle.width(espessura)
    turtle.fillcolor(cor)
    turtle.begin_fill()
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.end_fill()
    turtle.hideturtle()

def frente(posx,posy,lado):
    # Sectores
    sector(posx+lado/2,posy+lado/2,0,0.4*lado,60,'black')
    sector(posx+lado/2,posy+lado/2,120,0.4*lado,60,'black')
    sector(posx+lado/2,posy+lado/2,240,0.4*lado,60,'black')
    # Circunferência
    circunferencia(posx+lado/2,posy+(lado/2) - (lado/10), 0, lado/10, 'black','yellow',2)

def sector(posx,posy,orientacao,raio, angulo, cor_fundo):
    # Atributos
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    turtle.setheading(orientacao)
    turtle.fillcolor(cor_fundo)
    turtle.begin_fill()
    # Desenha
    turtle.forward(raio)
    turtle.left(90)
    turtle.circle(raio, angulo)
    turtle.left(90)
    turtle.forward(raio)
    turtle.left(180-angulo)
    turtle.end_fill()
    turtle.hideturtle()

def circunferencia(posx,posy,orientacao,raio,cor_fundo,cor_bordo, espessura):
    # Posiciona
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    # Define atributos
    turtle.setheading(orientacao)
    turtle.pencolor(cor_bordo)
    turtle.fillcolor(cor_fundo)
    turtle.width(espessura)
    # Desenha
    turtle.begin_fill()
    turtle.circle(raio)
    turtle.end_fill()
    turtle.hideturtle()

    
if __name__ == '__main__':
    radioactividade(-100,-100,400)
    turtle.exitonclick()
E agora, já está. Como vê devagar se vai ao longe.
Esta solução final resulta da nossa preguiça. E também claro, de uma análise apressada do problema. Deixamos ao leitor a tarefa de tornar a orientação do desenho um parâmetro do problema. Se quiser ainda pode alterar o código de modo que as cores do preenchimento possam ser outras. Deste modo pode inventar uma simbologia diferente.

sexta-feira, 11 de outubro de 2013

Programar é ... recordar!

Depois de muitos anos a programar todos nós temos um conjunto de soluções para diferentes problemas. quando se nos depara um novo problema um reflexo natural é perguntar: já resolvi este problema? Já resolvi um problema parecido? Já resolvi um problema que não sendo parecido me pode inspirar no modo de resolver o problema novo? Para tornar as coisas mais concretas olhemos para a figura seguinte:
Cinco quadrados concêntricos, coloridos. O problema é o de desenvolver um programa que crie este desenho. Depois de pensar um pouco, recordo-me do problema do logotipo dos jogos olímpicos:
Qual é analogia? O mesmo objecto repetido cinco vezes, mas com cores, posição e dimensão diferentes. Então é tentador seguir a mesma abordagem que neste caso: primeiro um programa que desenha um quadrado colorido, numa dada posição e com um dado comprimento para o lado. Como no caso anterior podia resolver este problema por partes: primeiro, um quadrado com um dado lado, depois numa dada posição e, finalmente com uma dada cor. Acho que não me levarão a mal se eu colocar já a versão final. Afinal, é o que nos vai acontecer à medida que estamos à vontade com a programação!
def quadrado_cor(posx,posy,lado, cor):
    # Posiciona
    turtle.penup()
    turtle.goto(posx,posy)
    turtle.pendown()
    # Define cor
    turtle.pencolor(cor)
    # Desenha
    for i in range(4):
        turtle.forward(lado)
        turtle.left(90)
    turtle.hideturtle()
Agora a segunda parte: desenhar cinco, variando a posição, o lado e a cor. Vamos fazer um desenho para nos ajudar, desenho esse que mostre a relação entre dois quadrados em função da diferença de tamanho dos respectivos lados (que designamos por incremento):
A partir do desenho não é difícil o que temos que mudar na posição e no tamanho. Na posição, e tomando como referência o canto inferior esquerdo a posição do quadrado externo relativamente ao interno será:

posx_ext = posx_int - incremento / 2
posy_ext = posy_int - incremento / 2

Quanto ao tamanho:
lado_ext = lado_int + incremento
Resolvida esta questão vamos então resolver o que falta.
def quadrados_concentricos(posx, posy,lado,incremento):
    quadrado_cor(posx,posy,lado,'red')
    quadrado_cor(posx - incremento/2,posy - incremento/2,lado + incremento,'green')
    quadrado_cor(posx - incremento,posy -incremento,lado + 2*incremento,'blue')
    quadrado_cor(posx - 3*incremento/2,posy-3*incremento/2,lado + 3*incremento,'black')
    quadrado_cor(posx - 2 * incremento,posy- 2*incremento,lado + 4*incremento,'orange')
    
        
        
if __name__ == '__main__':
    quadrados_concentricos(0,0,50,20)
    turtle.exitonclick()
Antes de ir fazer outra coisa volte ao código do logotipo dos jogos olímpicos e veja as semelhanças...