domingo, 28 de novembro de 2010

Problema 5.27

Como podemos gerar uma chave de encriptação? Eis a nossa primeira solução:

import random

def cria_chave():
"""Devolve uma chave para encriptar mensagens. Constrói uma permutação."""
alfabeto = 'abcdefghijklmnopqrstuvwxyz '
chave = ''
while alfabeto:
# escolhe próximo caracter
indice = random.randint(0,len(alfabeto)-1)
car = alfabeto[indice]
# actualiza chave
chave = chave + car
# actualiza alfabeto
alfabeto = alfabeto.replace(car,'')
return chave


Consegue perceber a ideia? É fácil. Escolhemos ao acaso uma letra do alfabeto e adicionamos à nossa permutação. Retiramos de seguida a letra do alfabeto. Repetimos o processo até esgotarmos as letras. Notar que usamos um espaço em branco!

Outra forma de fazer, agora com um ciclo for.

def cria_chave_2():
"""Devolve uma chave para encriptar mensagens. Constrói uma permutação."""
alfabeto = 'abcdefghijklmnopqrstuvwxyz '
chave = ''
for i in range(len(alfabeto)):
indice= random.randint(0,26-i)
chave = chave + alfabeto[indice]
alfabeto = alfabeto[:indice] + alfabeto[indice + 1:]
return chave

Analise as diferenças. Qual das duas versões é a sua preferida, e porquê?

Problema 5.22

Contar o número de caracteres, palavras e linhas num texto é um exercício interessante. Na sua aparente simplicidade coloca vários desafios, obrigando-nos a pensar nas diferentes situações que envolvem espaços em branco e mudanças de linha. Vamos mostrar várias soluções, algumas passam por simplificar o problema, um método que é de grande utilidade.

Vejamos uma primeira solução a que chamamos ingénua, pois parte da ideia de que o caracter ‘\n’ indica uma linha e um espaço em branco indica uma palavra.


def wc(texto):
"""Calcula o número de linhas, palavras e caracteres num texto. Não inclui
espaços em branco. Versão ingénua!!!!"""
num_caracteres = 0
num_palavras = 0
num_linhas = 0
for car in texto:
if car == '\n':
num_linhas = num_linhas + 1
elif car == ' ':
num_palavras = num_palavras + 1
else:
num_caracteres = num_caracteres + 1
return (num_caracteres, num_palavras, num_linhas)

Agora uma versão mais sofisticada.

def wc_2(texto):
"""Calcula o número de linhas, palavras e caracteres num texto.
Não inclui espaços em branco"""
num_caracteres = 0
num_palavras = 0
num_linhas = 0
comp_texto = len(texto)
posicao = 0
while posicao < comp_texto:
# analisa por casos
while (posicao < comp_texto ) and (texto[posicao] == ' '):
posicao = posicao + 1

while (posicao < comp_texto ) and (texto[posicao] == '\n'):
num_linhas = num_linhas + 1
posicao = posicao + 1

while (posicao < comp_texto) and (texto[posicao] != ' ') and (texto[posicao] != '\n'):
enc_car = True
num_caracteres = num_caracteres + 1
posicao = posicao + 1

if enc_car:
num_palavras = num_palavras +1
enc_car = False
return (num_caracteres, num_palavras, num_linhas)

Neste caso tratamos de modo mais claro várias linhas em branco ou vários espaços em branco consecutivos.

Mas será que precisamos de complicar assim tanto? Felizmente não! Basta saber um pouco mais sobre os métodos que se aplicam a cadeias de caracteres.... Então se pudermos usar split e splitlines:

def wc_3(texto):
"""Devolve o número de caracteres, palavras e linhas em texto. Inclui espaços em branco."""
return (len(texto.replace('\n','')), len(texto.split()), len(texto.splitlines()))

def wc_4(texto):
"""Devolve o número de caracteres, palavras e linhas em texto. Não inclui espaços em branco."""
texto_aux = texto.replace(' ','') # assim não inclui espaços em branco
return (len(texto_aux.replace('\n','')), len(texto.split()), len(texto.splitlines()))

Nestas duas versões a segunda não conta os espaços em branco. Atente-se que tivemos que criar uma cópia do texto original, o que, com ficheiros grandes não é muito simpático.

Problema 5.19

Pretendemos retirar as vogais que aparecem num dado texto. Esta solução mostra como por vezes a solução pode ser bastante simples:

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

Como se pode ver usamos as vogais para conduzir o processo de eliminação. Isto evita ter que andar num longo percurso do texto, caracter a caracter, e fazer um não menos complexo processo de selecção com ifs. Acresce que esta solução é mais geral. Se quisermos um programa que elimine um subconjunto de caracteres basta alterar uma instrução e uns pequenos ajustes, ou, melhor ainda, passar esses caracteres para parâmetro (formal).


def retira_caracteres(cad, caracteres):
""" Retira os caracteres numa cadeia, substituindo-as por espaços em branco."""
for ch in caracteres:
cad = cad.replace(ch,' ')
return cad

Problema 5.16

Vamos simular o comportamento da nossa tartaruga como resposta a comandos, mas agora com o comprimento e o ângulos determinados aleatoriamente entre valores de referência definidos como constantes.


import turtle
import random

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

Basta agora criar uma tartaruga, definir a sequência de comandos (no nosso casa escolhemos para representação cadeias de caracteres), e executar.

tarta = turtle.Turtle()
adn_tartaruga_alea(tarta,'ffefdtfftedf')
turtle.exitonclick()

Problema 5.1

Foi-me pedido que indicasse aqui qual a solução para o problema 5.1. Recordando. Existe um país no qual todas as estradas são de sentido único. Para além disso existe apenas uma e uma só ligação directa entre todos os pares de cidades. Pretende-se mostrar que existe (pelo menos) uma cidade que pode ser alcançada por estrada a partir de qualquer outra cidade, de modo directo ou de modo indirecto. Neste último caso existe apenas uma cidade de permeio.

Vamos então à solução que nos impõe sermos capazes de alguma abstracção. Vamos escolher uma cidade que tem um número máximo de ligações directas para si. Como é bom de ver ela tem que existir. Na realidade pode mesmo existir mais do que uma. Mas escolhamos uma. Seja C o nome da cidade e m o número de ligações. Vejamos se existe alguma cidade, chamemos-lhe X, que não esteja ligada directamente a essa cidade. Se não existir então a prova está terminada. E se existir? Se existir ela tem que estar ligada a uma das cidades que se ligam à nossa cidade escolhida. E porquê? Bem porque se não X teria para si m+1 ligações directas: todas as que se ligam a C mais a ligação de C para X. Não se esqueça que tem que existir uma ligação entre todas as cidades! E isso não pode ser, pois por hipótese m é o número máximo de ligações directas! Com X é uma cidade qualquer não ligada directamente a C isto completa a nossa prova, pois fica ligada indirectamente a C e só com uma cidade pelo meio!

Q.E.D.

terça-feira, 23 de novembro de 2010

Problema 5.15

As tartarugas são como pequenos robots, obedecendo a comandos. Neste problema os comandos são muito simples: ‘f’ para avançar em frente, ‘t’ para recuar, ‘e’ para virar à esquerda e ‘d’ para virar á direita. Queremos visualizar o que a tartaruga faz quando recebe uma dada sequência de comandos. Quando analisamos o problema fica claro que os dados de entrada são a tartaruga (afinal podemos querer enviar comandos diferentes a tartarugas diferentes...), e a sequência de comandos. A saída neste caso é o que observamos em termos de comportamento da tartatuga. Como representação dos dados vamos usar um objecto do tipo turtle e uma cadeia de caracteres para a sequência de comandos. Um olhar mais atento leva-nos a concluir que também falta definir a dimensão dos movimentos e o ângulo de viragem. Vamos admitir que esses elementos são constantes.

Como se trata de realizar repetidas vezes a obediência a um comando, que varia ao longo do tempo, o nosso programa vai ter como parte principal um ciclo. Como o número de vezes que executamos é fixo, uma vez conhecida a sequência de comandos, será usado um ciclo for. Vejamos então o código:


import turtle

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

Este código tem alguns pressupostos: os comandos são letras minúsculas, não há enganos nas letras, as constantes definidas directamente nos argumentos. Alterar o programa para estarmos mais precavidos não é difícil:

import turtle

def adn_tartaruga(tartaruga, adn):
""" Simula o comportamento da tartaruga ditado pelo seu ADN."""
adn.lower()
TAM = 60
ANG = 45
comandos = 'fted'
tartaruga.down()
for car in adn:
if car not in comandos:
print 'Comando desconhecido...bye'
break
elif car == 'f':
tartaruga.fd(TAM)
elif car == 't':
tartaruga.bk(TAM)
elif car == 'd':
tartaruga.rt(ANG)
else:
tartaruga.lt(ANG)


Agora é só experimentar.

segunda-feira, 1 de novembro de 2010

Problema 4.21

Este problema fala-nos de quadrados concêntricos cujas cores alternam entre o vermelho e o azul. É-nos dito também a posição e o valor do lado do quadrado mais pequeno e, ainda o modo como o quadrado aumenta.

É muita coisa para pensar ao mesmo tempo. Vamos então por partes. Vamos supor que é apenas um quadrado e que a cor é irrelevante. O leitor atento notará logo que assim formulado, trata-se na realidade do problema 4.20. E é! Vamos então resolver o dito.

def quadrado(tartaruga,x,y,lado):
"""Desenha um quadrado."""
# prepara
tartaruga.up()
tartaruga.goto(x,y)
tartaruga.hideturtle()
tartaruga.down()
# desenha
for i in range(4):
tartaruga.forward(lado)
tartaruga.right(90)

Esta solução não tem nada de especial: o problema foi dividido em duas partes. Na primeira, colocamos a tartaruga no sítio apropriado. Na segunda, desenhamos o quadrado, através de uma repetição de desenho do lado e rotação de 90 graus. Pensemos agora num outro detalhe: a cor.

def quadrado_cor(tartaruga,x,y,lado,cor):
""" Desenha um quadrado colorido."""
tartaruga.color(cor)
quadrado(tartaruga,x,y,lado)

Trivial, certo? A partir daqui, vamos querer desenhar quadrados coloridos concêntricos. Quantos serão será um parâmetro do problema. E terei um ciclo for. Algo assim:

