domingo, 20 de dezembro de 2015

Teste #3 - TP2

Pergunta 2
Temos uma árvore genealógica representada por um dicionário, sendo que as chaves são nomes de pessoas e os valores são os pais dessas pessoas. Trata-se pois de uma representação diferente (inversa) da discutida em post anterior. Pedem-nos um programa em que dado o dicionário e o nome de uma pessoa nos seja devolvido a lista dos avós dessa pessoa. É-nos sugerido que pensemos que os avós são os pais dos pais…
Com esta observação uma abordagem lógica seria começar por determinar os pais de uma pessoa. Como a representação liga uma pessoa (chave) aos seus pais (valor) podemos obter essa informação de modo directo.
def pais(ag,nome):
    return ag.get(nome,[])
Resolvido esta questão vamos então usar a definição de avós: pais dos pais.
def avos(ag,nome):
    p = pais(ag,nome)
    return pais(ag,p[0]) + pais(ag,p[1])
A estratégia é linear: determinamos os fpais e, depois, para cada um dos pais, os seus pais. Notar que o get tem um valor por defeito que é uma lista vazia.
Pergunta 3
Partimos de um ficheiro em que cada linha era composta por “nome apelido nota1 nota2 nota3\n” . Queremos criar um novo ficheiro em que cada linha é semelhante à do ficheiro de origem. A diferença é que foi acrescentada informação: a média das três notas e a referência “Aprovado” ou “Reprovado” em função da média ser igual ou superior a 47.5, para aprovação e menor para reprovação. A ideia da solução é simples, pois trata-se de criar um novo ficheiro. Percorremos o ficheiro linha a linha e transformamos cada linha antes de a introduzir no ficheiro de saída. Daí a solução:
def define_notas(fich_1,fich_2):
    with open(fich_1,'r',encoding='utf-8') as f_ent:
        with open(fich_2,'w',encoding='utf-8') as f_saida:
            for linha in f_ent:
                # define média
                nome,apelido,n_1,n_2,n_3 = linha.strip().split()
                media = (float(n_1) + float(n_2) + float(n_3))/3
                if media >= 47.5:
                    classific = 'Aprovado'
                else:
                    classific = 'Reprovado'
                # escreve nova linha
                nova_linha = nome + ' ' + apelido + ' ' + n_1 + ' ' + n_2 + ' ' + n_3 + ' ' + str('%4.2f' % media) + ' ' + classific + '\n'
                f_saida.write(nova_linha)

Teste #3 - TP1

Pergunta 2
Temos uma árvore genealógica representada por um dicionário, sendo que as chaves são nomes de pessoas e os valores são os pais dessas pessoas. Trata-se pois de uma representação diferente (inversa) da discutida em post anterior. Pedem-nos um programa em que dado o dicionário e o nome de uma pessoa nos seja devolvido a lista dos netos dessa pessoa. É-nos sugerido que pensemos que os netos são os filhos dos filhos…
Com esta observação uma abordagem lógica seria começar por determinar os filhos de uma pessoa. Como a representação liga uma pessoa (chave) aos seus pais (valor) temos que percorrer o dicionário recorrendo às chaves e ao conteúdo.
def filhos(ag,nome):
    fil = []
    for ch,val in ag.items():
        if nome in val:
            fil.append(ch)
    return fil
Resolvido esta questão vamos então usar a definição de netos: os filhos dos filhos.
def netos(ag,nome):
    f  = filhos(ag,nome)
    net = []
    for filho in f:
        net.extend(filhos(ag,filho))
    return net
A estratégia é linear: determinamos os filhos e, depois, para cada um dos filhos, os seus filhos. Como o que é devolvido pela função filhos é uma lista temos que recorrer ao uso de extend.
Pergunta 3
Partimos de um ficheiro em que cada linha era composta por “nome apelido nota1 nota2 nota3\n” . Queremos criar um novo ficheiro em que cada linha é semelhante à do ficheiro de origem. A diferença é que foi retirada a nota mais baixa. Com esta formulação o problema torna-se mais simples e a estratégia é básica: percorrer o ficheiro origem por linha (ciclo for), transformar a linha e guardá-la no novo ficheiro. Daí a solução:
def define_notas(fich_1,fich_2):
    with open(fich_1,'r',encoding='utf-8') as f_ent:
        with open(fich_2,'w',encoding='utf-8') as f_saida:
            for linha in f_ent:
                # transforma linha 
                #  —- retira pior nota
                nome,apelido,*notas = linha.strip().split()
                notas = list(notas)
                notas.remove(min(notas))
                #  —- escreve nova linha
                nova_linha = nome + ' ' + apelido + ' ' + ' '.join(notas) + '\n'
                f_saida.write(nova_linha)
O lado menos usual nesta solução está no recurso a *notas quando estamos a extrair as componentes da linha. Quem não estivesse familiarizado com essa notação poderia sempre fazer:
 nome,apelido,n1,n2,n3 = linha.strip().split()
            notas = [n1,n2,n3]

terça-feira, 15 de dezembro de 2015

Das aulas

