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.