def quad_concentricos(n,tartaruga,x,y,lado,cor,d):
"""Desenha n quadrados coloridos concêntricos."""
for i in range(n):
#desenha novo quadrado colorido

Mas como os faço concêntricos? A ideia é dada pela imagem seguinte:




Ou seja. Conhecidas as coordenadas do canto inferior esquerdo do último quadrado desenhado, a do próximo resulta de eu alterar essas coordenadas retirando um dado valor a x e aumentando desse mesmo valor y (notar o modo como estão orientados os eixos!). Se eu fizer isso, resulta claro que o novo lado vale a + 2 *d, se a dor o valor do lado inicial. Daqui resulta o código:

def quad_concentricos(n,tartaruga,x,y,lado,cor,d):
"""Desenha n quadrados concêntricos em cores alternadas."""
for i in range(n):
quadrado_cor(tartaruga,x-i*d, y+i*d, lado+i*2*d,cor)

Uso o valor de i para controlar o incremento. E como alterno as cores??? Se pensarmos bem, veremos que não é difícil. vou usar o valor de i: se for par, azul, se for ímpar, vermelho!!! E chego ao programa final.

def quad_concentricos(n,tartaruga,x,y,lado, cor_1, cor_2,d):
"""Desenha n quadrados concêntricos em cores alternadas."""
for i in range(n):
if i % 2 == 0:
quadrado_cor(tartaruga,x-i*d, y+i*d, lado+i*2*d, cor_1)
else:
quadrado_cor(tartaruga,x-i*d, y+i*d, lado+i*2*d, cor_2)




domingo, 31 de outubro de 2010

Problema 4.15

Este problema coloca basicamente a questão de saber como é que simulamos os lançamentos. A resposta, simples, consiste em usar o método randint do módulo random. O resto é o padrão básico de repetição um número de vezes fixo e conhecido, que se traduz pelo uso de um ciclo for.

def moeda_ao_ar(n):
"""Avalia o número de caras e coroas após n lançamentos de uma moeda. """
conta_caras = 0
conta_coroas = 0
for i in range(n):
valor = random.randint(0,1)
if valor == 0:
conta_caras = conta_caras + 1
else:
conta_coroas = conta_coroas + 1
return (conta_caras, conta_coroas)

Existe uma coisa que todos sabemos. Se a moeda não estiver enviesada, então podemos contar com 50% de probabilidades para num lançamento sair caras. Mas será mesmo assim? Para o saber façamos uma pequena experiência. Lançamos a moeda ao ar um certo número de vezes e contamos quantas vezes saiu caras. Mas repetimos esta experiência várias vezes e, de cada vez, anotamos o número de caras que saiu.

Com estes valores guardados podemos fazer um gráfico com a frequência dos valores obtidos. Se tudo bater certo com a teoria, esperamos obter uma distribuição em que o valor médio é o número que a teoria nos indica. Assim, se atiramos a moeda ao ar 10 vezes, repetindo a experiência 100 vezes, esperamos ter visto no final de toda a experiência 5 caras a maior parte das vezes. É claro que algumas vezes será menos e noutras mais. Vamos ver.

Caso de 100 experiências com 10 lançamentos em cada uma.



Agora 5000 experiências com 1000 lançamentos em cada uma.




Como se vê claramente, no primeiro caso temos um valor médio de 5 e, no segundo, de 500. Além disso, reparamos na forma da curva da distribuição, a forma de um sino. É isso mesmo, estamos perante uma distribuição normal, ou gaussiana!

Mas como obtivemos as imagens? Com o Python, claro! Para já mostro o código. Nas próximas aulas vão perceber o que ele significa e como podem começar a resolver questões semelhantes.

from operator import itemgetter
from pylab import *
import random


def moeda(vezes, numero):
# calcula
caras = {}
for i in range(vezes):
ca, co = moeda_ao_ar(numero)
caras[ca]= caras.get(ca,0) + 1
# visualiza
chaves, valores = split_dic(caras)
ver(chaves,valores)

def moeda_ao_ar(n):
"""Avalia o número de caras e coroas após n lançamentos de uma moeda. """
conta_caras = 0
conta_coroas = 0
for i in range(n):
valor = random.randint(0,1)
if valor == 0:
conta_caras = conta_caras + 1
else:
conta_coroas = conta_coroas + 1
return (conta_caras, conta_coroas)

def split_dic(dicio):
"""
Separa um dicionário numa lista com duas listas: chaves e valores.
A separação é feita de modo a ter as listas ordenadas pelas chaves.
"""
lista = dicio.items()
lista.sort(key=itemgetter(0))
chaves = [ chave for chave,valor in lista]
valores = [ valor for chave,valor in lista]
return chaves,valores


def ver(x,y):
"""Faz um plot simples."""
title('Distrib. Caras')
xlabel('Valor')
ylabel('Freq.')
plot(x,y)
show()

Problema 4.12

Este problema tão simples dá-nos a oportunidade de falar uma vez mais sobre como chegar ao programa a partir do enunciado do problema. Em primeiro lugar, temos que identificar os dados e o resultado. Como o que se pretende saber é se um dado número é perfeito, fica claro que o dado é o número e o resultado é um indicador (True ou False) que nos sinaliza se o número é perfeito ou não. Em segundo lugar, temos que perceber o modo de comunicação entre o programa e o seu ambiente. O enunciado por ou não impor um tipo específico de comunicação. Por exemplo, pode dizer “Escreva um programa em que é pedido ao utilizador...”, ou “... e imprima o resultado”. Nestes casos ficava claro que os dados eram introduzidos pelo utilizador (input, raw_input) e os resultados seriam impressos (print). Nado nos é dito, pelo que vamos supor que os dados são comunicados via parâmetros formais e os resultados são devolvidos através de return. Em terceiro lugar, devemos preocupar-mo-nos com a representação dos objectos. Aqui é trivial: são números e booleanos. Em quarto lugar, vamos responder à questão de como ligar os dados ao resultado, isto é, qual o processo (ou algoritmo) que nos vai permitir resolver o problema. Uma abordagem metodológica simples consiste na divisão do problema principal em sub problemas:

1. Calcular os divisores
2. Calcular a soma dos divisores
3. Testar se o número é igual à soma dos seus divisores (excluindo o próprio número!)

Com o que sabemos ainda não estamos em condições de resolver as questões 1. e 2. em separado. Para tal teríamos que guardar os divisores nalgum lado! Por isso, vamos juntar os dois passos em apenas um, calculando os divisores e mantendo associado a uma variável a soma dos divisores já encontrados.

def perfeito(n):
"""Determina se um número inteiro positivo é perfeito, isto é,
igual à soma dos seus divisores próprios."""
# 1. 2.: divisores e soma
soma = 0
for i in range(n):
if (n % i) == 0:
soma = soma + i
# 3. Testa e devolve resultado
return soma == n

Problema 4.11

O desafio é calcular o valor aproximado da série harmónica, valor esse controlado pelo erro máximo admissível. A solução é semelhante à do problema 4.10, mas agora vamos ter que usar como critério o erro. É-nos dito que esse erro é calculado pela diferença entre dois valores consecutivos da soma. De tudo isso decorre uma solução simples.

def harmonica_erro(erro):
""" Calcula valor da série harmónica com erro inferior ao dado."""
soma = 0
denominador = 1.0
while True:
nova_soma = soma + 1.0/denominador
if nova_soma - soma < erro:
return nova_soma
soma = nova_soma
denominador = denominador + 1

A solução apresentada tem por base um ciclo que vai sendo executado até se atingir o objectivo do erro máximo. Na realidade criámos um aparente ciclo infinito (a condição de saída nunca é falsa). Aparente porque o comado return permite sair do ciclo. Mais ainda: permite sair do programa! Notar também que sendo o numerador fixo (vale sempre 1) optou-se por gerar apenas o denominador. Por se tratar de uma divisão, tivemos que ter o cuidado de usar um float (1.0). Finalmente deve-se analisar o modo como temos sempre dois calores consecutivos associados às variáveis soma e nova_soma.

sábado, 30 de outubro de 2010

Problema 4.9

Uma resposta simples a esta pergunta consiste em ir gerando os números entre ind e sup, testando se são ímpares e imprimir os que forem. Ou seja, o que temos a fazer é repetir as duas operações seguintes:

- gerar próximo candidato
- se for ímpar, imprime

Duas versões possíveis de solução:

def impar_3(inf, sup):
"""Imprime os números impares entre inf e sup."""
while inf <= sup:
if (inf % 2) != 0:
print inf
inf = inf + 1

def impar_4(inf, sup):
"""Imprime os números impares entre inf e sup."""
for i in range(inf,sup+1):
if (i % 2) != 0:
print i


Como se nota, num casio usamos um ciclo while e no outro um ciclo for. Mas podemos ter ainda uma outra alternativa. Neste caso usamos o range para gerar os ímpares. Só temos que garantir que o começo é o primeiro ímpar maior ou igual a inf.

def impar_2(inf,sup):
"""Imprime os números impares entre inf e sup."""
inf = (inf - 1) % 2 + inf
for i in range(inf,sup + 1, 2):
print i


Reparar que no caso em que usamos o ciclo for o range vai até sup+1. Porquê?

domingo, 17 de outubro de 2010

Problema 4.7

Relembremos o enunciado:


Um ano é bissexto, isto é tem 366 dias, se for divisível por 4. Existem, no entanto excepções: se for divisível por 100 então não é bissexto. Mas também esta excepção tem uma excepção: se forem também divisíveis por 400 são bissextos. Escreva um programa que determine se um dado ano é bissexto ou não.

Mais um enunciado a requerer análise cuidada. Mas vejamos alguns exemplos.

2400: é divisível por 4 ? Sim! Então é ... Calma! Também é divisível por 100! Logo... Calma mais uma vez, não se precipite, pois também é divisível por 400. Conclusão: é bissexto!!!

A maneira mais fácil de fazer é colocar o caso inequívoco em primeiro lugar no nosso teste. E esse é ser divisível por 400. Vem depois a primeira excepção: divisível por 100. Só de seguida o caso usual da divisibilidade por 4. Se tudo isto falhar, garantidamente não é bissexto. Em programa:


def ano_bissexto(ano):
""" Determina se um ano é bissexto."""
if (ano % 400) == 0:
return True
elif (ano % 100) == 0:
return False
elif (ano % 4) == 0:
return True
else:
return False


Quando será o próximo ano bissexto??

Problema 4.5

Saber se um ponto se encontra ou não dentro de um círculo pode ser resolvido de modo rápido. Como acontece com frequência, fazer um desenho pode ajudar. Senão reparem.



Olhando para a figura fica claro que estar ou não dentro do círculo depende do comprimento do segmento que liga a origem dos eixos ao ponto e a sua relação com o tamanho do raio. Posto isto, temos a solução óbvia, amitindo que o centro está na posição (0,0)..

import math

def interior_circulo(x,y,raio):
""" Determina se o ponto está no interior do círculo.
Supõe centro no ponto (0,0)."""
distancia = math.sqrt(x ** 2 + y ** 2)
if distancia <= raio:
return True
return False

Quem estiver esquecido da trignometria ... tem que ir rever a matéria!

Problemas 4.3, 4.4 e 4.6

Problema 4.3

Os problemas da ficha 4 sobre condicionais são, em geral, muito acessíveis. Este não foge à regra.

def nota_iprp(nexame, teste,projecto):
"""Determina a nota de iprp. Exame vale 60%, teste, vale 20% e projecto 20%.
Asume que os valores introduzidos estão correctos."""
nota = 0.6 * exame + 0.2 * (teste + projecto)
if nota <9.5 :
return 0
else:
return 1


Problema 4.4

Este problema não tem solução única. Uma ideia a ter em conta pode ser o de minimizar o número de testes a efectuar.

def maior_3(n1,n2,n3):
""" Determina o maior dos três números."""
if n1 > n2:
if n1 > n3:
return n1
else:
return n3
else:
if n2 > n3:
return n2
else:
return n3

Claro que estas abordagens não são escaláveis. Imaginem se eram 100 números!! Veremos com a introdução dos ciclos que existe uma resposta a esta questão que não depende do número de números. Mas, se conhece bem Python, já sabe que também podia deixar o problema nas mãos das funções pré-definidas:

>>> max(1,6,3,9,34,98,4)
98
>>>

Problema 4.6

E agora a questão da fruta. Nada como ter um bom desconto...

def custo_fruta(peso, preco, peso_minimo):
""" Qual o custo da fruta?."""
a_mais = peso - peso_minimo
if a_mais > 0:
custo = peso_minimo * preco + a_mais * 0.75 * preco
else:
custo = peso * preco
return custo

Das aulas resultou que a maior dificuldade sentida foi a de entender o enunciado. Têm que se habituar a interpretar correctamente o texto que descreve o problema a resolver.

sábado, 9 de outubro de 2010

Problema 3.9

Problema básico, que se resolve por aplicaçção directa das fórmulas dadas.


def polar_para_cartesiana(r, theta):
""" Converte de coordenadas polares para cartesianas."""
x = r * math.cos(theta)
y = r * math.sin(theta)
return x,y

Problema 3.8

Este é um problema trivial do ponto de vista da programação, mas que obriga a ter conhecimentos sobre o domínio (neste caso trigonometria). Não existe um modo único de encontrar as fórmulas que definem o módulo (r), e o ângulo (teta) em função de x e de y. Aqui, optámos por usar o teorema de Pitágoras para chegar ao módulo e o arco tangente para obter o ângulo. Quem tiver dúvidas vai ter que rever os seus conhecimentos sobre trigonometria...


def cartesiana_para_polar(x,y):
""" converte de coordenadas cartesianas para polares."""
r = sqrt(x**2 + y**2)
theta = math.atan(float(y)/x)
return r, theta


Nota: Usar a função atan() pode trazer problemas pois o quadrante do ângulo não é determinada. Por exemplo, se as coordenadas forem (1,1) e (-1,-1) , vamos ter sempre o mesmo resultado, 45º, e não esse, na primeira situação, e 135º na segunda. Também há problemas caso o ponto seja (1,0). A solução está em usar atan2():



def cartesiana_para_polar(x,y):
""" converte de coordenadas cartesianas para polares."""
r = sqrt(x**2 + y**2)
theta = math.atan2(y,x)
return r, theta


Notar a vírgula a separar os argumentos, em vez da divisão.

Problema 3.4

Semelhante ao problema 3.3, com a diferença de nos fazer perceber ainda que existem várias maneiras de chegar ao mesmo resultado, e que algumas vezes quem não tem cão tem que caçar com rato.


import math

print math.sqrt((x ** 2 + x ** 2) * 2) / 2

print ((x ** 2 + x ** 2) * 2) ** (1.0 / 2) / 2

Problema 3.3

Problema muito simples que apenas pretendia duas coisas: (1) treinar a passagem de um enunciado para um pedaço de código e, (2) chamar a atenção para o facto de os operadores aritméticos estarem sobrecarregados dependendo o resultado final do tipo dos objectos usados. Use as duas versões para diferentes valores e repare nos resultados. São sempre iguais?


x = 4

print (((x - 6) / 3) + 2) * 3

print (((x - 6) / 3.0) + 2) * 3

terça-feira, 5 de outubro de 2010

Ovo Estrelado

No outro dia voltei ao passado quando almocei um dos meus pratos preferidos de miúdo: bife com batatas fritas e ovo estrelado. Soube-me bem e, em particular, o ovo estrelado estava óptimo. Quis saber como é tinha sido feito e o cozinheiro, entretanto chamado, prontificou-me a revelar o seu segredo. Para que seja eternizado, disse-me, vou escrever a receita numa folha de papel. No final deu-me a folha que dizia o seguinte:

RECEITA
Ovo Estrelado

Ingredientes: 1 ovo, óleo e sal q.b.
Material: fogão, frigideira, colher, prato

1- Coloque o óleo na frigideira e leve ao lume
2- Quando o óleo estiver quente, parta o ovo e deite-o na frigideira
3- Use a colher para regularmente deitar o óleo quente por cima do ovo
4- Quando tiver a consistência desejada, use a colher para o retirar da frigideira e coloque-o no prato.

Uns dias mais tarde, apeteceu-me comer um ovo estrelado e lembrei-me de usar a receita do restaurante. Fui buscar os ingredientes, preparei o material e retirei do bolso a receita que executei. No final, claro, comi o ovo!

Mas o que é que esta história tem a ver com a programação? Muito! É que programar é uma actividade que significa fundamentalmente escrever receitas (para resolver problemas), que alguém mais tarde vai usar. Vejamos como podemos explorar a analogia.

Em Python eu indico que vou escrever uma receita escrevendo a palavra reservada def. O nome da receita é nome da função. Os ingredientes são os parâmetros formais, isto é uma lista ordenada de nomes, separados por vírgulas e envolvidos por parêntesis. O material, são as eventuais constantes e nomes auxiliares criados pelo programa. Usar uma função significa (1) ligar os parâmetros reais aos parâmetros formais, processo esse que é feito quando chamamos (usamos) a função e, (2) executar as instruções da função. Do mesmo modo que na receita falamos em abstracto de um ovo, e quando realizamos a receita temos que usar um ovo concreto, também na programação os parâmetros formais são nomes genéricos, enquanto os parâmetros reais têm que remeter directa ou indirectamente para objectos. A ligação entre os parâmetros formais e os reais não é mais do que uma associação, ou seja uma instrução de atribuição implícita, entre o nome do parâmetro formal e o objecto para que remete o parâmetro real. Esta associação dura enquanto a função estiver a ser executada.

Um exemplo simples.

# Definição

def soma(x,y):
"""Calcula o valor da soma de x com y."""
return x + y

# Uso

a = 3
print soma(a,7)

Neste caso, o nome do programa é soma e os parâmetros formais são x e y. Chamamos a função usando o seu nome e os respectivos parâmetros reais. Neste caso, os parâmetros reais são o nome a (que está associado ao objecto 3) e o objecto 7. É como se tivesse-mos feito:

# Definição
def soma():
"""Calcula o valor da soma de x com y."""
x = a
y = 7
return x + y
# Uso
a = 3
print soma()

Porque devemos preferir sempre a primeira versão?? Pense um pouco e verá que a razão é simples!

Ao executar o código da função o resultado devolvido é, neste caso concreto, 10.

sábado, 2 de outubro de 2010

Problema 2.11

Quando chegarem a este problema, se os anteriores foram percebidos existe apenas a dificuldade de importar o módulo math e usar o método sqrt do módulo. Vejamos uma solução.

import math

def distancia(x1,y1,z1,x2,y2,z2):
"""
Distância Euclidiana entre dois pontos de coordenadas (x1,y1,z1) e (x2,y2,z2).
"""
dist= math.sqrt((x2 - x1)** 2 + (y2 - y1 ) ** 2 + (z2 - z1 ) ** 2)
return dist

Tendo em atenção o que foi dito em post anterior sobre legibilidade, modularidade e reutilização de código, acha que pode fazer melhor? Não custa nada tentar!

Problema 2.8

Este problema obedece à mesma matriz dos da ficha 2: aplicar a expressão que relaciona os dados ao resultado. Uma vez mais, começamos por identificar os dados: o raio e a altura de um cilindro e ainda a constante Pi. É-nos dito para usar como aproximação de Pi o valor 3.14159. Traduzindo o enunciado podemos apresentar a solução seguinte.

def area_cilindro(raio, altura):
"""
Calculo da área de um cilindro, conhecidos o raio da base
e a altura.
"""
return 2 * 3.14159 * raio * altura + 2 * (2 * 3.14159 * raio ** 2)

Um dos problemas que se colocam a quem programa é que os seus programas destinam-se a ser usados, lidos, modificados por outros. Muitas vezes até pelos próprios. Tudo isso leva a que os programas sejam modulares e claros. Estas propriedades estão ausentes mesmo num programa tão pequeno e simples como o anterior. Vamos tentar melhorá-lo.

def area_cilindro(raio, altura):
"""
Calculo da área de um cilindro, conhecidos o raio da base
e a altura.
"""
lateral = 2 * 3.14159 * raio * altura
base = 2 * 3.14159 * raio ** 2
return lateral + 2 * base

Nesta solução dividimos o nosso problema em dois sub-problemas. Obtemos o resultado de cada um e associamos esses objectos a nomes significativos. Depois é só combinar esses resultados para obter o resultado final. Concordarão que está mais legível!