Na aula de hoje falei numa organização das nossas músicas recorrendo a um dicionário. O dicionário é formado por pares (autor, lista dos álbuns do autor). Por sua vez, cada album da lista é representado por um dicionário com três pares. O primeiro é o par (nome, nome da música), o segundo o par (tipo, tipo da música), e o terceiro o par (emprestado, sim/não). Um exemplo de dicionário (muito pequeno …).
musicas = {'pink floyd': [{'nome':'dark side', ‘tipo':'pop','emprestado':False},{'nome':'wall','tipo':'pop','emprestado':True}],
 'doors': [{'nome':'LA woman’,'tipo':'rock','emprestado':True},{'nome':'live','tipo':'rock','emprestado':False}],
'jethro tull':[{'nome':'thick as a brick','tipo': ‘rock','emprestado':False}]}
Esta organização não pretende ser realista, mas apenas uma ajuda a compreender as operações sobre dicionários. Coloquei três questões para esta representação: (1) adicionar um album à base de dados de músicas; (2) Marcar um album como emprestado; (3) listar todas as músicas de um dado tipo. Vamos resolver por etapas.

No primeiro caso uma solução trivial será:
def acrescentar(dicio_musicas,autor,album):
    dicio_musicas[autor] = dicio_musicas.get(autor,[]) + [album]
Como se pode ver uma simples instrução resolve o assunto. Notar que os dicionários são objectos mutáveis nos valores pelo que as alterações efectuadas são permanentes e não é obrigatório usar o return para devolver a nova versão do dicionário. Refira-se ainda neste caso apenas o método get ou o setdefault eram apropriados, garantindo que não há interrupção por erro caso o autor ainda não exista no dicionário.
Para o segundo caso apresentamos duas soluções:
def emprestar(dicio_musicas,autor,album):
    albuns = dicio_musicas.get(autor,[])
    if albuns:
        for musica in albuns:
            if musica == album:
                musica['emprestado'] = True
                break

 
def emprestar_b(dicio_musicas,autor,nome_musica):
    albuns = dicio_musicas.get(autor,[])
    if albuns:
        for musica in albuns:
            if musica['nome'] == nome_musica:
                musica['emprestado'] = True
                break 
Nos dois casos verificamos se o autor existe e, depois, se o album existe. Passados estes dois testes fazemos a alteração. A primeira solução pressupõe que todo o album é fornecido como entrada, enquanto que no segundo caso apenas o nome do album é fornecido. Notar uma vez mais o não recurso a return.
Finalmente, para o terceiro problema:
def musicas_tipo(dicio_musicas, tipo):
    res = []
    for albuns_autor in dicio_musicas.values():
        # analisa musicas de um autor
        for album in albuns_autor:
            if album['tipo'] == tipo:
                res.append(album['nome'])
    return res
Agora, executarmos um ciclo, por autor, em que os álbuns dos autores são analisados à vez.

domingo, 13 de dezembro de 2015

Dicionários

Suponha escreveu um livro e tem dois índices: um, com os capítulos e as páginas de início e de fim do capítulo. Outro, o índice remissivo, com as palavras e as páginas onde ocorrem. Pretendemos estabelecer uma nova associação em que as palavras têm associadas os capítulos onde aparecem. Vejamos como podemos resolver esta questão. Comecemos por pensar na organização dos índices: como dicionários.
    indice = {'c1':[1,34], 'c2': [35,42], 'c3':[43,52],'c4':[53,60]}
    remissivo = {‘p1':[2,54],'p2':[36,37,50],'p3':[40]}
Agora a solução. Sabemos que queremos passar das palavras aos capítulos. Então uma ideia simples é a de percorrer os dicionários das palavras e, por cada palavra e por cada página em que ocorre, procurar qual o capítulo.
def cap_pal(indice, remissivo):
# inicializa novo dicionário
    sol = dict()
    # percorre indice remissivo por palavra
    for palavra, pgs in remissivo.items():
        # percorre por pagina
        #  para cada pagina onde ocorre a palavra determina o capitaulo
        # actualiza o dicionário e passa à palavra seguinte
    return sol
Tornemos este esboço mais concreto.
def cap_pal(indice, remissivo):
    sol = dict()
    # percorre por palavra
    for palavra, pgs in remissivo.items():
        # percorre por pagina
        cap = []
        for pagina in pgs:
            c = capitulo(indice,pagina)
            if c not in cap:
                cap.append(c)
        sol[palavra] = cap
    return sol
Esta solução tem o cuidado de não acrescentar mais do que uma vez o capítulo, ou seja, se a palavra ocorrer mais do que uma vez no capítulo tal só é indicado uma vez. Mas falta-nos escrever o código que determina em que capítulo está uma dada página.
def capitulo(dicio,pg):
    for ch, val in dicio.items():
        if val[0] <= pg <= val[1]:
            return ch

    return None
Percorremos o dicionário extraindo as páginas de início e de fim para cada capítulo e tentamos saber em que intervalo está a nossa página, para definir o capítulo. E pronto. Podemos passar ao exercício seguinte…

Ficheiros (II)

Vamos tentar resolver o problema de eliminar uma linha de um ficheiro. O ficheiro vai ter que ser lido e temos que identificar a linha a eliminar. Faz sentido fazer então uma leitura por linhas. Vejamos uma solução simples.
def elimina_linha_1(ficheiro,n):
    """Elimina a linha n do ficheiro."""
    with open(ficheiro,'r',encoding='utf-8') as fich:
        # lê as linhas
        linhas = fich.readlines()
        fich.close()
    with open(ficheiro,'w',encoding='utf-8') as fich:
        # elimina a linha n
        linhas.pop(n)
        # escreve nova versão
        fich.writelines(linhas)
        fich.close()
Como fizemos??? Lemos e guardámos todas as linhas. Depois eliminámos a linha pretendida. Finalmente apagámos o ficheiro antigo com a abertura em modo ‘w’ e colocámos lá a nova informação. Claro que podíamos ter feito uma leitura inicial linha a linha filtrando a linha pretendida. O resultado seria:
def elimina_linha_2(ficheiro,n):
    """Elimina a linha n do ficheiro."""
    with open(ficheiro,'r',encoding='utf-8') as fich:
        linhas = []
        for i,linha in enumerate(fich):
            if i != n:
                linhas.append(linha)
        fich.close()
    with open(ficheiro,'w',encoding='utf-8') as fich:
        fich.writelines(linhas)
        fich.close() 
Admita que agora a especificação é ligeiramente diferente. Não se trata de eliminar mas de substituir. O que fizemos anteriormente pode servir-nos de inspiração:
def substitui_linha_1(ficheiro,n, nova_linha):
    """Substitui linha n do ficheiro."""
    with open(ficheiro,'r',encoding='utf-8') as fich:
        novas_linhas = []
        for i,linha in enumerate(fich):
            if i == n:
                novas_linhas.append(nova_linha)
            else:
                novas_linhas.append(linha)
    with open(ficheiro,'w',encoding='utf-8') as fich:
        fich.writelines(novas_linhas)
Nesta solução, vamos copiando as linhas a manter e colocando a nova no lugar pretendido. Nada de especial, certo? Às vezes mais vale tentar uma solução simples mas que resulta do que uma rebuscada que nos pode trazer alguns dissabores. Mas nada impede agora que tente as suas soluções para as duas questões apresentadas.

Ficheiros (I)

Suponha que anda à procura num ficheiro de texto de um dado padrão. Como resolver o problema? Se pensar um pouco chegará à conclusão que vai ter que ler o ficheiro e analisar a cadeia de caracteres para determinar se o padrão o corre ou não. Podemos ler o ficheiro todo de uma vez, linha a linha ou todas as linhas (neste caso na forma de uma lista de linhas). Vejamos as diferentes soluções:
def wally_0(ficheiro,padrao):
    """wally está no ficheiro?"""
    with open(ficheiro,'r',encoding='utf-8') as f_ent:
        texto = f_ent.read()
        if padrao in texto:
            return True
        return False



def wally_1(ficheiro,padrao):
    """wally está no ficheiro?"""
    with open(ficheiro,'r',encoding='utf-8') as f_ent:
        for linha in f_ent:
            if padrao in linha:
                return True
        return False

    
def wally_11(ficheiro,padrao):
    """wally está no ficheiro?"""
    with open(ficheiro,'r',encoding='utf-8') as f_ent:
        texto = f_ent.readlines()
        for linha in texto:
            if padrao in linha:
                return True
        return False 

Suponhamos agora que a pergunta muda ligeiramente, e nos pedem para dizer não apenas se o padrão existe, mas também em que linha existe. Uma vez mais, uma breve reflexão leva-nos a concluir que ler todo o ficheiro de uma só vez não é agora adequado. Também ler todas as linhas não nos parece a melhor ideia: afinal mal encontremos o padrão podemos abandonar o programa e não precisamos de guardar na memória todos as linhas do ficheiro, que podem ser em número muito elevado. Como fazer? Essa uma resposta.
def wally_2(ficheiro,padrao):
    """em que linha está wally  no ficheiro?"""
    with open(ficheiro,'r',encoding='utf-8') as f_ent:
        for i,linha in enumerate(f_ent):
            if padrao in linha:
                return i+1
        return -1 
como se observa no código acima, usamos a função enumerate para nos resolver a dificuldade de ler as linhas e saber o seu número. E se agora nos disserem que se pretendem as linhas de todas as ocorrências? Hum,…, para isso precisamos de um contentor onde guardar a informação que vamos recolhendo por análise do ficheiro linha a linha.
def wally_3(ficheiro,padrao):
    """em que linhas está wally  no ficheiro?"""
    with open(ficheiro,'r',encoding='utf-8') as f_ent:
        num_lin = []
        for i,linha in enumerate(f_ent):
            if padrao in linha:
                num_lin.append(i+1)
        return num_lin 
Não param de mudar o que nos pedem: passou a ser a linha e a posição dentro da linha. Para resolver lá nos temos que socorrer dos métodos conhecidos sobre cadeias de caracteres:
def wally_4(ficheiro,padrao):
    """em que linha e posição na linha está wally?"""
    with open(ficheiro,'r',encoding='utf-8') as f_ent:
        for i,linha in enumerate(f_ent):
            pos = linha.find(padrao)
            if pos != -1:
                return i+1,pos
        return -1,-1
Como se pode ver o método find foi a nossa salvação. E este problema ainda mas para todas as ocorrências?
def wally_5(ficheiro,padrao):
    """em que linhas e posições nas linhas está wally?"""
    with open(ficheiro,'r',encoding='utf-8') as f_ent:
        num_pos_lin = []
        for i,linha in enumerate(f_ent):
            pos = linha.find(padrao)
            if pos != -1:
                num_pos_lin.append((i+1,pos))
        return num_pos_lin 
E se …. A sua imaginação é o limite. Tente perceber as alternativas e o modo como ficheiros, cadeias de caracteres e listas estão ligados entre si.

Os modos dos ficheiros...

Vimos nas aulas que os ficheiros podem ter vários modos básicos de abertura: leitura (r) , escrita (w) e acrescentar (a). Estes modos podem ser combinados em leitura e escrita adicionando o sufixo ‘+’. Na tabela abaixo procuramos mostrar o significado dos seis modos.
Há quem diga que uma imagem vale mais do que mil palavras (ou tabelas…), pelo que mostramos a mesma informação de outra maneira.
Deve ter em atenção que no modo ‘w+’ pode ler e escrever, mas só pode ler depois de ter escrito algo pois o ficheiro inicialmente tem todo o conteúdo apagado.

domingo, 29 de novembro de 2015

Vermes em passeio

Vamos regressar ao módulo turtle para ilustrar um aspecto pouco referido nas aulas. É possível a tartaruga marcar os pontos por onde passou graças ao comando stamp(). Também se pode apagar as marcas com vários comandos, por exemplo, com clearstamps(). O leitor é convidado a consultar o manual da linguagem para os detalhes. O exemplo que se segue mostra como podemos simular um passeio (mais ou menos) aleatório de um verme.
import turtle
import random

def bug(length, step, life):
    # initialization
    turtle.penup()
    theta = 0
    dtheta = 1
    turtle.color(random.choice([‘red','blue','yellow','green','black']))
    # draw bug
    for j in range(length):
        turtle.forward(step)
        turtle.left(theta)
        theta += dtheta
        turtle.stamp()  
    # move
    for i in range(life):
        turtle.clearstamps(1)
        if abs(ycor()) > 400:
            turtle.left(30)
        if abs(xcor()) > 400:
            turtle.right(30)
        if theta > 10 or theta < -10:
            dtheta = -dtheta                
        turtle.forward(step)
        turtle.left(theta)
        theta += dtheta
        turtle.stamp()

if __name__ == '__main__':
    turtle.setworldcoordinates(-500, -500, 500, 500)
    bug(10, 15, 500)
    turtle.exitonclick()
O programa acima tem três parâmetros: o comprimento do verme (length), a amplitude de cada movimento (step), e o numero de movimentos (life), Tem também três partes distintas. Na primeira, inicializamos o sistema, definimos o valor da variação da orientação que nos permite simular um movimento ondulatório (dtheta), e escolhemos aleatoriamente uma cor. Na segunda, o primeiro ciclo for, desenhamos o verme inicial. Na terceira, pomos o verme e movimentar-se. Notar-se-á que fechamos a zona onde se pode movimentar, um quadrado 400X400. Com o método setworlcoordinates() limitamos o tamanho máximo do mundo. Execute o programa para ter uma ideia do que se passa. Procure alterar os vários parâmetros e ver as consequências. Altere o programa para que o passeio seja mais naturalmente aleatório. Nota: Este exemplo foi adaptado do livro de Mark J. Johnson "A concise introduction to programming ion Python".

Árvore Genealógica

No texto de apoio à disciplina é introduzido um conceito simplista de árvore genealógica. Estas árvores são representadas por um dicionário em que as chaves são nomes de pessoas (um progenitor) e os valores são listas de nomes de pessoas (os filhos). Esta representação permite, por exemplo, obter os filhos de alguém de modo trivial:
def filhos(dicio,progenitor):
    """ lista dos filhos."""
    return dicio.get(progenitor,[])
Também é fácil listar os netos de alguém. Por exemplo:
def netos(dicio,progenitor):
    """ Lista netos. Filhos dos filhos"""
    desc1 = filhos(dicio,progenitor)
    if desc1:
        net = []
        for elem in desc1:
            desc2 = filhos(dicio,elem)
            if desc2:
                net = net + desc2
        return net
    else:
        return []
Esta solução calcula primeiro os filhos e, caso existam, os filhos dos filhos. Não é difícil propor versões alternativas para esta questão. Por exemplo:
def netos_b(arv_genea,progenitor):
    net = []
    desc1 = filhos(arv_genea,progenitor)
    for prog in desc1:
        net.extend(filhos(arv_genea,prog))
    return net
Aqui simplificamos o código no interior do ciclo recorrendo ao método extend. Mas ainda podemos aproximarmo-nos mais da defino natural: netos são os filhos dos filhos:
def netos_b(dicio ,progenitor):
    """ Lista dos netos. Os filhos dos filhos"""
    return filhos_b(dicio,filhos_b(dicio,[progenitor]))

def filhos_b(dicio,lista_progenitores):
    lista_filhos = []
    for filho in lista_progenitores:
        lista_filhos.extend(dicio.get(filho,[]))
    return lista_filhos
Note-se que tivemos que alterar a função filhos para agora me dar a lista dos filhos de um conjunto de pessoas.
Agora o conceito básico de progenitor de alguém:
def progenitor(ag,nome):
    for p,fil in ag.items():
        if nome in fil:
            return p
    return None
Neste caso vamos procurar o nome na lista dos filhos de alguém que, a existir, será o progenitor. Passemos a outros exemplos. Irmãos são pessoas que têm um progenitor comum. Com está organizada a árvore genealógica cada pessoa tem um progenitor único, logo:
def irmaos(ag,nome1,nome2):
    """ Têm o mesmo progenitor?"""
    prog1 = progenitor(ag, nome1)
    prog2 = progenitor (ag,nome2)
    return prog1 == prog2
Socorrendo-nos de novo da ideia de progenitor podemos definir o conceito de avô/avó.
def avo(dic,nome):
    """ Quem é o avô/avó do nome."""
    prog = progenitor(dic,nome)
    if prog:
        return progenitor(dic,prog)
    return None
Deixamos ao leitor o cuidado de definir outras relações de parentesco. Por exemplo, tente o conceito de primos (os filhos de irmãos). Por outro lado, alguns dos conceitos acima são booleanos. Imagine que em vez de se saber se duas pessoas são primos se pretende saber quais os primos de alguém. Não lhe deve ser difícil chegar à solução.
Finalmente, pense numa implementação mais realista de uma árvore genealógica, em que cada pessoa tem associado os seus pais. E faça tudo de novo!!!

domingo, 22 de novembro de 2015

Listas por Compreensão (II)

Estamos habituados em matemática a definir um conjunto de duas formas distintas: por extensão, quando indicamos os seus elementos, ou por intenção, quando indicamos uma regra que nos permite identificar os elementos do conjunto. Em Python podemos descrever uma lista de elementos também destas duas formas. À segunda chamamos listas por compreensão. As listas por compreensão estão ligadas à resolução de problemas que obedecem a um certo padrão. Por exemplo, admitamos que queremos um programa que gere números inteiros, aleatoriamente, entre um certo intervalo. Uma solução simples seria:
import random

def gera_numeros(n,inf, sup):
     res = []
     for i in range(n):
          num = random.randint(inf,sup)
          res.append(num)
     return res
Este padrão em que temos um acumulador onde vão sendo guardados elementos através de um processo repetitivo, pode ser implementado também por recurso a listas por compreensão:
def gera_numeros_b(n,inf, sup):
     res = [ random.randint(inf,sup) for i in range(n)]
     return res
Como se percebe a sintaxe envolve, nesta versão básica, os indicadores de lista (“[“ e “]”), seguido de uma expressão que nos permite gerar os elementos da lista, seguido do processo repetitivo (ciclo for). Podemos aplicar este modelo em diferentes situações.

Elevar os números de aula lista ao quadrado:
def quadrados(lista):
     return [elem**2 for elem in lista]
Produto escalar de dois vectores:
def escalar_comp(x,y):
     """ Produto escalar de dois vectores."""
     return sum([x[i]*y[i] for i in range(len(x))])
A lista por compreensão gera os produtos e a função sum faz a soma.
Somas parciais (percorrer a lista e gerar uma nova list em que na posição i é colocada a soma dos números da primeira lista desde o início até (inclusive) i):
def somas_parciais(lista):
     """ somas de 1 a i."""
     return [sum(lista[:i+1]) for i in range(len(lista))]
Podemos dizer que, de um modo geral ,este padrão obedece ao modelo:
def my_map(func, lista):
     return [func(elem) for elem in lista]
O leitor poderá pensar que não é muito poderoso este modo de programar. Por exemplo, não podemos filtrar elementos a incluir na lista em função de um dado critério. Mas as listas por compreensão têm uma sintaxe mais completa. Admitamos que queremos construir uma lista a partir de outra, retendo apenas os seus elementos positivos:
def filtro_nega(lista):
     return [ elem for elem in lista if elem > 0]
  
Agora temos a expressão, seguida do ciclo for, seguida do if. Outros exemplos: Lista dos índices das ocorrências de um dado elemento:
def ocorre(elem, lista):
     """ localizaçoes das ocorrências de elem na lista."""
     return [ i for i in range(len(lista)) if lista[i] == elem]
Conta o número de ocorrências:
def conta(elem, lista):
     """Quantas ocorrências de elem na lista."""
     return sum([ 1 for e in lista if e == elem])
Parece melhor. Mas e problemas em que existem mais do que um ciclo? Por exemplo, quando pretendemos construir a lista de pares ordenados formados com elementos de outras duas listas fazemos:
def combina(a,b):
     """ pares ordenados com elementos de a e de b."""
     res = []
     for elem_a in a:
          for elem_b in b:
               res.append((elem_a,elem_b))
     return res
Mas também esta situação está contemplada:
def combina_b(a,b):
     return [(elem_a,elem_b) for elem_a in a for elem_b in b]
Como se vê a sintaxe é dada pela expressão, seguida do ciclo mais externo, seguida do ciclo mais interno. No ciclo interno podemos referir objetos do ciclo externo.

Um exemplo simples consiste em obter a lista dos elementos de uma lista de listas:
def aplana(lista_listas):
     """Aplanar uma lista de listaas."""
     return [elem for linha in lista_listas for elem in linha  ]
Podemos ainda complicar mais as coisas? Podemos! Numa lista por compreensão no lugar da expressão podemos ter … uma lista por compreensão!!! Vejamos como esse facto nos permite obter a transposta de uma matriz de modo simples:
def transposta_b(matriz):
     """transposta de uma matriz."""
     return [ [matriz[j][i] for j in range(len(matriz))] for i in range(len(matriz[0]))]
Agora a expressão é uma lista por compreensão e deve ser considerada como estando no interior do ciclo for que aparece a seguir! Podemos usar este código para escrever um programa que roda 90 graus no sentido dos ponteiros do relógio uma imagem a preto e branco representada por uma lista de listas de 1s e 0s.
def roda_90(imagem):
     """ Rodar 90 graus uma imagem."""
     img_trans = transposta_b(matriz)
     nova_imagem = [linha[::-1]  for linha in img_trans]
     return nova_imagem
Estaremos limitados a dois ciclos? Mais uma vez a resposta é negativa. Segue-se um exemplo, que mostra como se podem obter três inteiros que verificam a condição do Teorema de Pitágoras:
def pitagoras(n):
     return [(x,y,z) for x in range(1,n) for y in range(1,n) for z in range(1,n) if x**2 + y**2 == z**2]
Executando o programa verificamos a ocorrência de repetições. Mas podemos resolver facilmente esse problema:
def pitagoras_b(n):
     return [(x,y,z) for x in range(1,n) for y in range(x,n) for z in range(y,n) if x**2 + y**2 == z**2]
E se em vez da potência ser 2 fosse outro inteiro maior do que 2? Estamos perante o chamado último Teorema de Fermat. Mas não aconselho a tentar prová-lo deste modo…
def fermat(n,k):
     return [(x,y,z) for x in range(1,n) for y in range(x,n) for z in range(y,n) if x**k + y**k == z**k]
O leitor continua a pensar que tudo isto é muito limitado. Vamos então a um exemplo em que a expressão é um pouco mais complexa, pois inclui uma condicional. Neste caso podemos implementar uma função em que todos os elementos de uma lista de determinado valor são substituídos por outro elemento:
def my_replace(novo, velho,lista):
     return [novo if elem == velho else elem for elem in lista]
Pois é, estamos perante um novo tipo de expressão: objecto if condição else objecto. Vamos usar esta ideia para obter o negativo de uma imagem a preto e branco:
def negativo_img(imagem):
     return [[ 0 if elem == 1 else 1  for elem in linha] for linha in imagem]
Para terminar, vamos deixá-lo com um exemplo que nos permite calcular uma lista de números primos, recorrendo ao algoritmo conhecido por Sieve of Erathostenes.
def primos(n):
     up = int(math.sqrt(n))
     no_primes = [j for i in range(2,up+1) for j in range(i**2,n+1,i)]
     return [ x for x in range(2,n+1) if x not in no_primes]
Interessante, não concorda? Talvez. Chegados aqui coloca-se a questão de saber se é possível realizar com listas por compreensão coisas que não se podem fazer de outro modo. E resposta é um redondo não. Então porquê usar? Se olharmos para os exemplos anteriores é claro que há medida que queremos fazer coisas mais complexas a legibilidade do programa diminui. No entanto existe uma razão forte: o uso de listas por compreensão torna os programas muito mais rápidos! Então o nosso conselho final é: use as listas por compreensão sempre que o tempo de execução for crítico ou, não sendo crítico, sempre que a legibilidade do programa não seja comprometida.

sexta-feira, 20 de novembro de 2015

Desenhar uma grelha: um exercício de programação

Nas aulas discutimos o problema de desenhar uma grelha rectangular com n células, cada uma um quadrado com um dado comprimento do lado. Uma das soluções que apareceu baseava-se na ideia de desenhar a grelha como nós humanos geralmente fazemos: desenhar separadamente as linhas verticais e as horizontais. Vejamos uma solução básica:
import turtle

def grelha_1(dim, lado):
    """ Solução básica."""
    # verticais
    turtle.setheading(90)
    for i in range(dim+1):
        # posiciona
        turtle.penup()
        turtle.goto(i * lado,0)
        turtle.pendown()
        # desenha
        turtle.forward(dim*lado)
    # horizontais
    turtle.setheading(0)
    for i in range(dim+1):
        # posiciona
        turtle.penup()
        turtle.goto(0,i*lado)
        turtle.pendown()
        # desenha
        turtle.forward(dim*lado)    
    turtle.hideturtle()
Como se pode ver as linhas são desenhadas em separado e cada tipo de linha (verticais ou horizontais) é desenhada no interior de um ciclo. A parte relevante em cada ciclo é o posicionamento da tartaruga para desenhar a linha. Fazemos isso através de um goto.
Uma primeira alteração possível é considerar a possibilidade de controlar a localização do canto inferior esquerdo. Vejamos como se pode fazer.
def grelha_2(dim, lado,pos_x,pos_y):
    """ Controlando a posição do canto inferior esquerdo."""
    # verticais
    turtle.setheading(90)
    for i in range(dim+1):
        # posiciona
        turtle.penup()
        turtle.goto(pos_x + i * lado,pos_y)
        turtle.pendown()
        # desenha
        turtle.forward(dim*lado)
    # horizontais
    turtle.setheading(0)
    for i in range(dim+1):
        # posiciona
        turtle.penup()
        turtle.goto(pos_x,pos_y+i*lado)
        turtle.pendown()
        # desenha
        turtle.forward(dim*lado)    
    turtle.hideturtle()
Como se notará a alteração é mínima, e traduz-se a colocar o valor das coordenadas do canto inferior esquerdo no sítio certo.
Suponhamos agora que nos pedem uma solução em que seja também possível controlar a orientação do quadrado. Esta questão já obriga a uma ginástica adicional, mas a questão central mantém-se a mesma: definir os pontos em que se iniciam as linhas. Eis uma solução, não muito elegante, mas que funciona…
def grelha_4(dim, lado,pos_x,pos_y,orient):
    """ Controlando a posição e a orientação. """
    # verticais
    for i in range(dim+1):
        # posiciona
        turtle.penup()
        turtle.goto(pos_x,pos_y)
        turtle.setheading(orient)
        turtle.forward(i*lado)
        turtle.setheading(90+orient)
        turtle.pendown()
        # desenha
        turtle.forward(dim*lado)
    # horizontais
    for i in range(dim+1):
        # posiciona
        turtle.penup()
        turtle.goto(pos_x,pos_y)
        turtle.setheading(90+orient)
        turtle.forward(i*lado)
        turtle.setheading(orient)
        turtle.pendown()
        # desenha
        turtle.forward(dim*lado)    
    turtle.hideturtle()
Outras alterações poderiam ser feitas, como seja mudar a espessura das linhas ou a sua cor. Mas o que não nos agrada é a legibilidade do código. Afinal o nosso ponto de partida foi considerar o desenho da grelha com base no conceito de linha, mas esse conceito está explicitamente ausente nas soluções acima. Vamos remediar a situação definindo uma função que nos permite desenhar uma linha, com uma dada posição inicial uma orientação e um dado comprimento. Não é difícil.
def linha(pos_x,pos_y, orient, tam):
    # posiciona
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    turtle.setheading(orient)
    turtle.pendown()
    # desenha
    turtle.forward(tam)
    turtle.hideturtle()
Na posse desta nova construção (abstracção), podemos refazer as soluções acima apresentadas. Comecemos pela básica:
def grelha_5(dim, lado):
    """ Solução básica."""
    # horizontais
    for i in range(dim+1):
        linha(0,i*lado,0,dim*lado)
    # verticais
    for i in range(dim+1):
        linha(i*lado,0,90,dim*lado)
    turtle.hideturtle()
O leitor concordará que corresponde de modo mais claro à forma como enunciámos a solução. Passemos à posição.
def grelha_6(dim, lado,pos_x,pos_y):
    """ Solução com controlo da posição do canto inferior esquerdo."""
    # horizontais
    for i in range(dim+1):
        linha(pos_x,pos_y+i*lado,0,dim*lado)
    # verticais
    for i in range(dim+1):
        linha(pos_x+i*lado,pos_y,90,dim*lado)
    turtle.hideturtle()
Elementar, não acha? Finalmente a orientação. Aqui decidimos usar um pouco dos nossos conhecimentos de trignometria, para definir as novas posições de início das linhas.
Na posse deste conhecimento a solução vem, finalmente, como:
import math

def grelha_7(dim, lado,pos_x,pos_y,orient):
    """ Solução com controlo da posição do canto inferior esquerdo e a orientação."""
    deg_rad = math.pi/180
    # horizontais
    for i in range(dim+1):
        linha(pos_x+i*lado*math.cos((orient+90)* deg_rad),pos_y+ i*lado*math.sin((orient+90)* deg_rad),orient,dim*lado)
    # verticais
    for i in range(dim+1):
        linha(pos_x+i*lado*math.cos(orient * deg_rad),pos_y+ i*lado*math.sin(orient * deg_rad),90+orient,dim*lado)
    turtle.hideturtle()
Chegados a este ponto podemos achar que o trabalho está feito e, por isso, podemos passar a outro problema. Mas… e se alguém nos pedir a nossa solução para criar um tabuleiro de xadrez? Precisamos colorir as células, mas como fazer? A dificuldade reside no facto de termos olhado para a grelha não como uma grelha, isto é formada por células justapostas, mas como linhas que se cruzam. E precisamos partir de novo à aventura: criar a dita grelha formada por quadrados. Mas aprendemos algo com o caso anterior, a saber: usar abstração para criar primitivas é positivo.
Para começar precisamos de uma primitiva para desenhar um quadrado:
def quadrado(pos_x, pos_y, lado, orient):
    # posiciona
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    turtle.setheading(orient)
    turtle.pendown()
    # desenha
    for i in range(4):
        turtle.forward(lado)
        turtle.lt(90)
    turtle.hideturtle()
E vamos percorrer de novo a nossa via sacra. Primeiro a versão básica:
def grelha_8(dim, lado):
    """ Solução básica."""
    # Por linhas
    for i in range(dim):
        # linha i
        for j in range(dim):
            quadrado(j*lado,i*lado,lado,0)    
    turtle.hideturtle()
Não muito diferente, certo? Só que agora temos uma perspectiva matricial, pelo que precisamos de um ciclo dentro de outro ciclo. Controlar a posição é trivial:
def grelha_9(dim, lado, pos_x, pos_y):
    """ Solução com controlo da posição do canto inferior esquerdo."""
    # Por linhas
    for i in range(dim):
        # linha i
        for j in range(dim):
            quadrado(pos_x+j*lado,pos_y+i*lado,lado,0)    
    turtle.hideturtle()
A quatro da orientação pede um pouco mais de atenção como na versão anterior, mas a lógica é semelhante: trata-se de definir os pontos iniciais para cada quadrado:
def grelha_10(dim, lado, pos_x, pos_y, orient):
    """ Solução com controlo da posição do canto inferior esquerdo e da orientação."""
    deg_rad = math.pi/180
    # Por linhas
    for i in range(dim):
        # linha i
        p_x = pos_x+i*lado*math.cos((orient+90)*deg_rad)
        p_y = pos_y+i*lado*math.sin((orient+90)*deg_rad)        
        for j in range(dim):
            quadrado(p_x,p_y,lado,orient)
            p_x = p_x+lado*math.cos(orient*deg_rad)
            p_y = p_y+lado * math.sin(orient*deg_rad)                   
    turtle.hideturtle()
Notar que nesta solução o ciclo interior apenas controla o número de vezes que desenhamos um quadrado numa linha.

. Agora sim podemos dar o trabalho por encerrado!!! Mas, espere aí, ouço-o dizer, a passagem para o ponto de vista das células quadradas não era para termos mais graus de liberdade, nomeadamente em relação à cor dos quadrados??? É verdade sim senhor. Então vamos a isso. A solução mais simples consistirá em poder desenhar quadrados coloridos…
def quadrado_cor(pos_x, pos_y, lado, orient,cor):
    # cor
    turtle.color(cor)
    # posiciona
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    turtle.setheading(orient)
    turtle.pendown()
    # desenha
    turtle.begin_fill()
    for i in range(4):
        turtle.forward(lado)
        turtle.lt(90)
    turtle.end_fill()
    turtle.hideturtle()
E agora eis o nosso tabuleiro bi-color:
def grelha_11(dim, lado, pos_x, pos_y, orient):
    """ Solução com controlo da posição do canto inferior esquerdo e da orientação."""
    deg_rad = math.pi/180
    # Por linhas
    for i in range(dim):
        # linha i
        p_x = pos_x+i*lado*math.cos((orient+90)*deg_rad)
        p_y = pos_y+i*lado*math.sin((orient+90)*deg_rad)        
        for j in range(dim):
            if (i+j)%2 == 0:
                quadrado_cor(p_x,p_y,lado,orient,'black')
            else:
                quadrado_cor(p_x,p_y,lado,orient,'gray')
            p_x = p_x+lado*math.cos(orient*deg_rad)
            p_y = p_y+lado * math.sin(orient*deg_rad)                   
    turtle.hideturtle() 
Veja apenas como conseguimos o efeito das cores alternadas… Experimente o código e aprecie o resultado.
Se quiser outro tipo de tabuleiros coloridos é só adaptar. Agora é mesmo a sua vez de fazer alguma coisa. Eu vou descansar um pouco!

quarta-feira, 18 de novembro de 2015

Teste #2 - Turma TP2

Pergunta 1
Diga o que são objectos mutáveis e exemplifique.
Resposta: São objectos que podem ter o seu valor alterado sem alterar a sua identidade. As listas são um exemplo.

b) Analise a listagem seguinte e diga, justificando, o que vai aparecer no lugar dos pontos de interrogação.
>>> x_2 = ([1,2,3],[4,5,6])
>>> x_2[0][1] = 'b'
>>> x_2
???             --> ([1,'b',3],[4,5,6])
>>> x_2[0] = (7,8,9)
???            --> Dá erro de atribuição. Não  se pode alterar, por atribuição,  o valor de um tuplo
Pergunta 2
Pretendia-se obter o valor aproximado da função geo(r) igual ao somatório com i a variar de 0 até infinito de 1/(r^i). Valorizava-se a consideração da precisão.
Versão sem precisão (soma de n termos):
def geo(r,n):
    res = 0
    for i in range(n):
        res += 1/pow(r,i)
    return res
Agora uma versão com a precisão a ser definida.
def geo_b(r,prec):
    erro = 1
    res = 0
    n = 0
    while erro > prec:
        aux = res
        res += 1/pow(r,n)
        erro = abs(aux - res)
        n += 1
    return res
Esta solução funciona com base na consideração da diferença de dois valores consecutivos. Para este tipo de séries infinitas isso não causa problema. Podemos mesmo usar outra solução:
def geo_c(r,prec):
    i = 1
    termo = res = 1
    while termo > prec:
        termo = 1/pow(r,i)
        res += termo
        i += 1
    return res
Pergunta 3
Pedia-se para transformar uma imagem a preto e branco, representada por uma lista de listas de uns (preto) e zeros (branco), na sua imagem no espelho, sem destruir a imagem original. Uma pequena reflexão diz-nos que é suficiente inverter a imagem, linha a linha.
def espelho(imagem):
    nova_imagem = []
    for linha in imagem:
        nova_linha = linha[:]
        nova_linha.reverse()
        nova_imagem.append(nova_linha)
    return nova_imagem

Teste #2 - Turma TP1

Pergunta 1

a) Diga o que entende por um objecto ser homogéneo. Indique um tipo de objectos que tem esta característica.
Resposta: quando as componentes do objecto têm quer ser todas do mesmo tipo. Exemplo: cadeias de caracteres.

b) Considere a seguinte listagem. Diga, justificando, o que vai aparecer no lugar dos pontos de interrogação.
>>> x_1 = [(1,2,3),(4,5,6)]
>>> x_1[0] = (7,8,9)
>>> x_1
??? --> [(7,8,9),(4,5,6)]
>>> x_1[0][1] = 'b'
??? --> Dá erro de atribuição. Não  se pode alterar, por atribuição,  o valor de um tuplo
Pergunta 2
Implemente um programa que calcula o valor aproximado da função zeta(s) igual ao somatório quando n varia de 1 a infinito de 1/(n^s). Valorizava-se uma solução que controlasse o erro máximo da solução.

Sem considerar a precisão, a solução baseia-se em somar um certo número de termos:
def zeta(s,k):
    res = 0
    for n in range(1,k+1):
        res += 1/pow(n,s)
    return res
Considerando agora a precisão:
def zeta(s,prec):
    erro = 1
    res = 0
    n = 1
    while erro > prec:
        aux = res
        res += 1/pow(n,s)
        erro = abs(aux - res)
        n += 1
    return res
Pergunta 3
Dadas duas imagens a preto e branco, representadas por uma lista de listas de uns (preto) e zeros (branco), construa uma nova imagem, sem destruir as anteriores, que é preta (um) numa dada posição apenas quando as duas imagens de entrada são ambas pretas nessa posição. A solução mais básica:
def intersecta(img_1, img_2):
    nova_img = []
    for i in range(len(img_1)):
        nova_linha = []
        for j in range(len(img_1[0])):
            if img_1[i][j] == 1 and img_2[i][j] == 1:
                nova_linha.append(1)
            else:
                nova_linha.append(0)
        nova_img.append(nova_linha)
    return nova_img
Uma alternativa que evita o teste dentro dos ciclos:
def intersecta(img_1, img_2):
    nova_img = []
    for i in range(len(img_1)):
        nova_linha = []
        for j in range(len(img_1[0])):
                nova_linha.append(img_1[i][j] * img_2[i][j])
        nova_img.append(nova_linha)
    return nova_img
Em vez do produto também se podia usar um AND lógico. Ainda outra alternativa seria construir uma imagem só com zeros e colocar a um sempre que o produto das outras for igual a um,, ou a soma for igual a 2. Fica para exercício.

terça-feira, 17 de novembro de 2015

Teste # 1 - Especial

Pergunta 2

Uma slot-machine tem três posições, podendo aparecer um de 5 objectos diferentes em cada posição. Pretendia-se uma programa que simulasse uma jogada indicando se se ganhou (todas as posições iguais) ou se se perdeu. Esta pergunta era muito simples, tanto mais que se dava a informação relativamente ao método choice do módulo random. Assim uma solução possível seria:
import random

def slot_machine():
    valores = 'ABCDE'
    res_1 = random.choice(valores)
    res_2 = random.choice(valores)
    res_3 = random.choice(valores)
    if res_1 == res_2 and res_2 == res_3:
        return True
    return False
Notar que os valores possíveis são fixos e o número de posições também (igual a 3). Podemos generalizar para qualquer conjunto de objectos e qualquer número de posições. Vejamos como.
def slot_machine_b(valores,k):
    res = ''
    for i in range(k):
        res += random.choice(valores)
    return all_equal(res)

def all_equal(cadeia):
    conta = cadeia.count(cadeia[0])
    return conta == len(cadeia)
Como se observa passamos a usar um ciclo for com uma variável a fazer de acumulador. Uma variante para este caso:
def slot_machine_c(valores,k):
    ganhar = [''.join(k * [str(elem)]) for elem in valores]
    res = ''
    for i in range(k):
        res += random.choice(valores)
    return res in ganhar
Aqui usamos listas por compreensão para gerar todas as sequências de vitória, o que facilita o teste de tudo igual.

Pergunta 3

Pediam-se dois programas usando o módulo turtle. Um primeiro, que permitia desenhar uma lâmina, parametrizando a sua posição central, o tamanho dos dois braços e a orientação. O segundo, devia usar o primeiro para desenhar diferentes tipos de ventoinhas. Comecemos pelo primeiro.
def lamina(cent_x,cent_y,lado_1,lado_2,angulo, orient):
    # inicialização
    turtle.showturtle()
    turtle.penup()
    turtle.goto(cent_x, cent_y)
    turtle.setheading(orient)
    turtle.pendown()
    turtle.dot(10)
    # lamina superior
    turtle.forward(lado_2//2)
    turtle.lt(angulo)
    turtle.forward(lado_1)
    # recentra
    turtle.penup()
    turtle.goto(cent_x, cent_y)
    turtle.setheading(orient+180)
    turtle.pendown() 
    # lamina inferior
    turtle.forward(lado_2//2)
    turtle.lt(angulo)
    turtle.forward(lado_1)  
    # finaliza
    turtle.hideturtle()
Limita-se a ser … comprido. Mas não é difícil. Os comentários dispensam mais explicações. Procure tornar o código mais sintético, por exemplo, usando um mesmo programa para desenhar as duas partes da lâmina.

Resolvida esta questão, o segundo programa é trivial.
def ventoinha(num, dist, cent_x,cent_y,lado_1,lado_2,angulo, orient):
    # inicialização
    turtle.showturtle()
    turtle.penup()
    turtle.goto(cent_x, cent_y)
    turtle.pendown()
    turtle.dot(10) 
    # desenha num lâminas
    for i in range(num):
        lamina_c(cent_x,cent_y,lado_1,lado_2,angulo, orient)
        orient += dist

segunda-feira, 16 de novembro de 2015

Imagens à roda

Todos nos habituámos a brincar com programas que manipulam imagens. Com frequência queremos um programa que roda uma imagem de 90 ou de 180 graus. Vamos ver como podemos resolver o primeiro caso. Depois o segundo é trivial.

Já discutimos como podemos representar uma imagem a preto e branco em Python: uma lista de listas. Para resolver este problema sabemos que temos que percorrer toda a imagem e para cada elemento da imagem saber qual será a sua nova posição. Mas no caso de uma rotação de 90 graus isso é fácil de resolver se olharmos globalmente para a matriz que representa a nossa imagem: a coluna i da matriz vai passar a ser a linha i da nova matriz… Daí a solução seguinte:
def roda_90(imagem):
    """Baseia-se na construção da transposta da imagem vista como uma matriz."""
    imagem_aux = list()
    # troca colunas por linhas = transpõe
    for coluna in range(len(imagem[0])):
        nova_linha = list()
        for linha in imagem:
            nova_linha.append(linha[coluna])
        imagem_aux.append(nova_linha)
    # inverte dentro das linhas
    for linha in range(len(imagem_aux)):
        imagem_aux[linha] = imagem_aux[linha][::-1]
    return imagem_aux
Esta solução numa ideia simples: percorrer por colunas e para cada coluna ir buscar os elementos das diferentes linhas. O modo como fazemos isto obriga-nos no final a inverter todas as linhas. Esta ultima parte não é simpática. Mas podemos mudar as coisas.
def roda_90_b(imagem):
    """Baseia-se na construção da transposta da imagem vista como uma matriz."""
    imagem_aux = list()
    # troca colunas e linhas = transpõe
    for coluna in range(len(imagem[0])):
        nova_linha = list()
        for linha in imagem:
            nova_linha = [linha[coluna]] + nova_linha
        imagem_aux += [nova_linha]
    return imagem_aux
Como se pode ver, em vez de usarmos append, que coloca sempre no final, usamos a operação de concatenação de listas (+), colocando o novo elemento sempre à cabeça, o que permite ter no final a nova linha já invertida.

Como temos vindo a explicar ao longo destes pequenos posts há sempre muitas alternativas. Algumas fazem uso de um conhecimento mais profundo de Python. É o caso da solução que apresentamos a seguir.
def roda_90_c(imagem):
    copia = copy.deepcopy(imagem)
    transposta = list(zip(*copia))
    final = [linha[::-1] for linha in transposta]
    return final
Neste exemplo, usamos um argumento na forma *copia. Isto significa que o objecto copia vai ser desmembrado nos seus elementos e é sobre esses elementos que vai ser aplicada a função zip. Deste modo obtemos a transposta. Depois é só inverter cada linha usando listas por compreensão. Refira-se finalmente que usámos uma cópia profunda da imagem inicial para não a destruir.

Cuidar da imagem...

Durante as aulas vimos como se podia representar uma imagem a preto e branco através de uma lista de listas de 0s e 1s. A questão concreta colocada na aula era a de produzir o negativo de uma imagem, sem destruir a imagem original. O exercício não era difícil. Temos que percorrer toda a imagem e efectuar a alteração dos 0s para 1s e os 1s para 0s. Como a estrutura é 2D vamos precisar de dois ciclos. Como não queremos a destruição da imagem original precisamos construir uma nova. Uma solução que respeite o enunciado é a seguinte:
def negativo(imagem):
    nova_imagem = []
    for linha in imagem:
        nova_linha = []
        for coluna in linha:
            if coluna == 0:
                nova_linha.append(1)
            else:
                nova_linha.append(0)
        nova_imagem.append(nova_linha)
    return nova_imagem
Como se pode ver a imagem é construída a partir de uma imagem vazia. Os dois ciclos são percorridos pelo conteúdo.

Podemos fazer de modo um pouco diferente. Criamos uma cópia da imagem inicial, e alteramos de acordo com o enunciado. Neste caso percorremos os ciclos for pela posição, pois estas são necessárias para a actualização da imagem. A cópia é feita usando o método deepcopy do módulo copy para assegurar que as duas imagens ficam totalmente separadas.
import copy

def negativo(imagem):
    copia = copy.deepcopy(imagem)
    for linha in range(len(imagem)):
        for coluna in range(len(imagem[0])):
            if copia[linha][coluna] == 0:
                copia[linha][coluna] = 1
            else:
                copia[linha][coluna] = 0
    return copia
Estas duas versões parecem esgotar as alternativas. Mas não é bem verdade isso. Podemos evitar o recurso ao teste no interior dos dois ciclos.
def negativo_b(imagem):
    copia = copy.deepcopy(imagem) 
    for linha in range(len(imagem)):
        for coluna in range(len(imagem[0])):
            copia[linha][coluna] = (copia[linha][coluna] + 1) %  2
    return copia
O que fizemos? Usemos o conhecimento de que o resto da divisão de um numero por dois ou é 0 ou é 1. No nosso caso somamos 1 ao conteúdo da matriz. Se for 0, fica 1. Se for 1 passa a 0! E pronto. Agora é que esgotámos as alternativas. Certo? Não, errado! Mais uma variante:
def negativo_c(imagem):
    copia = copy.deepcopy(imagem) 
    for linha in range(len(imagem)):
        for coluna in range(len(imagem[0])):
            copia[linha][coluna] ^= 1
    return copia
Esta solução é semelhante à anterior, são que usamos a operação binária ou exclusivo.

segunda-feira, 26 de outubro de 2015

Teste # 1 - TP2

Pergunta 2

Como encontrar os números perfeitos entree 1 e um dado n? Por número perfeito entende-se um número que é igual ao produto de dois inteiros iguais. Por exemplo, 25 = 5 * 5 é um número perfeito. Outra forma de definir seria dizer que é um número cuja raiz quadrada é um inteiro.

Uma solução muito simples será calcular os sucessivos produtos de um inteiro por si próprio filtrando os resultados menores ou iguais a n.
def perfeito(n):
 for i in range(1,n+1):
  prod = i * i
  if prod <= n:
   print(n)
Fácil, certo? Mas será que vale a pena repetir i ciclo n vezes?? Por exemplo, se n=100 é claro que basta testar os inteiros … até 10, ou seja,até à raiz quadrada de 100. Daí uma nova solução.
import math

def perfeito(n):
 sup = int(math.sqrt(n))
 for i in range(1,sup+1):
  prod = i * i
  if prod <= n:
   print(n)
E pronto. Notar apenas que temos que garantir que os argumento de range são números inteiros.

Pergunta 3

Esta pergunta é semelhante à colocada na TP1. Daí que apresentemos apenas a solução. O leitor deve referir-se ao texto anterior caso não entenda o que é apresentado. Dito isto, chamamos a atenção para o modo como conseguimos que a formação seja diferente, alterando apenas o modo de modificar a nova posição.
import turtle

def triangulo(lado,orientacao,pos_x,pos_y,cor):
    # inicializa tartaruga
    turtle.showturtle()
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    turtle.setheading(orientacao)
    turtle.colormode(255)
    turtle.pencolor(cor)
    turtle.pendown()
    # desenha triangulo
    for i in range(3):
        turtle.forward(lado)
        turtle.left(120)
    turtle.hideturtle()
    
def boneco(lado,orientacao,pos_x,pos_y,cor,n):
    for i in range(n):
        triangulo(lado,orientacao,pos_x,pos_y,cor)
        pos_x = pos_x - lado//5
        pos_y = pos_y - lado//5
        orientacao = orientacao + 15
        lado = lado + 10

if __name__ == '__main__':
    #triangulo(40,45,25,50,(0,255,255))
    boneco(50,0,0,0,(0,0,255),10)
    turtle.exitonclick()

Teste #1 - TP1

Pergunta 2

Era pedido um programa que calculasse a percentagem de ocorrências de caras no lançamento de uma moeda ao ar. O número dos lançamentos era um parâmetro do programa.

Não era preciso pensar muito para perceber que: (a) vamos precisar de simular o lançamento da moeda um número fixo de vezes e, (b) contar o número de vezes em que ocorreu caras. Um modelo inicial de solução poderá ser:
def caras(n):
 # inicializa contagem
 for i in range(n):
  # lança moeda
  if #caras?:
   # actualiza contagem
 return #percentagem
Aprofundando um pouco mais a solução, torna-se claro que vamos precisar de um acumulador que conte o número de vezes em que saiu caras:
def caras(n):
 # inicializa contagem
 conta = 0
 for i in range(n):
  # lança moeda
  if #caras?:
   # actualiza contagem
   conta = conta + 1
 return conta/n * 100
E estamos perante o padrão ciclo - acumulador!
Simular o lançamento pode ser conseguido recorrendo ao módulo random e ao método randint.
import random 

def caras(n):
 # inicializa contagem
 conta = 0
 for i in range(n):
  # lança moeda
  resultado = random.randint(0,1)
  if resultado == 0:
   # actualiza contagem
   conta = conta + 1
 return conta/n * 100
Nesta solução admitimos que 0 significa caras. Também podíamos usar outra representação, usando agora o método choice.
import random 

def caras(n):
 # inicializa contagem
 conta = 0
 for i in range(n):
  # lança moeda
  resultado = random.choice(’caras’,’coroas’)
  if resultado == ‘caras’:
   # actualiza contagem
   conta = conta + 1
 return conta/n * 100
Pergunta 3

Este problema tinha duas partes. Primeiro, escrever um programa que permite desenhar um quadrado, tendo como parâmetros o tamanho do lado, a cor do traço, a orientação e a posição. Segundo, usar este programa como auxiliar para desenhar vários quadrados, alterando os parâmetros em cada um de modo a desenhar uma forma semelhante a um nautilus. Divididas as questões deste modo a primeira questão era trivial (e já resolvida nas aulas,…).
import turtle

def quadrado(lado,orientacao,pos_x,pos_y,cor):
    # inicializa tartaruga
    turtle.showturtle()
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    turtle.setheading(orientacao)
    turtle.colormode(255)
    turtle.pencolor(cor)
    turtle.pendown()
    # desenha quadrado
    for i in range(4):
        turtle.forward(lado)
        turtle.right(90)
    turtle.hideturtle()
Como se pode ver, no início inicializamos a tartaruga de acordo com os parâmetros, para de seguida desenhar o quadrado. Não há muito mais que se possa dizer.

Quanto à segunda parte do problema só tínhamos que perceber que se trata de repetir o desenho do quadrado com diferente orientação, posição e tamanho. E foi para isso que escrevemos o primeiro programa…
def boneco(lado,orientacao,pos_x,pos_y,cor,n):
    for i in range(n):
        # Desenha Quadrado
        quadrado(lado,orientacao,pos_x,pos_y,cor)
        # Actualiza parâmetros
        pos_x = pos_x + lado//5
        pos_y = pos_y + lado//5
        orientacao = orientacao + 15
        lado = lado + 10
Muitos não entenderam o significado da expressão “utilize o primeiro programa como auxiliar”. Espero que com o exemplo concreto tenham agora percebido.

sábado, 17 de outubro de 2015

Erros Comuns (de novo)

Em textos anteriores, em novembro de 2011, descrevemos um conjunto de erros muito frequentes, uns mais graves do que outros, uns cometidos por novatos em programação, outros por programadores mais experientes. Aconselhamos os leitores que estão a fazer a sua formação inicial a passarem os olhos por esses posts. Vou agora referir dois erros graves cometidos por novatos, ligados aos conceitos de parâmetros formais e parâmetros reais.
def toto(n):
    n = 5
....
Para que serve o argumento (parâmetro formal n) se logo a seguir, no início do código da função, a associação é destruída??? Não serve para nada!
def toto(5):
....
Este erro radica no desconhecimento de que os parâmetros formais (argumentos das funções) são apenas nomes que durante a activação da função vão ficar associados a objectos.
Espero não encontrar estes erros no teste da próxima terça-feira...

Codifica e descodifica

[Problema 3.18 (dos apontamentos)] Um método muito simples para codificar/descodificar um texto baseia-se na ideia de usar uma chave de substituição. Uma variante básica do método consiste em substituir uma letra por outra que se encontre a uma certa distância dela. Por exemplo, se a distância for 2, então o c substitui o a, o d substitui o b e assim sucessivamente. Vamos escrever um programa que dado um texto e um inteiro que representa a distância, devolve o texto encriptado de acordo com essa distância.

Uma maneira de pensar a solução remete para o padrão ciclo-acumulador: usamos uma variável onde vai sendo acumulado o texto codificado, sendo que o processo de acumulação é guiado por um ciclo em que cada caractere do texto original é analisado sequencialmente.
def codifica(texto_normal,chave):
    """Codifica um texto pelo método de substituição. A chave é a distância
    para codificar. Exemplo: 'a' passa a 'c' se chave for 2."""
    texto_enc =''
    for car in texto_normal:
        texto_enc = texto_enc + substitui(car,chave)
    return texto_enc
Este programa funciona caso a função substitui fizer o pretendido: substituir um caractere pelo seu substituto de acordo com a chave. Podemos testar o programa desde que façamos uma implementação preliminar desta função auxiliar:
def substitui(car,chave):
 return ‘a’
Se testarmos o resultado será um texto do mesmo comprimento que o original formado apenas pelo caractere a. Nada de muito entusiasmante! No entanto, esta forma de proceder permite testar a lógica da nossa solução. Vamos então implementar a sério a função. A nossa solução usa o facto de cada caractere ter um código numérico. Por exemplo, o a tem como código 97. Existem duas funções em Python que nos vão ajudar: ord(car), fornece o código do car e chr(num) que dá o caractere que tem num como código.
def substitui(car,chave):
    return chr(ord(car)+chave)
E já está! Suponhamos que queremos também obter o original de um texto codificado de acordo com uma dada chave. Trivial:
def descodifica(texto_normal,chave):
    """Descodifica um texto pelo método de substituição. A codificação foi feita de acordo 
    com a seguinte estratégia. A chave é a distância
    para codificar. Exemplo: 'a' passa a 'c' se chave for 2."""
    texto_enc =''
    for car in texto_normal:
        texto_enc = texto_enc + substitui(car,-chave)
    return texto_enc
Como se pode verificar esta função é quase idêntica à sua inversa. A única coisa que mudou é que na chamada da substitui usamos o simétrico (-chave) como valor.

Esta solução é um pouco limitada e supõe que o alfabeto é infinito à esquerda e à direita. Com efeito, se tivermos um caractere que tem o código de valor máximo (ou mínimo) qual vai ser o seu substituto. Por outro lado, existem códigos que não correspondem a letras, mas antes a sinais ou caracteres de controlo. Que fazer? Em baixo apresentamos uma solução para estas questões. Passa por definir explicitamente o alfabeto, e usar o método de procura do índice de um caractere numa cadeia (alfabeto). Por outro lado, para codificar os caracteres nos extremos do alfabeto, usamos a aritmética módulo tamanho do alfabeto (operação %). Na prática, isto corresponde a termos os caracteres organizados de forma circular.
def codifica(texto_normal,chave):
    """Codifica um texto pelo método de substituição. A chave é a distância
    para codificar. Exemplo: 'a' passa a 'c' se chave for 2. Supõe que
    os caracteres são as 26 letras (minúsculas) do alfabeto  e o branco."""
    alfabeto = 'abcdefghijklmnopqrstuvwxyz '
    texto_encriptado = ''
    for car in texto_normal:
        novo_codigo = (alfabeto.index(car) + chave) % len(alfabeto)
        novo_car = alfabeto[novo_codigo]
        texto_encriptado = texto_encriptado + novo_car
    return texto_encriptado
Notar que nesta solução não usamos nenhuma função auxiliar. Mas isso pode ser feito sem problemas, considerando que é a função substitui que sabe qual é o alfabeto. Fica para o leitor a escrita do equivalente para descodificar.

sexta-feira, 16 de outubro de 2015

Tiro ao alvo

Já todos jogamos com arco e flechas. O objectivo é acertar num alvo colocado a uma certa distância. A nossa ideia é simular com um programa de computador o treino de um jogador. Para começar vamos supor que apenas se pretende calcular a percentagem de vezes que acerta no alvo. Assumimos que o alvo tem a forma de um circulo. Podemos começar com uma primeiro esboço do programa.
def tiro_alvo(raio, num):
 # inicialização
 for i in range(num):
  # atira flecha
  # verifica se acertou
        pass
 # calcula percentagem de acertos
Temos assim que resolver vários subproblemas. Comecemos pelo mais simples: calcular a percentagem. Isso envolve contar os acertos e dividir pelo número total de lançamentos.
def tiro_alvo(raio, num):
 # inicialização
 conta = 0
 for i in range(num):
  # atira flecha
  # verifica se acertou
 # calcula percentagem de acertos
 return conta / num
Estamos perante o padrão ciclo-acumulador.

Como simular o lançamento da flecha? Uma hipótese simples é escolher de modo aleatório as coordenadas do ponto onde vai incidir a flecha. Recorremos ao módulo random.
import random

def tiro_alvo(raio, num):
 # inicialização
 conta = 0
 for i in range(num):
  # atira flecha
  x = random.randint(-raio,raio)
  y = random.randint(-raio,raio)
  # verifica se acertou
 # calcula percentagem de acertos
 return conta / num
Na solução acima escolhemos considerar que os pontos onde vai cair a flecha se encontra num quadrado de lado 2*raio e centrado no ponto (0,0). Falta agora verificar se acertamos no alvo ou não. A solução passa por calcular a distância (euclidiana) do ponto ao centro. Se for maior do que o raio, então estará fora do alvo.
import random
import math

def tiro_alvo(raio, num):
 # inicialização
 conta = 0
 for i in range(num):
  # atira flecha
  x = random.randint(-raio,raio)
  y = random.randint(-raio,raio)
  # verifica se acertou
  if math.sqrt(x**2 + y**2) < raio:
   conta = conta + 1
 # calcula percentagem de acertos
 return conta / num
E pronto, chegámos ao fim. Como se observou é possível construir uma solução por etapas a partir de um modelo geral.

Podemos tornar o problema ligeiramente diferente, considerando um alvo mais realista contendo várias zonas, sendo que cada uma tem um valor associado: vale tanto mais o nosso lançamento quanto mais perto estiver do centro. Na solução que vamos apresentar considerámos que existem três zonas. Por outro lado, agora o resultado pretendido é a soma de pontos obtido por um jogador.
def tiro_ao_arco_2(raio_1,raio_2,raio_3,num):
    pontos = 0
    for i in range(num):
        # atira flecha
        x = random.randint(-raio_1,raio_1)
        y = random.randint(-raio_1,raio_1)
        dist_centro = math.sqrt(x**2 + y**2)
        if dist_centro <= raio_3:
            pontos = pontos + 50
        elif dist_centro <= raio_2:
            pontos = pontos + 20
        elif dist_centro <= raio_3:
            pontos = pontos + 10
        else:
            pontos = pontos - 5
    return pontos
A partir daqui pode imaginar as variantes que quiser, incluindo desenhar, usando o turtle o alvo e a localização das flechas…

quinta-feira, 15 de outubro de 2015

Uma tartaruga na prisão

Nas aulas vimos como podíamos resolver o problema de ter uma tartaruga num passeio aleatório mas sem poder sair de uma prisão circular. A nossa solução, muito simples, baseou-se no uso das primitivas distance(x,y) e towards(x,y). A primeira, permite determinar a distância entre a posição actual da tartaruga e a posição (x,y). A segunda, calcula o ângulo entre a linha que une a posição corrente da tartaruga e (x,y) com o eixo dos X. Eis o código básico:
import turtle
import random

def random_walk_prison(raio,pos_x,pos_y,ang,step,num):
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    turtle.setheading(ang)
    turtle.pendown()
    for i in range(num):
        if turtle.distance(pos_x, pos_y) > raio:
            turtle.setheading(turtle.towards(pos_x,pos_y))
        turtle.forward(step)
Esta solução permite controlar a dimensão da prisão (raio), a posição inicial da tartaruga (pos_x e pos_y), o ângulo inicial (ang), a dimensão de cada passo (step) e o número de passos da tartaruga (num). O código é fácil de entender: enquanto não tivermos esgotado o número de passos concedido, avançamos. Se ultrapassarmos o limite autorizado voltamos para trás na direção de onde partimos. Notar que a primitiva towards não altera a orientação, pelo que é necessário usar o setheading.

Se executarmos este programa o resultado não é muito entusiasmante: a tartaruga anda para trás e para a frente ao longo de uma linha. Tudo menos um caminhar aleatório. Vamos por isso pensar em melhorar o código. Começamos por coisas simples: mudar a forma e a cor da tartaruga e retirar o rasto.
def random_walk_prison_1(raio,pos_x,pos_y,ang,step,num):
    turtle.setheading(ang)
    turtle.shape('turtle') 
    turtle.color('blue')
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    for i in range(num):
        if turtle.distance(pos_x, pos_y) > raio:
            turtle.setheading(turtle.towards(pos_x,pos_y))
        turtle.forward(step)
Não se pode dizer que esteja muito melhor. Vamos então introduzir alguma variabilidade no caminhar da tartaruga, fazendo com que se movimente sem ser apenas para a frente e para trás.
def random_walk_prison_2(raio,pos_x,pos_y,ang,step,num):
    turtle.setheading(ang)
    turtle.shape('turtle') 
    turtle.color('blue')    
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    for i in range(num):
        if turtle.distance(pos_x, pos_y) > raio:
            offset = random.randint(-10,10)
            turtle.setheading(turtle.towards(pos_x,pos_y) + offset)
        turtle.forward(step)
Como se pode ver recorremos ao método randint do módulo random. Já nos parece mais razoável, mas podemos melhorar ainda o programa, desenhando a prisão. No nosso caso desenhamos uma circunferência de raio definido pelo utilizador. Também alteramos a espessura do traço.
def random_walk_prison_3(raio,pos_x,pos_y,ang,step,num):
    # prisão
    turtle.penup()
    turtle.goto(pos_x,pos_y-raio-step)
    turtle.pendown()
    turtle.width(5)
    turtle.pencolor('red')
    turtle.circle(raio+step)
    # tartaruga
    turtle.setheading(ang)
    turtle.shape('turtle') 
    turtle.color('blue')    
    turtle.penup()
    turtle.hideturtle()
    turtle.goto(pos_x,pos_y)
    turtle.showturtle()
    for i in range(num):
        if turtle.distance(pos_x, pos_y) > raio:
            offset = random.randint(-10,10)
            turtle.setheading(turtle.towards(pos_x,pos_y) + offset)
        turtle.forward(step)
E pronto… ou talvez não. Porque é que a tartaruga tem que ter o centro da prisão sempre como referência?? Não tem, pois claro! Então vamos alterar isso.
def random_walk_prison_4(raio,pos_x,pos_y,ang,step,num):
    # prisão
    turtle.penup()
    turtle.goto(pos_x,pos_y-raio-step)
    turtle.pendown()
    turtle.width(5)
    turtle.pencolor('red')
    turtle.circle(raio+step)
    # tartaruga
    turtle.setheading(ang)
    turtle.shape('turtle') 
    turtle.color('blue')    
    turtle.penup()
    turtle.goto(pos_x,pos_y)
    for i in range(num):
        if turtle.distance(pos_x, pos_y) + step >= raio:
            offset = random.randint(-10,10)
            turtle.setheading(turtle.heading() + 180 + offset )
            print(turtle.heading())
        turtle.forward(step)
Note como mandamos para trás a tartaruga. A partir daqui … use a sua imaginação.

quarta-feira, 16 de setembro de 2015

Porquê Python?

Muitas vezes me perguntam porquê a opção por Python como linguagem para a cadeira introdutória de programação. Poderia indicar muitas razões (simplicidade da sintaxe, legibilidade do código, não força nenhum paradigma de programação, vários módulos que expandem a linguagem, adoptada por grandes escolas e empresas, ...). Mas sugiro que procurem por opiniões de pessoas externas à disciplina. Deixo aqui uma referência:
Lorena Barba: Porquê Python?

Boas leituras!

Cá estamos de novo

Mais um ano que começa.
A finalidade deste blogue não se alterou: servir de apoio à cadeira de IPRP, da licenciatura em Engenharia Informática da Universidade de Coimbra.

sexta-feira, 30 de janeiro de 2015

Exame Recurso - Pergunta 1b

As respostas dadas pela generalidade dos(as) alunos(as) denota uma incompreensão de aspectos básicos e fundamentais de Python. Em Python tudo são objectos, e os objectos têm atributos. O nome é um dos atributos, e é através do nome que chegamos a outro valor, o valor. Assim, temos por exemplo:
>>> x = 5
>>> x
5
>>> a = ‘xpto’
>>> a
xpto
Neste exemplo, criámos objectos (5, ‘xpto’) e demos-lhes um nome (x, a).

Mas também se fizermos:
>>> def acme(n):
…  return 2*n
…
>>>
Criamos também um objecto, agora do tipo função, e estamos a dar-lhe o nome acme. Se agora perguntarmos pelo valor do objecto de nome acme o que recebemos de volta é o descritor da função:
>>> acme
<function acme at 0x102778488>
Então, no caso da pergunta do exame, quando fazemos:
a = funcao
Estamos a dizer que a é um outro nome para o objecto de tipo função, funcao. Logo, ao fazermos:
>>> print(a)
o que vai aparecer é a impressão do valor associado ao objecto de nome a, que é o mesmo do associado a funcao:
>>> def funcao():
…    print('128')
…
>>> a = funcao
>>> a
<function funcao at 0x102778510>
Notar que pode agora usar o nome a em vez de funcao e obter o mesmo resultado:
>>> funcao()
128
>>> a()
128

Exame Recurso - Pergunta 1a

Nesta pergunta estavam envolvidos vários conceitos: alcance das variáveis (local, global), passagem de parâmetros (real, formal) e mutabilidade. Na primeira situação, na chamada da função f1, a lista l (variável global, mutável) é usada como parâmetro real. Deste modo, no início da execução da função, l e lista são nomes de objectos que têm a mesma identidade. No entanto, na primeira instrução de f1 a atribuição lista = [1,2,3] faz com que os nomes ficam associados a objectos diferentes. Deste modo, todas as alterações a lista não afectam l, pelo que na linha 12 vai aparecer o valor (inalterado) de l = [1,2].

Na segunda situação, ao contrário da anterior, não é desfeita a ligação dos nomes l e lista ao mesmo objecto. Tratando-se de um objecto mutável, todas alterações feitas ao parâmetro formal serão reflectidas no parâmetro real, pelo que ao acrescentarmos 3 à lista usando o método append, essa alteração vai também afectar l. Logo, na linha 17, vai aparecer [1,2,3].

quinta-feira, 8 de janeiro de 2015

Teste Final - Pergunta 4

Esta pergunta envolvia manipular um ficheiro em que cada linha é formada por um nome seguido de um ou mais números inteiros. O objectivo é criar um novo ficheiro, deixando o antigo inalterado, adicionando no final de cada linha a soma dos números dessa linha. Valorizava-se uma solução em que as linhas no ficheiro eram reordenadas por ordem crescente das somas.

Solução:

A solução apresentada inclui o ordenamento das linhas. Para isso usamos o método itemgetter do módulo operator. Começamos por percorrer o ficheiro linha a linha, calculando a soma dos números e guardando o resultado numa lista. É feito o ordenamento da lista de seguida. Na terceira parte, criamos o novo ficheiro e guardamos a informação.
from operator import itemgetter

def trata_ficheiro(ficheiro,novo_ficheiro):
    with open(ficheiro,'r') as f_in:
        novos_dados = list()
        for linha in f_in:
            lista_linha = linha[:-1].split()
            valor = sum([int(elem) for elem in lista_linha[1:]])
            lista_linha.append(valor)
            novos_dados.append(lista_linha)
    
    novos_dados.sort(key=itemgetter(-1))
    with open(novo_ficheiro,'w') as f_out:
        for linha in novos_dados:
            linha[-1] = str(linha[-1]) + '\n'
            cadeia = ' '.join(linha)
            f_out.write(cadeia)
            
    return novos_dados

Teste Final - Pergunta 3

Guardamos num dicionário as classificações que diversos filmes receberam dos críticos (um valor entre 1 e 5). Queremos manter o dicionário actualizado e saber qual o filme com melhor pontuação média. Só podem ser candidatos a melhor filme os que tiverem pelo menos 3 valores.

Solução:

A solução abaixo usa o setdefault para actualizar o dicionário das classificações. Deste modo fica resolvida a questão de incluir uma primeira classificação ou simplesmente actualizar. Actualizado o dicionário, ele é percorrido, item a item, para determinar o melhor, prevendo o caso de não ter as 3 classificações.
def melhor_filme(filmes, classific):
    # actualiza
    nome = classific[0]
    valores = classific[1:]
    filmes[nome] = filmes.setdefault(nome,[]) + valores
    # calcula melhor
    m_filme = ''
    m_pont = 0
    for nome,pontos in filmes.items():
        comp = len(pontos)
        if comp < 3:
            continue
        media = (sum(pontos)/comp)
        if media > m_pont:
            m_filme = nome
            m_pont = media
    return (m_filme, m_pont)

Teste Final - Pergunta 2

Simular um jogo em que se atiram dardos a um alvo. No caso do teste o problema era mais simples pois não se pretendia simular o jogo mas apenas desenhar o alvo e simular o lançamento de um dardo. E fazer isto usando o módulo turtle. E construindo o programa de forma modular.

Vamos então dividir a solução em duas partes: uma para desenhar o alvo, e outra para simular o lançamento do dardo. Por outro lado, vamos fazer com que o desenho do alvo seja feito à custa de uma mesma função, a que desenha um círculo de uma dada cor, numa dada posição e com o dado raio. Essa função chama-se circunf. A partir dela desenhar o alvo é fácil (função alvo). Estas última precisa saber apenas o centro do alvo e o valor dos raios. Os cuidados a ter são desenhar as circunferências da maior para a mais pequena (notar o ordenamento inverso da lista dos raios), e controlar a alternância das cores.

O lançamento do dardo recorre ao módulo random para geral a posição, e ao comando dot para marcar a localização do dardo. Neste último caso, não conhecendo o comando dot, podia socorrer-se uma vez mais ao desenho de uma circunferência…
import turtle

def circunf(posx, posy, raio, cor):
    # posiciona
    turtle.pu()
    turtle.goto(posx,posy-raio)
    turtle.pd()
    # define cor e desenha
    turtle.fillcolor(cor)
    turtle.begin_fill()
    turtle.circle(raio)
    turtle.end_fill()
    turtle.hideturtle()
    
def alvo(posx, posy,raios):
    """
    raios: lista dos raios dos discos
    """
    raios.sort(reverse=True)
    cor = 'black'
    for raio in raios:
        circunf(posx,posy,raio,cor)
        if cor == 'black':
            cor = 'white'
        else:
            cor = 'black'

            
def dardo():
    import random
    posx = random.randint(-100,100)
    posy = random.randint(-100,100)
    turtle.pu()
    turtle.goto(posx,posy)
    turtle.pd()
    turtle.dot(8,'red')
    turtle.hideturtle()


if __name__ == ‘__main__’:
    alvo(0,0,[20,40,60,80])
    dardo()
    turtle.exitonclick()
Como se pode ver pelo código o programa é facilmente generalizável. Alvo de vários tamanhos (directo) e cores (passando as cores para argumentos), por exemplo.

Teste Final - Pergunta 1a

Pedia-se para dizer o que eram e para que serviam as excepções e as asserções. Por outro lado, também se pretendia saber as suas semelhanças e as suas diferenças, e ainda a apresentação de um exemplo concreto de cada.

De um modo geral excepções e asserções são formas de controlar possíveis erros ou verificar condições num programa.

As excepções são instruções de controlo, enquanto as asserções são instruções simples. As excepções permitem que o programa continue a ser executado, mesmo na presença de um erro, pois prevê acções de remediarão. No caso das asserções isso não acontece, pois o programa termina no caso da asserção não se verificar. Assim estas últimas são sobretudo usadas para depurar os programas.

As excepções são activadas (raise) quando durante a execução se verifica um erro. Os erros podem ser de diversos tipos, podendo as excepções ser genéricas ou dirigidas a um ou mais tipos de erro. Um exemplo simples e clássico é prevenir erros nas operações:
def divisao_protegida(x,y):
 try:
  res = x/y
  return res
 except ZeroDivisionError:
  print(‘ERRO: divisão por zero’)
No caso das asserções elas permitem verificar condições para que um programa possa correr. Um exemplo, também clássico, permite evitar um ciclo infinito:
def factorial(x):
 assert (x >=0), ‘ERRO: número negativo’
 res = 1
 while x:
  res *= x
  x -= 1
 return res

Teste Final - Pergunta 1b

A pergunta 1b do teste pedia para dizer, justificadamente, o que aparecia no lugar dos pontos de interrogação na sessão no interpretador seguinte:
>>> def xpto(x):
…    x += 1
…   print(x)
…   return
…
>>> x = 7
>>> print(xpto(x))
>>> ??? # —> 8
>>> ??? # —> None
>>> x
>>> ??? # —> 7
>>>
À frente dos pontos de interrogação colocámos o resultado real. Das respostas fica a certeza de que ainda existe muita confusão conceptual.

Vamos por partes.

Na sessão começamos por definir a função xpto. A função tem um parâmetro formal de nome x. O corpo da função consiste no incremento de uma unidade do parâmetro formal, seguido pela impressão do seu valor, terminando pela instrução de return, neste caso sem expressão associada.

De seguida associamos o nome x ao objecto 7.

Mandamos depois executar a função xpto passando como parâmetro real o nome x, pedindo ainda para imprimir o resultado da execução.

No final, queremos saber qual o valor associado ao nome x.

Que conceitos estão em jogo?

Desde logo o nome x fora da definição é uma variável do ambiente global, enquanto o parâmetro formal, com o mesmo nome x, é uma variável local à definição. Por outro lado, ao ser associado ao objecto 7, o nome x corresponde a um objecto imutável.

O que acontece quando mandamos executar a função xpto? É feita a passagem de parâmetros, que em Python significa que a identidade do x global (parâmetro real) é passada para o x local (parâmetro formal). Repito: a identidade! De seguida é executada a instrução x += 1. Como se trata de um objecto imutável as identidades do x global e do x local ficam diferentes. Os dois objectos, embora com o mesmo nome, estão guardados em zonas diferentes do espaço dos objectos!

Da execução de xpto resulta a instrução print(x) que vai imprimir o novo valor de x (local), ou seja 8.

Por outro lado, todas as funções devolvem sempre um resultado. Caso não exista return, ou a instrução de return não tenha uma expressão associada, o valor devolvido pela função é sempre o mesmo, o objecto None. Esse objecto é passado à instrução print que faz o que lhe compete, imprimindo … None.

Depois de executada a função xpto os nomes locais à definição (incluindo os parâmetros formais) deixam de estar associados a objectos. Independentemente disso, como já existia separação entre os objectos quando se tenta saber o valor do x “externo” o seu valor continua a ser o anteriormente definido, ou seja, 7. Isto tudo porque o objecto associado é imutável!!!

Portanto, a passagem de parâmetros não se faz por cópia, mas sim por passagem da identidade (referência) do parâmetro real ao parâmetro formal. Por outro lado não é o facto de uma variável ser global e outra local que explica o que aconteceu

Entendeu? Para tirar dúvidas veja o que acontece na situação seguinte:
>>> def xpto2(x):
…    x.append('a')
…    print(x)
…
>>> x = [1,2,3]
>>> print(xpto2(x))
[1, 2, 3, 'a']
None
>>> x
[1, 2, 3, ‘a']