Mas podemos ainda pensar no aspecto modularidade e reutilização de código. Para tal criamos duas definições auxiliares.


def area_lateral(raio, altura):
""" Calculo da área lateral de um cilindro."""
return 2 * 3.14159 * raio * altura

def area_circulo(raio):
""" Calculo da área da base de um cilindro."""
return 3.14159 * raio ** 2

def area_cilindro(raio, altura):
""" Calculo da área de um cilindro."""
area_lat = area_lateral(raio,altura)
area_base = area_circulo(raio)
return area_lat + 2 * area_base

A legibilidade mantém-se e passamos a ter duas definições autónomas que podemos usar para outros cálculos, como por exemplo a área de um círculo.

Problema 2.7

Resolver um problema quando se tem uma fórmula que liga directamente os dados ao resultado é simples. Mesmo assim podemos ter várias versões. Vejamos com o exemplo do cálculo do declive de uma recta conhecidas as coordenadas de dois dos seus pontos.


def declive(x1,y1,x2,y2):
"""
Calcula o declive de uma recta conhecidos dois dos seus pontos.
"""
return (y2 - y1) / (x2 - x1)

Mais simples não há! Com uma pequena mudança, introduzimos um nome, que associamos ao resultado.

def declive(x1,y1,x2,y2):
"""
Calcula o declive de uma recta conhecidos dois dos seus pontos.
"""
inclina = (y2 - y1) / (x2 - x1)
return inclina

Quando começamos a efectuar alguns testes, verificamos que há resultados incorrectos. Por exemplo, se tentarmos com os pontos (3,4) e (6,6) obtemos 0 e não o valor correcto 0.66. O problema está na divisão. Se os argumentos são inteiros o resultado será o quociente inteiro! Duas soluções para o problema. Uma é introduzir os dados como floats. Mas esta não é a melhor opção, pois coloca do lado do utilizador a responsabilidade de saber como foi definida a solução. Assim a melhor escolha, neste caso, é forçar uma das coordenadas a ser um float, no interior da definição.

def declive(x1,y1,x2,y2):
"""
Calcula o declive de uma recta conhecidos dois dos seus pontos.
"""
inclina = (float(y2) - y1) / (x2 - x1)
return inclina

Mas e se as abcissas forem iguais?


Evaluating ficha_2_2010_sol.py
Traceback (most recent call last):
File "/Applications/WingIDE.app/Contents/MacOS/src/debug/tserver/_sandbox.py", line 150, in
File "/Applications/WingIDE.app/Contents/MacOS/src/debug/tserver/_sandbox.py", line 83, in declive_2
ZeroDivisionError: float division


Oops! Como corrigir? Uma vez mais podemos acautelar isso aquando do uso do programa ou na própria definição. Neste último caso existem várias alternativas de que falaremos ao longo do curso. Passam todas por filtrar o caso que leva ao erro. Deixamos aqui a solução mais simples.

def declive_3(x1,y1,x2,y2):
"""
Calcula o declive de uma recta conhecidos dois dos seus pontos.
"""
if x1 == x2:
print 'Erro: as abcissas não podem ser iguais'
return False
else:
inclina = (float(y2) - y1) / (x2 - x1)
return inclina


A solução consiste em fazer um teste à condição de erro que, a verificar-se, leva à impressão de uma mensagem de erro e ao terminar do programa que devolve o objecto False.

Problema 2.1

Recordemos o enunciado.


É de madrugada, mais precisamente 6h00 da manhã. Já acordado, ouve o seu relógio de parede bater 6 badaladas. De acordo com o seu relógio de pulso passaram 30 segundos entre a primeira badalada e a última. Pensa: quando tendo demorará a bater as 12 badaladas quando for meio-dia? Sem muito reflectir, conclui que vão ser 60 segundos. Porquê? Bem se demorou 30 segundos a bater as 6 badaladas é porque cada uma demora 5 segundos. Certo? Logo 12 * 5 = 60. Piece of cake! Mas será mesmo assim? Pense um pouco e dê-me a sua opinião.


Esta abordagem ignora as leis do mundo físico: (1) as badaladas têm uma duração, seguramente, mas (2) existe um intervalo de tempo que decorre entre duas badaladas.

Recorrendo à velha ideia de por um problema em equação temos então:

6 * X + 5 * Y = 30 (eq. 1)


com X a duração das badaladas, e Y a duração dos intervalos. O nosso problema é então:

12 * X + 11 * Y = ??? (eq. 2)


O que é que podemos desde já dizer? Multiplicando ambos os termos da equação 1 por 2, fica:

12 * X + 10 * Y = 60 (eq. 3)


ou ainda,

12 * X + 11 * Y = 60 + Y (eq. 4)


logo podemos concluir que vai demorar mais do que 60 segundos. Mas quanto? De (eq. 4) resulta que depende da duração dos intervalos, Y. Mas mais não podemos dizer. Na realidade, existem uma infinidade de soluções!!! Por exemplo, se admitirmos que a badalada é instantânea, isto é, X = 0, então Y = 6 e as 12 badaladas demorarão 66 segundos. Outras combinações são possíveis!

Problema 1.6

Os problemas da Ficha 1 são todos muito acessíveis. Não deixam por vezes de nos ensinar algumas coisas. Uma é que devemos testar sempre o programa na tentativa de eliminar a possibilidade de erros. Os testes devem ser feitos usando exemplos que explorem casos típicos e limites. Vamos exemplificar com o problema 1.6. Recordemos o enunciado.

Imagine uma lista telefónica em que cada página tem L linhas e cada linha C colunas. Em cada posição (Linha,Coluna) é guardada a informação pertinente. Admita que as páginas, linhas e colunas se iniciam sempre a 1. Conhecido o índice da sua informação, em que página, linha e coluna se encontra?

Trata-se de um problema semelhante ao de saber quantas horas, minutos e segundos estão contidos num certo número de segundos. Assim, a nossa solução passa por saber a quantas páginas corresponde o nosso índice. Este valor obtém-se usando uma divisão inteira do nosso índice pelo número de células numa página. No entanto, com as páginas começam a 1, temos que adicionar precisamente 1 ao valor com a divisão. Sabida a página, vamos tentar saber a linha. Para isso temos que determinar primeiro qual o índice dentro da página, valor que se obtém do resto da divisão anterior. É este elemento que agora vai ser desdobrado pelas linhas. Como cada linha tem C colunas é por C que temos que dividir o valor do resto. Também aqui, porque a linha começa a 1, temos que adicionar um ao resultado. Finalmente, falta conhecer a posição (coluna) dentro da linha. Repetimos a estratégia: primeiro usamos o valor do resto da última divisão. Calculando o resto deste valor, quando o dividimos pelo número de células numa linha, obtemos o resultado final pretendido.

Como se vê este problema resolve-se basicamente com divisões inteiras e cálculo do resto. Entendido isto, listemos o código.


# Entrada dos dados
pos = input("posição: ")
L = input("Linhas: ")
C = input("Colunas: ")
# página
pagina = (pos / (L * C)) + 1
print "pagina: ", pagina
# Linha
resto_pagina = pos % (L * C)
linha = resto_pagina / C + 1
print "linha: ", linha
# Coluna
resto_linha = resto_pagina % C
coluna = resto_linha % C
print "coluna: ", coluna

Tudo isto é muito bonito. Mas testarmos o programa em situações limite temos problema. Eis um exemplo:


posição: 15
Linhas: 5
Colunas: 3
pagina: 2
linha: 1
coluna: 0
>>>

Outro exemplo ainda:


posição: 6
Linhas: 5
Colunas: 3
pagina: 1
linha: 3
coluna: 0
>>>


Conclusão: se a célula estiver ou na última linha de uma página ou na última coluna de uma linha, temos erro! É fácil de ver que isso ocorre quando alguma das divisões dá resto zero. Em breve aprenderemos a usar estruturas de controlo condicionais que nos permitirão resolver estes casos marginais.

sábado, 18 de setembro de 2010

Começar tudo outra vez...

Vamos começar, segunda, dia 20, nova aventura.

Dou as boas vindas aos novos e cumprimento os antigos.

Esta cadeira tem, infelizmente, acumulado uma elevada taxa de insucesso. Cada vez que penso nisso encontro sempre duas razões: falta de trabalho continuado e falta de prática, muita prática!

Esperemos melhorar este ano.

Neste espaço irei continuar a colocar informação, comentários, exemplos, ligados à matéria de IPRP. Como calculam, o blogue não substitui nem as aulas, nem os outros elementos de estudo (bibliografia) e de ajuda (DEIAcademy, Mooshak).

Boa sorte!

sexta-feira, 5 de fevereiro de 2010

Reflexões (sobre o exame de recurso)

Deixo aqui algumas reflexões suscitados pelo exame de recurso.

Em primeiro lugar, continuo a achar estranho que se entregue um ponto para corrigir quando somando a cotação das perguntas respondidas é impossível ter nota para passar. O único resultado disso é mais tempo para corrigir e, consequentemente, atrasar a divulgação dos resultados. Também me choca ver casos de pessoas que respondem a tudo mas têm zero no final. Infelizmente não são poucos.

Em segundo lugar, a quantidade de erros de português, erros lexicais, de sintaxe e de semântica é muito preocupante. O pouco cuidado na apresentação também é regra.

Em terceiro lugar, confunde-me que alunos do ensino superior não consigam interpretar/entender o enunciado. Por exemplo, se eu coloco ??? numa listagem, isso não é a saída do interpretador! Que não entendam quando se pede uma resposta geral ou uma resposta específica. Se eu coloco um exemplo com os vectores [1,2,3] e [4,8,12] (pergunta 3) não é porque queira um programa para calcular a diferença absoluta daqueles vectores apenas. O mesmo se aplica quando apresenta um tabuleiro 4 X 4. Todos estes aspectos estavam bem clarificados no enunciado de cada pergunta!!

Finalmente, registo a vossa dificuldade em dominar a complexidade de resolver um problema. Vamos por isso às perguntas concretas, primeiro as teóricas, depois as de programação.

Pergunta 1

Um conceito nuclear em programação é o de objecto e das suas características. Chegar ao final do ano e ainda não saber que os objectos têm identidade, valor e tipo e que é com base nestas características que se distinguem os que são mutáveis dos que não são.

Pergunta 2

Quem não tiver a noção do que é um tipo de dados (objecto + operações) não consegue responder. O erro mais comum foi dizer que são equivalentes pois posso transformar um ficheiro numa cadeia de caracteres. Mas assim deixamos de estar a operar com um ficheiro...

Pergunta 3

Esta pergunta (que valia 25% da nota!) marca a fronteira de quem merece e de quem não merece passar! Posso admitir (embora estranhe) que têm umas convicções matemáticas bizarras relativamente a operações com módulos. Tratava-se de efectuar a soma do módulo das diferenças componente a componente. Não se tratava, por exemplo, de somar todos os valores de um vector, somar todos os valores de outro vector, fazer a diferença e depois tomar o valor absoluto do resultado. E que posso dizer de quem calcula o máximo apenas sobre um valor? Ao fim de um ano, não posso aceitar que não consigam fazer um programa que envolve um acumulador e um ciclo.

Pergunta 4

No exame da época normal tinha parecido um problema parecido: passar informação de um ficheiro para um dicionário. Deixei aqui no blogue vários posts sobre o assunto. Mas mesmo assim...

Todos os problemas envolvem conhecimento sobre a linguagem de programção e sobre o processo de construção da solução. Este não foge à regra. A melhor maneira de dominar a complexidade de um problema é abordar a solução de modo progressivo, procurando só tomar decisões concretas quando não há alternativa. Vejamos neste caso. Num olhar global, a estratégia é fácil de enunciar: (a) ler o ficheiro linha a linha; (b) extrair a informação relevante; (c)colocar no dicionário. Daí o esboço do programa:

def dic_max_temp_b(ficheiro):
""" Do ficheiro ao dicionário."""
# inicialização
# ---- leitura
# lê linha
while # há informação:
# extrai informação
# coloca no dicionário
# lê linha
# finaliza
return # resultado

A inicialização consiste em abrir o ficheiro e criar o dicionário vazio. No final, fechar o ficheiro.

def dic_max_temp_b(ficheiro):
""" Do ficheiro ao dicionário."""
# inicialização
f_in = open(ficheiro, 'r')
dic = {}
# ---- leitura
# lê linha
while # há informação:
# extrai informação
# coloca no dicionário
# lê linha
# finaliza
f_in.close()
return dic


A leitura tem que ser feita linha a linha, pois é assim que a informação relevante está organizada e é assim que terá que ser processada:

def dic_max_temp_b(ficheiro):
""" Do ficheiro ao dicionário."""
# inicialização
f_in = open(ficheiro, 'r')
dic = {}
# ---- leitura
# lê linha
linha = f_in.readline()

while # há informação:
# extrai informação
# coloca no dicionário
# lê linha
linha = f_in.readline()
# finaliza
f_in.close()
return dic

Já só falta extrair a informação e colocar no dicionário. Quantos elementos existem por linha? Três: o nome do produto, o número da experiência e o valor da temperatura. Tendo o cuidado de saber que é preciso retirar a marca de fim de linha podemos obter e separar a informação contida numa linha com uma única instrução:

def dic_max_temp_b(ficheiro):
""" Do ficheiro ao dicionário."""
# inicialização
f_in = open(ficheiro, 'r')
dic = {}
# ---- leitura
# lê linha
linha = f_in.readline()
while # há informação:
# extrai informação
cod,num,temp = linha[:-1].split()
# coloca no dicionário
# lê linha
linha = f_in.readline()
# finaliza
f_in.close()
return dic

Esta forma de fazer é genérica. Quem não partiu a linha nos seus constituintes e manteve a cadeia de caracteres tinha o problema de saber em que posições começava e acabava cada elemento. E isso deu azo a muita confusão. Imaginem que quem colocou a informação no ficheiro coloca diferentes espaços entre elementos? Ou que o número de experiências é maior do que dez? Ou que a temperatura pode vir antes do número da experiência? A solução de manter uma cadeia de caracteres ainda funciona? Se não (o que é verdade!) que alterações é preciso fazer no código?

Agora já só falta colocar a informação no dicionário. Temos duas situações: ou já lá está algo, ou não. O primeiro caso ainda se subdivide em dois: ou o que já existe é maior do que o novo valor ou não: Primeira divisão:

if dic.has_key(cod):
...
ou (porque o método has_key já não existe em Python 3.0)

if cod in dic.keys():
...

ou ainda

if dic.get(cod, False):
...

E vamos à segunda divisão:

if cod in dic.keys():
if dic[cod] < int(temp):
# substitui
else:
# introduz

que dá origem a:

if cod in dic.keys():
if dic[cod] < int(temp):
dic[cod] = int(temp)
else:
dic[cod] = int(temp)

E notamos duas coisas. Uma, importante, é que temos que fazer a conversão para inteiros. Experimente testar se ‘4’ > ’10’ e perceberá a razão! Outra, é o facto de andarmos com código repetido. Nesta última situação, ajuda saber um pouco de dicionários em Python. Tudo se pode fazer numa linha de código.

def dic_max_temp_b(ficheiro):
""" Do ficheiro ao dicionário."""
# inicialização
f_in = open(ficheiro, 'r')
dic = {}
# ---- leitura
# lê linha
linha = f_in.readline()
while # há informação:
# extrai informação
cod,num,temp = linha[:-1].split()
# coloca no dicionário
dic[cod] = max(dic.get(cod,-100),int(temp))
# lê linha
linha = f_in.readline()
# finaliza
f_in.close()
return dic

Pergunta 5

Esta pergunta tinha algumas dificuldades: (a) saber posicionar a tartaruga para cada quadrado; (b) em cada momento saber a cor do quadrado. Mas estas dificuldades eram fáceis de ultrapassar se se lembrassem que tínhamos feito um problema exactamente igual na ficha sobre imagens! Muito me surpreendeu ainda tantos alunos não perceberem que deviam usar o programa para desenhar um quadrado de uma dada cor de forma autónoma. Isto significa que não conseguem pensar no problema dividindo-o em partes mais simples, como ainda fizemos para o caso do problema 4. Vamos ver então como se resolve a questão. Em primeiro lugar tomamos a decisão de imprimir linha a linha:


def tabuleiro(tartaruga,n,lado):
# inicialização
for l in range(n):
# imprime linha l

A inicialização consiste apenas em colocar a tartaruga numa dada posição inicial. Essa posição tanto podia ser comunicada como parâmetro, como podia ser definida em termos absolutos no interior do programa. Segunda questão: como desenhamos uma linha? Decisão: quadrado a quadrado:

def tabuleiro(tartaruga,n,lado):
# inicialização
for l in range(n):
# desenha linha l
# inicialização da linha
for c in range(n):
# desenha coluna c

Neste momento, não podemos adiar mais a decisão de saber a cor. Podemos ter uma solução que só depende da alternância de cor dentro de uma linha. Mas, se formos por aí, não nos podemos esquecer que as linhas também alternam a sua cor de início! Outra solução, melhor, é tomar a decisão em termos da linha e da coluna. Basta perceber que sempre que a soma dos índices da linha e da coluna for par o quadrado é branco:

def tabuleiro(tartaruga,n,lado):
# inicialização
for l in range(n):
# desenha linha l
# inicialização da linha
for c in range(n):
# desenha coluna c
if (l + c) % 2 == 0:
# quadrado branco
else:
# quadrado preto

E como se desenha o quadrado? Fazendo uso do programa fornecido:

def tabuleiro(tartaruga,n,lado):
# inicialização
for l in range(n):
# desenha linha l
# inicialização da linha l
for c in range(n):
# desenha coluna c
if (l + c) % 2 == 0:
quad(tartaruga, lado,‘white’)
else:
quad(tartaruga,quad,‘black’)

Como estamos a ver, das duas dificuldades à pouco enunciadas, só resolvemos a da cor. Falta a da posição! No início será (0,0). Esta decisão tem consequências? Claro: sempre que mudarmos de linha temos que colocar a tartaruga na posição (0, linha). Como podemos fazer isso? De modo relativo ou de modo absoluto. De modo relativo: tendo em atenção quanto avançámos mandamos a tartaruga recuar igual valor. De modo absoluto: usando o comando setx(0). Mas e o valor da coordenada da linha, necessário sempre que mudamos de linha? Também não é difícil de ver que deve ser o valor que tinha mais o valor do lado:

def tabuleiro(tartaruga,n,lado):
# inicialização
tartaruga.pu()
tartaruga.goto(0,0)
tartaruga.pd()
for l in range(n):
# desenha linha l
# inicialização da linha l
tartaruga.setx(0)
tartaruga.sety(tartaruga.ycor() + lado)
# desenha linha
for c in range(n):
if (c + l) % 2 == 0:
quad(tartaruga,lado,'white')
else:
quad(tartaruga,lado,'black')

Claro que também se podia usar tartaruga.sety(l * lado). Mas assim ainda não avançamos coluna a coluna?! Pois não. Por cada quadrado desenhado temos que avançar o valor do lado:

def tabuleiro(tartaruga,n,lado):
# inicialização
tartaruga.pu()
tartaruga.goto(0,0)
tartaruga.pd()
for l in range(n):
# desenha linha l
# inicialização da linha l
tartaruga.setx(0)
tartaruga.sety(tartaruga.ycor() + lado)
# desenha linha
for c in range(n):
if (c + l) % 2 == 0:
quad(tartaruga,lado,'white')
else:
quad(tartaruga,lado,'black')
tartaruga.setx(tartaruga.xcor() + lado)

Esta solução é simples e elegante. Podemos facilmente alterar o programa para que o tabuleiro não seja do tipo n X n,outras cores, desenho por colunas em vez de ser por linhas. Mudar de forma é que é mais complexo. Mas nas nossas experimentações íamos descobrir é que não protegemos devidamente o reposicionamento da tartaruga: em certas situações ficam um rasto visível. Mas até isso se resolve sem grande dificuldade.

def tabuleiro_b(tartaruga,pos,n1,n2,lado, cor_1, cor_2):
tartaruga.pu()
tartaruga.goto(pos)
tartaruga.pd()
for c in range(n1):
# reposiciona
tartaruga.pu()
tartaruga.sety(pos[1])
tartaruga.setx(tartaruga.xcor() + lado)
tartaruga.down()
# desenha coluna
for l in range(n2):
if (c + l) % 2 == 0:
quad(tartaruga,lado,cor_1)
else:
quad(tartaruga,lado,cor_2)
tartaruga.pu()
tartaruga.sety(tartaruga.ycor() + lado)
tartaruga.down()

Divirta-se!

Exame de Recurso

P1 a)
O que distingue os objectos mutáveis dos objectos imutáveis?

Resposta

No caso dos objectos mutáveis podemos alterar o valor sem alterar a identidade, nos imutáveis não.

P1 b)

Os ficheiros e as cadeias de caracteres são tipos de dados equivalentes? Isto é: tudo o que posso fazer com um objecto de um dos tipos, posso fazer com um objecto do outro. Justifique a sua opinião.

Resposta
Um tipo de dados remete para objectos e operações comuns com esses objectos. Por exemplo, os inteiros e os floats são tipos de dados equivalentes. O mesmo não se pode dizer dos ficheiros de texto e das cadeias de caracteres. Um ficheiro é uma longa cadeia de caracteres, estruturada em linhas, mutável e armazenada de forma permanente. As cadeias de caracteres não têm estrutura interna, são imutáveis e apenas existem durante a execução do programa onde estão definidas. Quanto às operações elas são diversas. No caso dos ficheiros temos operações de consulta por elemento (read), por linha (readline) ou por linhas (readlines). O acesso para consulta é sequencial, ao contrário das cadeias, em que o acesso é directo (indexação ou fatiamento). A operação de escrita (alteração) existe nos ficheiros (por elemento, por linha). alterar uma cadeia de caracteres obriga a criar um novo objecto.


P2
Imagine a seguinte sessão no interpretador de Python:


>>> x = [] * 4
>>> y = [[] for i in range(4)]
>>> z = [[]] * 4
>>> x
??? <---- (a)
>>> y
??? <---- (b)
>>> z
??? <---- (c)
>>> x == y
??? <--- (d)
>>> y == z
??? <---- (e)
>>>


Indique, justificando, o resultado devolvido pelo interpretador nas posições assinaladas por ???.

Resposta

O operador de *, quando um dos operandos é numérico e o outro uma sequência, significa repetição. Repete-se o conteúdo, devolvendo-se o resultado dentro da sequência.


(a) []
(b) [[],[],[],[]]
(c) [[],[],[],[]]
(d) False
(e) True

O operador == significa igualdade dos valores e não igualdade das identidades. Isso explica os resultados booleanos (d) e (e). x, y e z são objectos com identidades diferentes, mas y e z têm o mesmo valor.

P3

Suponha que tem duas listas de inteiros. Uma forma de determinar em que medida são diferentes consiste em calcular a soma das diferenças absolutas dos seus elementos. Uma fórmula simples será:





Aqui as listas são x e y, e têm n elementos. As barras verticais denotam o valor absoluto, isto é, correspondem à operação abs em Python.
Implemente o respectivo programa. Os argumentos são as duas listas de inteiros, como se mostra no exemplo da listagem abaixo.


>>> print distancia([1,2,3],[4,8,12])
18
>>>



Resposta

def d(x,y):
""" A distância entre duas listas de inteiros."""
soma = 0
for i in range(len(x)):
soma = soma + abs(x[i] - y[i])
return soma

Usando listas por compreensão:

def d_b(x,y):
return sum([ abs(x[i] - y[i]) for i in range(len(x))])

P4

Suponha que tem um ficheiro de texto onde guarda em cada linha o código da amostra de um produto, seguido do número de uma experiência feita com a amostra, seguido finalmente do valor da temperatura a que a experiência foi feita. A listagem abaixo mostra a parte inicial de um ficheiro tipo.


A 1 20
B 1 40
C 1 30
A 2 -10
C 2 5
B 2 15
B 3 60


Desenvolva um programa que leia esta informação e crie um dicionário em que as chaves são o código do produto e o valor a máxima temperatura a que a experiência foi feita. Admita que a temperatura pode variar entre -100 e 100 graus centígrados. A listagem abaixo mostra o resultado de executar o programa caso o ficheiro tivesse apenas os dados acima indicados.


>>> print dic_max_temp('/tempo/data/exame_r.txt')
{'A': 20, 'C': 30, 'B': 60}
>>>


Resposta

def dic_max_temp(ficheiro):
""" Do ficheiro ao dicionário."""
f_in = open(ficheiro, 'r')
dic = {}
linha = f_in.readline()

while linha != '':
cod,num,temp = linha[:-1].split()
dic[cod] = max(dic.get(cod,-100),int(temp))
linha = f_in.readline()
f_in.close()
return dic

P5

Usando o módulo cTurtle construa um programa que desenhe um tabuleiro quadrado semelhante ao da figura .






Por semelhante entende-se que pode ter outra dimensão. Suponha que tem ao seu dispor o programa que lhe permite desenhar um quadrado da cor que precisar, como se mostra na listagem abaixo.


def quad(tartaruga,lado,cor):
"""Desenha Quadrado da cor."""
tartaruga.begin_fill()
tartaruga.pen(shown=False, pencolor='black', fillcolor= cor)
for i in range(4):
tartaruga.fd(lado)
tartaruga.rt(90)
tartaruga.end_fill()




Resposta

def tabuleiro(tartaruga,n,lado):
tartaruga.pu()
tartaruga.goto(0,0)
tartaruga.pd()

for l in range(n):
# reposiciona no início da linha
tartaruga.setx(0)
tartaruga.sety(tartaruga.ycor() + lado)
# desenha linha de quadrados
for c in range(n):
if (c + l) % 2 == 0:
quad(tartaruga,lado,'white')
else:
quad(tartaruga,lado,'black')
tartaruga.setx(tartaruga.xcor() + lado)

segunda-feira, 25 de janeiro de 2010

Erros que já não deviam existir!!!

Mais uma vez faço uso de um pedido de um aluno para tentar fazer pedagogia. Não sei se é o melhor caminho, até porque pode parecer que estou a reforçar a ideia de que se estuda resolvendo os problemas dos exames. Estes pedidos, que tenho recebido nos últimos dias (afinal o exame está à porta!!), levam-me a questionar sobre o nosso método de ensino e o vosso modo de aprender(?). No vosso caso, é claro que muitos são os que não trabalham ao longo do ano. Na melhor das hipóteses assistem passivamente às aulas e guardam depois uns dias antes do exame para se prepararem. E fazem-no tentando decifrar fora de tempo os acetatos das aulas teóricas, e olhando para as soluções de provas anteriores. No melhor dos casos, tentam resolver à vossa maneira, com imensas dificuldades e com muito não saber. Assim não se aprende! Na melhor das hipóteses passa-se no exame, mas não se aprende. Hoje é véspera de exame e, por isso, vou esquecer as minhas metafísicas.

No exame normal, o problema 4 pedia para ler um ficheiro de texto e construir um dicionário em que as chaves são inteiros e os valores a lista das palavras cujo comprimento é igual ao valor da chave. Este problema obriga a saber o que são e como se podem manipular ficheiros de texto e dicionários. Não é lendo apressadamente durante o exame o Manual do Python que estes conceitos se aprendem! Por isso é melhor praticar com exemplos. Em vários post recentes tratei precisamente de ficheiros de texto e de dicionários... Parece que sem sucesso! Mas vamos à solução que me foi enviada:


def exame(ficheiro):
dic={}
lista=[]
string=''
abrir=open(ficheiro,"r")
string+=abrir.read() #string sera a cadeia de caracteres contida no ficheiro#
lista.append(string.split()) #aqui divido a cadeia de caracteres numa lista e junto-a a 'lista'#

for palavra in range(len(lista)):
dic[palavra]=lista[palavra]
print dic

Dizia, e bem, o vosso colega que não funcionava correctamente, devolvendo apenas um dicionário com uma única chave (0) cujo valor associado era a lista de todas as palavras. Este tipo de erro indicia desde logo onde está o problema. Mas para ter essa percepção é preciso ter experiência de programação (e de erros!). Em vez de dizer já onde está o erro, olhemos para o problema e como o podemos resolver.


Vamos uma vez mais por partes. E neste problema a solução passa por dois subproblemas: (1) obter a lista das palavras e, (2)
a partir da lista das palavras construir o dicionário:


def exame(ficheiro):
# (1) obtém palavras
# (2) forma dicionário
return # dicionário

Primeiro comentário à solução: existem grosso modo dois modos de introduzir informação num programa (pelos parâmetros e por instruções de entrada) e dois modos de comunicar resultados (por return e por instruções de impressão). A escolha depende do problema e do que nos é pedido. Neste caso, era evidente que a entrada de dados era por parâmetro e a saída por return.

Passemos ao primeiro sub-problema. Um ficheiro é uma longa cadeia de caracteres, que vamos ter que ler e dividir em palavras.

# (1) obtém lista palavras
f_in = open(ficheiro, ‘r’)
lista_palavras = f_in.read().split()

Segundo comentário sobre a solução apresentada. Qual o sentido de fazer:

string = ‘’
string += abrir.read()

ou de fazer:

lista = []
lista.append(string.split())

Nenhum! Ainda por cima pode dar origem a erros. As associações entre nomes e objectos são feitas através da operação de atribuição, por exemplo a = 5. Só depois disto é que o nome existe no espaço de nomes, associado a objecto. Neste caso, se quero associar ao nome string a cadeia de caracteres do ficheiro, faço:

string = abrir.read()

e o mesmo para a lista:

lista = string.split()

Neste segundo caso está uma parte do erro do programa. É que, em vez de lista estar associada à lista das palavras, fica associada a uma lista que só tem um elemento que é a lista das palavras! Exemplificando, fica:

lista = [[‘toto’,‘abacadabra’]]

e não

lista = [‘toto’,‘abacadabra’]

uma maneira de resolver o problema seria fazer como no caso de string (feio, como já se disse, mas funcional):

lista = []
lista += string.split()

Tem que se perceber melhor os métodos. E, já agora, outra coisa. Os métodos têm uma sintaxe simples:

<objecto>.<método>(<argumentos>)

Aplico o método ao objecto, usando eventualmente argumentos. Mas qual é o resultado da aplicação?? Um objecto!!! Implicações? Podermos aplicar métodos em cadeia;

<objecto>.<método>(<argumentos>).<método>(<argumentos>)....<método>(<argumentos>)

como no exemplo acima:

lista_palavras = f_in.read().split()

Ao objecto ficheiro aplica-se o método read, que devolve um objecto que é uma cadeia de caracteres à qual se aplica o método split!!!

Agora é tempo de resolver a segunda parte. Para simplificar, vamos supor que não nos preocupam nem as palavras repetidas nem os eventuais sinas de pontuação. A solução que tem isto em conta está no post com a solução do exame.

Então o que temos que fazer:

# (2)
por cada palavra em lista de palavras:
acrescentar a palavra ao dicionário

Temos assim uma estratégia que envolve um ciclo. Quem controla o ciclo é o conteúdo da lista e não os índices (ver outro post recente sobre o assunto). Mas precisamos da chave também! Claro, mas isso é fácil: é que, por definição a chave é igual ao comprimento da palavra. E como acrescentamos? Existe a possibilidade de a chave já existir! Certo. Mas usando um get ( ver post recente sobre o assunto) podemos resolver as duas situações possíveis só com uma instrução. Logo o código:

dic = {}
for palavra in lista_palavras:
comp = len(palavra)
dic[comp] = dic.get(comp,[]) + [palavra]

Pergunta: porque é que, neste caso, temos que fazer primeiro dic = {}, e nos casos anteriores de string e lista não??

Terceiro comentário sobre o código apresentado. O ciclo é controlado pelos índices da lista de palavras (não é por colocar for palavra in ... que palavra é uma palavra!) Então, palavra, vai valer, 0, 1, 2,.... Essas serão as chaves do dicionário criado. Mas não é feita a ligação entre as palavras e o seu comprimento. O que se está a fazer é criar uma associação (chave: valor) = (indice da palavra na lista : palavra). Juntando este erro, ao outro já anteriormente referido e relacionado com o mau uso do append, vai dar algo como:
{0: [‘toto’,‘abacadabra’]}



Juntando agora todas as peças do puzzle:

def exame_ec(ficheiro):
f_in = open(ficheiro,'r')
lista_palavras = f_in.read().split()
dic = {}
for palavra in lista_palavras:
comp = len(palavra)
dic[comp] = dic.get(comp, []) + [palavra]
return dic


E pronto! A complexidade domina-se com método, conhecimento e ... muita prática!

sábado, 23 de janeiro de 2010

Equívocos IV

Programar é um exercício em que se procura equilibrar diferentes objectivos, por vezes contraditórios. Um exemplo? Clareza e eficiência! Quando estamos a aprender a programar manda a prudência que se dê prioridade à clareza, o que resulta normalmente num código simples. Acontece que nem sempre o que é claro para uma pessoa é também claro para outra. Isto da clareza é um pouco subjectivo, mas muitas vezes está ligado a desconhecimento sobre o que é possível fazer com a linguagem.

Vem tudo isto a propósito de um comentário feito por um de vocês a uma solução minha de um dos problemas do exame normal. Esse comentário, fez-me pensar que ainda há quem não sabe como se pode percorrer uma sequência. Embora posteriormente tenha ficado convencido não ser esse o caso do vosso colega resolvi manter este post. Vamos então a isso usando um exemplo simples com listas. O modo de percorrer a lista depende do que queremos fazer.

Exemplo A: imprimir os elementos nas posições ímpares:

def percorre_lista_a(lista):
""" Imprimir os elementos nas posições ímpares."""
for indice in range(len(lista)):
if indice % 2 == 1:
print lista[indice]

A referência é feita às posições e só nos é pedido para imprimir. Logo, vamos percorrer pelos índices e não precisamos guardar os elementos mas apenas imprimir.

Exemplo B: Devolver os elementos nas posições ímpares:


def percorre_lista_b(lista):
"""Devolve os elementos nas posições ímpares."""
resultado = []
for indice in range(len(lista)):
if indice % 2 == 1:
resultado.append(lista[indice])
return resultado

Aqui introduzimos uma variável que vai ficar ligada a um acumulador, uma lista.

Exemplo C: Devolver os elementos que são ímpares:

def percorre_lista_c(lista):
"""Devolve os elementos que são ímpares."""
resultado = []
for elemento in lista:
if elemento % 2 == 1:
resultado.append(elemento)
return resultado

Como queremos os elementos vamos percorrer por conteúdo. Filtramos na mesma os que são ímpares e guardamos os resultados na lista de nome resultado.
Exemplo D: Devolver o elemento maior e sua posição:

def percorre_lista_d(lista):
"""Devolve o maior elemento e respectivo índice."""
maior = lista[0]
indice_maior = 0
for indice,elemento in enumerate(lista):
if elemento > maior:
maior = elemento
indice_maior = indice
return (maior, indice_maior)

Aqui necessitamos das duas coisas: as posições e o conteúdo. Um modo simples de conseguir isso é usar o método enumerate. Não esquecer de colocar a seguir a for o nome das variáveis que vão ficar associadas, em cada etapa do ciclo, ao índice e ao conteúdo!

sexta-feira, 22 de janeiro de 2010

Dúvidas ... e certezas!

Recebi de um colega vosso o seguinte email (apenas reproduzo as partes relevantes):

----------------------------------------------------------------------------------
Pretendo criar um programa que conte o número de ocorrências que cada
palavra tem num ficheiro. Parti do exercício do exame, uma vez que
tenho que retirar sinais de pontuação.

Tenho este código mas não consigo continuar.

def conta_palavras():
  a = open('texto.txt','r')
  tex = a.read().split()
  txt = []
  sinais = ['!',',','.',':']
  for palavra in tex:
      if palavra[-1] in sinais:
          palavra = palavra[:-1]
          txt.append(palavra)
      else:
          palavra = palavra
          txt.append(palavra)



------------------------------------------------------------------------------------

Olhando para o que está feito, e sabendo que a solução do exame foi colocada aqui neste blogue, fica-me a convicção que @ alun@ não sabe da sua existência. Esta situação de ignorância foi confirmada recentemente pela quantidade de alun@s que apareceram para ver o exame mas que desconheciam a existência do blogue, logo nem sequer tinham visto a respectiva solução. É um espanto! Curiosamente tenho tido feedback de pessoas de outros países que estão a seguir o que aqui tenho colocado. Sinal dos tempos!!

Olhando para o código, verifico que a parte para obter a lista de palavras está mais ao menos aceitável. Algumas notas:

a) O nome do ficheiro não é dado como parâmetro da função, o que é uma má prática de programação;
b) Deve-se procurar usar nossos que tenham algum significado. Se um programa tiver centenas de milhares de linhas de código, aparecer por lá um a diz pouco do que é e para que serve;
c) Há uma ineficiência no if: fazer palavra = palavra, serve para quê? Por outro lado, o código pode encolher:

for palavra in text:
if palavra[-1] in sinais:
palavra = palavra[:-1]
txt.append(palavra)

Agora vamos voltar ao problema. Uma coisa que devem procurar fazer é construir o programa passo a passo, deixando claro a vossa estratégia. Por exemplo, neste caso seria algo como:

def fich_dic(ficheiro):
""" Lê o conteúdo do ficheiro e constrói um dicionário
com chave uma palavra e valor o número de vezes que a palavra
ocorre no texto.
"""
# obter a lista de palavras
# filtra os sinais
# constrói dicionário
return # dicionário

Aparentemente avançámos pouco, mas pelo menos temos as tarefas bem identificadas e podemos passar a resolver as que sabemos resolver sem pensar muito:

# obter a lista de palavras
f_in = open(ficheiro, 'r')
lista_pal = f_in.read().split()

Que sinais vamos querer filtrar, e onde podem aparecer? Admitamos que podem aparecer no final, ou isolados, mas sempre só um símbolo. Outra decisão importante é saber se usamos a lista de palavras que já temos ou se fabricamos uma nova. Tendo decidido podemos passar a uma solução:

# filtra os sinais
especiais = ['\n','.',',','!','?']
lista_final = []
for palavra in lista_pal:
# símbolos especiais fora
if palavra in especiais:
continue
elif palavra[-1] in especiais:
palavra = palavra[:-1]
# acrescenta
lista_final.append(palavra)

Todas as decisões estão claras no código: símbolo isolado ou no final, nova lista. Reparar que esta solução torna trivial mudarmos os sinais a considerar. Imagine agora que pode ter outras marcas, como tabulações (‘\t’) ou outras. Acha que se resolve a questão, aumentando apenas a listas de sinais? É que este tem dois símbolos?! Experimente fazer:

# filtra os sinais
especiais = ['\n','.',',','!','?','\t']
lista_final = []
for palavra in lista_pal:
# símbolos especiais fora
if palavra in especiais:
continue
elif palavra[-1] in especiais:
palavra = palavra[:-1]
elif palavra[-2:] in especiais:
palavra = palavra[:-2]
# acrescenta
lista_final.append(palavra)

E verá a surpresa. Mesmo com o teste não funciona!. É que na realidade as marcas de controlo vão ter uma tradução ... bizarra: em vez de ‘\t’ aparece ’\\t’! Coisas. E se tiver marcas com mais símbolos? Pense um pouco e veja como resolvia a questão.

Mas voltemos à questão de querer manter a lista de palavras inicial. Uma solução simples seria:

# filtra os sinais
especiais = ['\n','.',',','!','?']
for indice,palavra in enumerate(lista_pal):
if palavra in especiais:
lista_pal.remove(palavra)
elif palavra[-1] in especiais:
lista_pal[indice] = palavra[:-1]

Note-se o recurso a enumerate!

Agora a construção do dicionário. Não há muito a dizer. A chave são as palavras, e os valores inteiros. Sendo os valores objectos imutáveis podemos usar o método get, e eis a solução para esta parte.

# Constrói dicionário
dic = {}
for palavra in lista_final:
dic[palavra] = dic.get(palavra,0) + 1
return dic

Como explicámos nas aulas e também neste blogue o método get é uma fprma elegante de resolver esta questão: Se a palavra ainda não estiver no dicionário, o método devolve o valor por defeito (0), que é somado a 1 , sendo o novo para colocado no dicionário. Se existir, devolve o valor que é na mesma somado e actualizado.

Vamos agora juntar todos os bocados:

def fich_dic(ficheiro):
""" Lê o conteúdo do ficheiro e constrói um dicionário
com chave uma palavra e valor o número de vezes que a palavra
ocorre no texto.
"""
# obter a lista de palavras
f_in = open(ficheiro, 'r')
lista_pal = f_in.read().split()
# filtra os sinais
especiais = ['\n','.',',','!','?']
lista_final = []
for palavra in lista_pal:
# símbolos especiais fora
if palavra in especiais:
continue
elif palavra[-1] in especiais:
palavra = palavra[:-1]
# acrescenta
lista_final.append(palavra)
# Constrói dicionário
dic = {}
for palavra in lista_final:
dic[palavra] = dic.get(palavra,0) + 1
return dic

quinta-feira, 21 de janeiro de 2010

Equívocos III

Sabemos que os ficheiros de texto são uma longa cadeia de caracteres com características especiais. O seu conteúdo está armazenado de modo permanente num disco externo. Para aceder ao seu conteúdo o ficheiro tem que ser primeiro aberto. O que acontece quando fazemos:


>>> f_in = open(‘/tempo/data/ficheiro.txt’,’r’)

O que acontece é semelhante ao que se passa quando fazemos, por exemplo, a= 5. Aqui, associamos o nome a ao objecto 5. No caso da instrução acima, associamos o nome f_in a um objecto do tipo ficheiro:


>>> f_in = open('/tempo/data/ficheiro.txt')
>>> f_in
<open file '/tempo/data/ficheiro.txt', mode 'r' at 0xe02c50>
>>>

Podemos agora, através do nome f_in aceder ao conteúdo do ficheiro. O acesso é por norma sequencial, existindo uma marca para a posição (o caracter) que pode ser acedido. Essa marca pode ser manipulada pela operação seek.

Existem três comandos fundamentais de leitura: read, readline, readlines. No primeiro caso lemos o conteúdo de todo o ficheiro a partir da marca, no segundo, lemos a próxima linha a partir da marca, no terceiro, lemos todas as linhas partir da marca.

Vejamos o uso de read:

def le(fich):
f_in = open(fich,'r')
texto = f_in.read()
print 'Todo:\n',texto

f_in.seek(0,0)

texto = f_in.read(7)
print 'Parte:\n',texto

f_in.close()

if __name__ == '__main__':
le('/tempo/data/ficheiro.txt')

Notar que podemos ler apenas n caracteres. Notar ainda que o ficheiro fica agora guardado como uma cadeia de caracteres e associado ao nome texto. Finalmente, esteja atento ao modo como voltamos ao início do ficheiro com a operação seek. Mas admitamos que queríamos manipular o ficheiro palavra a palavra. Como proceder?

def le_palavras(fich):
f_in = open(fich,'r')
texto = f_in.read()
lista_palavras = texto.split()
print 'Todas as palavras:\n',lista_palavras

f_in.seek(0,0)
texto = f_in.read().split()
print 'Todas as palavras - Bis:\n',texto

f_in.close()


if __name__ == '__main__':
le_palavras('/tempo/data/ficheiro.txt')

Veja-se como podemos fazer de duas maneiras diferentes. Percebe porque é o mesmo? Note-se ainda que passamos a ter uma lista de cadeias de caracteres.

Passemos agora à leitura por linhas.

def le_linha(fich):
f_in = open(fich,'r')
linha = f_in.readline()
print 'Linha a linha:\n'

while linha != '':
print 'Linha: ', linha
linha = f_in.readline()

f_in.seek(0,0)

texto = f_in.read().split('\n')
print 'Linha a linha:\n'
for linha in texto:
print 'Linha: ',linha

f_in.close()


if __name__ == '__main__':
le_linha('/tempo/data/ficheiro.txt')

No programa apresentado vemos duas maneiras de fazer o processamento do conteúdo do ficheiro linha a linha. Se executar o código verá que existe uma diferença subtil entre usar o readline e a associação read-split. Veja bem:

>>>
Evaluating ficheiros.py
Linha a linha:

Linha: Este pequeno texto pode

Linha: ser um exemplo do que

Linha: significa um ficheiro de...

Linha: texto

Linha a linha:

Linha: Este pequeno texto pode
Linha: ser um exemplo do que
Linha: significa um ficheiro de...
Linha: texto
Linha:
>>>

O readline mantém as marcas de fim de linha!!! Como pode alterar a situação? É muito fácil:

def le_linhas(fich):
f_in = open(fich,'r')
linha = f_in.readline()
print 'Linha a linha:\n'

while linha != '':
print 'Linha: ', linha[:-1]
linha = f_in.readline()
f_in.close()

Como se vê retiramos o último caracter à linha que é precisamente o \n! Também podemos ler as linhas aos bocados:

def le_linhas(fich):
f_in = open(fich,'r')
linha = f_in.readline(5)
print 'Todas as linhas:\n'

while linha != '':
print 'Linha: ', linha
linha = f_in.readline(5)
f_in.close()

Neste caso em vez de ler a linha toda de uma vez, ela vai ser lida considerando, no exemplo apresentado, 5 caracteres de cada vez. Experimente!

Passemos à última situação. Vamos ler todas as linhas de uma vez, usando readlines. O resultado é uma lista de cadeias de caracteres, em que cada elemento da lista é uma linha.

def le_linhas(fich):
f_in = open(fich,'r')
lista_linhas = f_in.readlines()
print 'Todas as linhas:\n', lista_linhas

f_in.seek(0,0)

linha = f_in.readline()
print 'Linha a linha:\n'

while linha != '':
print 'Linha: ', linha
linha = f_in.readline()

f_in.seek(0,0)

texto = f_in.read().split('\n')
print 'Todas as linhas:\n', texto

f_in.close()


if __name__ == '__main__':
le_linhas('/tempo/data/ficheiro.txt')

Notar que é conservado o caracter fim de linha. Para comparação apresentamos como se podia fazer recorrendo ao readline e ao read. Veja as diferenças!

Para concluir. Também neste caso podemos limitar o número de linhas a ler (no exemplo abaixo, serão 3 as linhas.)

def le_linhas(fich):
f_in = open(fich,'r')
lista_linhas = f_in.readlines(3)
print 'Todas as linhas:\n', lista_linhas
f_in.close()

Em conclusão, apenas dizer que o comando a usar depende do problema. Queremos manipular caracteres, palavras ou linhas? Não esquecer também do detalhe do sinal de fim de ficheiro.

Oops! Já quase que me esquecia. Sempre que tiver que manipular todo um ficheiro, linha a linha, pode também usar esta construção, simples e clara:

def le_ficheiro(ficheiro):
f_in = open(ficheiro, 'r')
for linha in f_in:
print 'Linha: ', linha
f_in.close()
.

segunda-feira, 18 de janeiro de 2010

Dicionários

Existem muitos modos de construir dicionários. Podemos, por exemplo, começar com um dicionário vazio e depois ir acrescentando pares (chave : valor), ou podemos indicar explicitamente os pares na inicialização. Neste último caso as sintaxes possíveis são variadas, e aconselhamos o leitor a ver isso mesmo no manual da linguagem. O que queremos ilustrar aqui é outra possibilidade: temos uma lista que contém alternadamente as chaves e os valores correspondentes e pretendemos, a partir dessa lista, construir o correspondente dicionário. Estamos a admitir que não existem chaves repetidas na lista.

A solução trivial será:

def dicio_lista(lista_chaves_valores):
"""Constrói um dicionário a partir de uma lista com chaves e valores
alternados."""
dicio ={}
for i in range(len(lista_chaves_valores)/2):
dicio[lista_chaves_valores[2*i]] = lista_chaves_valores[2*i + 1]
return dicio

Mas podemos fazer melhor usando o método zip e o construtor para dicionários dict:

def dicio_lista_b(lista_chaves_valores):
"""Constrói um dicionário a partir de uma lista com chaves e valores
alternados."""
return dict(zip(lista_chaves_valores[::2], lista_chaves_valores[1::2]))

Nas duas soluções tivemos que lidar com a separação dos elementos em chaves e valores. As chaves estão nas posições pares e os valores nas posições ímpares da lista. Claramente preferimos a segunda!

Equívocos II

Existem dois métodos que se aplicam a dicionários que, por serem semelhantes, levam a alguns erros. São setdefault e get. Para explicitar as diferenças vamos considerar um exemplo concreto. Admitamos que estamos a fazer o índice de um livro, isto é queremos associar a cada palavra a indicação das páginas do livro onde essa palavra ocorre. Vamos usar um dicionário para guardar esta associação. Suponhamos que queremos implementar o método que acrescenta uma palavra (e a respectiva página) ao índice. Uma solução trivial seria:

def add_palavra_triv(indice,palavra,pagina):
if palavra in indice:
indice[palavra].append(pagina)
else:
indice[palavra] = [pagina]

Note-se que não é preciso fazer if palavra in indice.keys(). Atente-se ainda no modo distinto como temos que tratar o caso de a palavra estar, ou não, no dicionário. O leitor mais conhecedor de Python pode saber que é possível fazer tudo sem precisar do teste, e achar que usar o setdefault ou o get é o mesmo. Propõe por isso duas soluções alternativas:

def add_palavra_get(indice,palavra, pagina):
indice.get(palavra,[]).append(pagina)

def add_palavra_set(indice,palavra, pagina):
indice.setdefault(palavra,[]).append(pagina)

Mas, para sua surpresa, se executar o código seguinte o resultado não é bem o que estava à espera.

>>> dic = {'eu':[1,5,7], 'tu': [2,4,7]}
>>> add_palavra_get(dic, 'eu', 10)
>>> add_palavra_get(dic, 'ele', 20)
>>> print dic
{'eu': [1, 5, 7, 10], 'tu': [2, 4, 7]}
>>> add_palavra_set(dic,'tu', 8)
>>> add_palavra_set(dic,'ele', 33)
>>> print dic
{'eu': [1, 5, 7, 10], 'tu': [2, 4, 7, 8], 'ele': [33]}

Fica claro que no caso em que a chave não está no dicionário os dois métodos são diferentes: enquanto setdefault acrescenta o novo par, o mesmo não acontece com o método get.

Podemos definir como regra que sempre que os valores forem objectos mutáveis deve preferir setdefault. Caso se tratem de objectos imutáveis deve optar por get. Por exemplo, se o que pretende é apenas contar quantas vezes uma palavra ocorre num texto, deve usar:


indice[palavra] = indice.get(palavra,0) + 1