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

Clarificação

Este blogue é de apoio à cadeira de IPRP, como se clarifica no cabeçalho e na side-bar Bem Vindo. Por isso, e com naturalidade, é um blogue moderado por mim: todos os comentários têm que ser previamente aprovados antes de divulgados. Todos conhecemos o que se passa na blogosfera para saber que isso é um imperativo.

Fiz um post sobre o problema da fraude, no contexto de IPRP. Não quis fazer um exercício de moral, genérico, sobre o tema. Nem o vou fazer agora. Tudo o que tinha a conversar sobre o assunto, fi-lo em privado com quem entendi, e em privado ficará!

De tudo isto resulta que todos os comentários sobre o assunto não serão publicados. Os dois que aparecem no post referido, apenas o foram devido ao princípio da inércia: eu andava até essa data a aprovar tudo sem ler. E não penso que deva agora fazer censura sobre o que objectivamente deixei passar. Mas vai passar a ser diferente.

O problema da fraude não é para mim uma questão polémica. Quem quiser discorrer sobre o assunto, terá que procurar outros locais para o fazer.

Equívocos I

Sempre que corrigimos exames encontramos muitos erros, alguns muito básicos outros mais sofisticados. Fazer uma colectânea com os mais comuns, procurando explicar onde está o erro parece ser uma boa ideia. Tenho dúvidas se será muito pedagógico. Mas enquanto não me decido sobre esta metafísica questão indico aqui um ou outro. Estou aberto a que me enviem as vossas dúvidas sobre outras situações, que procurarei igualmente clarificar.

Coisas com listas

Um erro típico é tentar aceder a uma parte da lista que não existe, recebendo um erro do género IndexError: list assignment index out of range. Se uma lista tem, por exemplo, 5 objectos e perguntamos pelo objecto na posição 7 ... lá temos o erro. Mas pode ser mais subtil. Olhemos para o código:

x = []
for i in range(5):
x[i] = i

O erro é exactamente o mesmo. Acontece que a lista x não tem nenhum valor quando tentamos lá colocar algo. Mesmo que seja na posição 0. É que esta não existe numa lista vazia!! No exemplo acima, uma maneira de resolver a questão seria usar listas por compreensão:


x = [ i for i in range(5)]

Assim fabricamos os elementos, e colocamos depois tudo numa lista.

Outra questão prende-se com o facto de as listas serem objectos mutáveis. Assim podemos substituir o seu valor sem alterar a sua identidade. Existem muitos métodos que se podem aplicar a listas. Alguns têm por objectivo alterar o objecto. Não têm que devolver nada. Mas em Python, quando não há um return explícito no código, é devolvido o objecto None. Exemplo de um erro comum, por não saberem o que isto significa.

x = []
for i in range(5):
x= x.append(i)

print x

Executar o código acima resulta no erro AttributeError: 'NoneType' object has no attribute 'append'. Porquê? Precisamente porque na primeira passagem no interior do ciclo, x.append(i) coloca 0 na lista x, mas depois devolve None, valor esse que é associado ao mesmo objecto x pela instrução de atribuição (x = ...), desfazendo a anterior ligação ao objecto lista. Como resultado, na segunda passagem pelo ciclo temos um erro pois x está agora associado a um objecto do tipo None, que não possui nenhum método append.

domingo, 17 de janeiro de 2010

Yin & Yang

Muitos são os que advogam que vivemos num mundo pleno de opostos: ciências e humanidades é um exemplo paradigmático. E, mesmo dentro de cada categoria, ainda nos oferecem outras oposições: ciência versus engenharia ou arte versus design. Em qualquer das situações alguns ainda pretendem destacar o nobre (e.g., ciência) e o menos nobre (e.g., design). Nada de mais ilusório. Tudo se interliga e alimenta mutuamente.




A programação é um campo onde se juntam os quatro aspectos referidos: ciência e arte, engenharia e design. O que os une: a criatividade! Um excelente programa é aquele que é eficaz e eficiente, mas também belo, elegante e funcional. Esses aspectos sobressaem tanto mais quanto mais complexo for o problema que são supostos resolver. Dominar a complexidade é pois a tarefa principal do programador consciente. Isso só se consegue com rigor e disciplina, mas também com treino, exploração e intuição.

O exemplo que a seguir apresentamos é simples, servindo apenas para demonstrar algumas ideias básicas sobre como dominar a complexidade. O problema é o seguinte: quais os pares de números inteiros positivos, até um certo limite, que são primos entre si. Dos números dizem-se primos entre si se apenas admitirem como divisores comuns a unidade.

A estratégia será a seguinte:
a) gerar os pares de números até ao limite dado
b) para cada par: calcular os divisores de cada número, determinar a sua intersecção, e verificar se esta se reduz ao número 1.
c) acumular os resultados parciais


def primos_entre_si(limite):
"""Devole a lista dos pares de números que são primos entre so."""
# Acumula resultado
resultado = []
# Gera pares a testar
for i in range(limite):
for j in range(limite):
# testa
if pes(i,j):
resultado.append((i,j))
# Devolve resultado
return resultado

Este código traduz a nossa estratégia. É simples e claro. Mas mais do que isso: torna explícito algumas decisões. Por exemplo, que vamos acumular os resultados numa lista. Como princípio de projecto estas decisões devem ser tomadas o mais tarde possível. O ponto b) está concentrado numa chamada de função (pes). Mas nem tudo são rosas no código apresentado. Basta ter consciência de uma ineficiência: analisa pares de números idênticos, (n,n), ou repetidos, (n,m) e (m,n). Este problema pode ser tratado agora ou mais tarde, pois não interfere com o resto do programa. Este facto, poder deixar para mais tarde, é sempre um sinal de que o programa está a ser bem desenhado! Passemos então à a questão de saber se dois números são primos ente si (função pes). Aqui entra o conhecimento que temos sobre o domínio: dois números são primos entre si se o seu máximo divisor comum for 1.


def pes(m,n):
"""Primos entre si?"""
return mdc(m,n) == 1

E agora, para concluir, o programa que calcula o máximo divisor comum. Uma vez mais vem em nosso auxílio o conhecimento que temos sobre o Algoritmo de Euclides:

def mdc(m,n):
"""Algoritmo de Euclides para o máximo divisor comum."""
if n == 0:
return m
else:
return mdc(n, m % n)

É recursivo! Mas podemos facilmente propor uma solução iterativa, se não estamos confortáveis com a recursividade:

def mdc_iter(m,n):
while n > 0:
m, n = n, m % n
return m

Podemos também ser tentados a usar:

def mdc_iter_b(m,n):
while n:
m, n = n, m % n
return m

A diferença entre estas duas últimas soluções é ténue. Mas não gostamos da última. O código fica menos claro e mais dependente do modo como a linguagem Python interpreta a condição Falso.

Por falar da linguagem, e do conhecimento que se tem sobre a linguagem, podíamos usar como solução para o cálculo do máximo divisor comum:

def mdc_py(m,n):
from fractions import gcd
return gcd(m,n)

A partir da versão 2.6 apareceu o novo tipo Fraction. Como sabemos, a simplificação de fracções passa por saber o máximo divisor comum. Por isso o módulo usa esta operação.

Regressemos ao programa principal para corrigir as ineficiências. Sabemos o que fazer para evitar repetições:

def primos_entre_si_b(limite):
"""Devolve a lista dos pares de números que são primos entre so."""
# Acumula resultado
resultado = []
# Gera pares a testar
for i in range(1,limite):
for j in range(i+1,limite):
# testa
if pes(i,j):
resultado.append((i,j))
# Devolve resultado
return resultado

def pes(m,n):
"""Primos entre si?"""
return mdc(m,n) == 1

Foi só mexer no comando range dos dois ciclos for!

Juntemos as peças do puzzle:

# Primos entre si

def primos_entre_si(limite):
"""Devole a lista dos pares de números que são primos entre so."""
# Acumula resultado
resultado = []
# Gera pares a testar
for i in range(1,limite):
for j in range(i+1,limite):
# testa
if pes(i,j):
resultado.append((i,j))
# Devolve resultado
return resultado

def pes(m,n):
"""Primos entre si?"""
return mdc(m,n) == 1

def mdc(m,n):
from fractions import gcd
return gcd(m,n)

if __name__ == '__main__':
print primos_entre_si(100)


E podemos terminar. Ou será que não? Vamos ver o que acontece se eu coloco os números que são primos entre si numa grelha quadrada. Para ficar mais bonito vou usar o módulo cImage. Mas não quero mexer no que fiz: o cálculo e a visualização devem estar bem separados. A estratégia, uma vez mais neste caso, é simples:

a) criar uma imagem vazia com a dimensão do número máximo (do limite)
b) colorir os pixeis nas posições que correspondem a pares de números primos entre si:

from cImage import *

def mostra_pes(limite,lista_pares):
janela = ImageWin('Primos',limite,limite)
imagem = EmptyImage(limite,limite)
pixel_vermelho = Pixel(255,0,0)
for cord_x,cord_y in lista_pares:
imagem.setPixel(cord_x,cord_y,pixel_vermelho)
imagem.setPixel(cord_y,cord_x,pixel_vermelho)
imagem.draw(janela)
janela.exitOnClick()

Ligamos as duas partes:

def main(limite):
lista = primos_entre_si(limite)
mostra_pes(limite,lista)

Notar como restaurámos a matriz completa, incluindo os pares (x,y) e (y,x)). E executamos!




Que bela textura obtida a partir da matemática... olhem bem para a imagem e descubram o padrão. Há uma ordem escondida!!

Vimos que era importante ter conhecimento sobre o domínio mas também sobre a linguagem. Além disso, com rigor e disciplina, avançando passo a passo, não procurando resolver todo o problema de uma só vez, construímos uma solução simples, clara, eficiente, reutilizável. Arte, Design, Ciência e Engenharia.





(*) As imagens são do excelente livro: The Plenitude: Creativity, Innovation, and Making Stuff, Rich Gold, MIT Press, 2007.

sábado, 16 de janeiro de 2010

Anagramas

Em post anterior falámos de Will McGunan e dos problemas que colocam quando está a seleccionar candidatos para um emprego onde é preciso saber Python. A segunda questão que coloca é um pouco mais complexa. Temos uma palavra e uma lista de palavras guardadas num ficheiro, uma por linha. A ideia é construir todos os anagramas da palavra dada que são palavras da lista.

Por exemplo, roma é um anagrama de amor.


Vamos construir a solução por etapas.

def ana(s,file_in):
anas=anagrams(s)
lst_ana=del_duplicates(anas)
resultado=filtro(file_in,lst_ana)
return resultado

Este é o nosso programa principal! Usa três programas auxiliares:

anagrams: para calcular a lista de anagramas de uma palavra

del_duplicates: que elimina eventuais duplicações

filtro: que selecciona quais os anagramas que são palavras válidas.

Analisemos os casos mais simples, começando pelo processo de filtragem:

def filtro(file_in,lst_ana):
"""One word in each line."""
fin=open(file_in)
lst_pal=[pal.strip() for pal in fin.read().split('\n')]
lst_ana_final= [pal for pal in lst_pal if pal in lst_ana]
fin.close()
return lst_ana_final

Lemos o ficheiro todo de uma só vez, dividimos em palavras e retiramos eventuais espaços em branco (linha 4). Na linha 5, apenas consideramos as palavras que estão quer na lista dos anagramas quer na lista das palavras. É agora a vez de eliminar as duplicações:

def del_duplicates(lst):
if len(lst) == 0:
return lst
elif lst.count(lst[0]) > 1:
return del_duplicates(lst[1:])
else:
return [lst[0]] + del_duplicates(lst[1:])
return [lst[0]] + del_duplicates(lst[1:])

Trata-se de uma solução recursiva: a função chama-se a ela própria. Não era necessário usar recursividade, podendo, caso queira pensar numa solução mais convencional.

Deixámos para o fim a questão de encontrar os anagramas. E aqui, mais uma vez, a solução é recursiva. No entanto, agora não é fácil encontrar uma solução que não faça usa de recursividade. Experimente por si!

def anagrams(s):
# Return the list of anagrams for s
if s == "":
return [s]
else:
ans = []
for w in anagrams(s[1:]):
for pos in range(len(w)+1):
ans.append(w[:pos]+s[0]+w[pos:])
return ans

Porque, e como, funciona esta solução? A ideia é a seguinte: fabricamos (recursivamente) os anagramas com a palavra sem o seu primeiro caracter, para depois inserirmos esse caracter em todas as posições possíveis em cada um dos anagramas gerados. O processo termina se a palavra não tiver nenhum caracter.

E pronto. Só falta testar. Arranje um ficheiro com palavras e faça o exercício. Acho que, desta vez, era capaz de conseguir o lugar!!

Metarmofoses

Will McGugan é um conhecido programador de Python, especialista no desenvolvimento de jogos. Mas também trabalha na selecção de candidatos a emprego onde o conhecimento de Python é importante. No seu blogue dá conta de duas questões tipo que costuma colocar aos candidatos. A primeira é a seguinte. Suponha que precisa de desenvolver um programa que receba como parâmetro um número inteiro e devolva uma cadeia de caracteres que representa o número, mas onde foram colocadas vírgulas para marcar os milhares.

Por exemplo:

>>> milhares_com_virgulas(1234)
'1,234'
>>> milhares_com_virgulas(123456789)
'123,456,789'
>>> milhares_com_virgulas(12)
'12'

Estes exemplo são elucidativos do que se pretende e também dos casos que têm que se ter em conta. Existem várias maneiras de resolver o problema. Pense um pouco antes de olhar para a solução e procure desenvolver a sua solução.


def milhares_com_virgulas(num):
# muda representação
cadeia = list(str(num))
# calcula resultado
resultado = ''
while len(cadeia) > 3:
aux = ',' + ''.join(cadeia[len(cadeia)-3:])
resultado = aux + resultado
cadeia = cadeia[:-3]
resultado = ''.join(cadeia)+ resultado
return resultado

A nossa solução faz uso de uma mudança de representação e dos comandos str, list e join para comutar entre números, listas e cadeias de caracteres. Socorre-se também de uma variável acumulador onde vamos construindo o resultado.

terça-feira, 12 de janeiro de 2010

Notas Exame

Estão no WoC as notas finais após a realização do exame de época normal.

Os(as) Alunos(as):


Ângelo Pinto (LEI)
António Marques (LEI)
Daniel Frutuoso (LEI)
Mafalda Carvalho (LEGI)
Maria Goretti (LEGI)
Sílvia Rocha (LEGI)
Sofia Carvalho (LEGI)

devem contactar o docente, QUINTA-FEIRA, dia 14 de Janeiro, pelas 14h30.

domingo, 10 de janeiro de 2010

Fraude

Estou a corrigir os exames, como sabem. E na fase em que estou, já deu para perceber muita coisa. Mas só quero referir uma: a fraude. Ou como se diz mais vulgarmente, copianço. E começo por relembrar o que vinha escrito no cabeçalho dos mini testes:

A fraude denota uma grave falta de ética e constitui um comportamento não admissível num estudante do ensino superior e futuro profissional licenciado. Qualquer tentativa de fraude leva a anulação da prova tanto do facilitador como do prevaricador.

O discurso é claro. Quem copiou e quem deixou copiar está reprovado na cadeira. Mesmo que isso só tenha envolvido uma parte ínfima da prova (ou dos trabalhos, ou do que seja que deviam ter feito sozinhos(as)).

Mas para que fique ainda mais claro, envio a url de um sítio com o enunciado da política com que me identifico.

http://robotics.eecs.berkeley.edu/~pister/etc/Cheating.htm

Leiam, e pensem bem, antes de se meterem por caminhos obscuros. Quem já se meteu ... lamento!

Mini Teste Especial

1. Que modos existem para quebrar a execução normal de um ciclo? Quais
são as suas diferenças?

Solução

break: termina imediatamente a execução do ciclo envolvente;

return: termina a execução do ciclo e do programa. Devolve o objecto associado ou None, caso não exista nenhum.

2. Considere a seguinte definição:

def add2me(x):
return x + x

Indique, justificando, quais os resultados esperados ao executar os comandos:

>>> add2me(23.4)
???
>>> add2me({1:’a’})
???
>>> add2me(’toto’)
???
>>> add2me([1,2,3])
???

Solução

O operador + é um operador sobrecarregado, podendo ser utilizado com objectivos de diferentes tipos. No entanto, em cada caso os operandos têm que ser compatíveis: posso somar um inteiro com o float, mas não posso somar um inteiro com um alista.

Na linha 2: 46.8. é o resultado de somar um número real consigo próprio.

Na linha 4: Dá erro pois a operação de soma não está definida para dicionários.

Na linha 6: ‘totototo’. Resulta da concatenação da cadeia de caracteres com ela própria.

Na linha 8: ‘[1,2,3,1,2,3]. Como no caso anterior, mas agora com as listas.

3. Admitindo que seq é uma lista de caracteres, explique o que faz o programa
da listagem abaixo. A sua resposta não pode ser baseada em generalidades.
Em concreto, queremos saber para cada valor de seq qual o valor
devolvido pela instrução return.

def mist(seq):
lst = []
for i in range(len(seq)):
for j in range(i+1):
lst.append(’’.join(seq[j:i+1]))
return lst

Solução

A melhor maneira de perceber o que fazem estas funções mistérios é simular à mão a sua execução. Se utilizarmos como entrada [‘a’,’v’,’e’] o resultado será:
['a', 'av', 'v', 'ave', 've', 'e']

Neste programa temos um nome, lst associado a um objecto do tipo lista e que vai sendo alterado num processo repetitivo. A repetição processa-se em dois tempos: em primeiro lugar, é feito um ciclo indexado pelas posições dos caracteres na lista passada como argumento, isto é, seq. Para cada uma destas posições temos um segundo ciclo. Nele, pegamos num pedaço da sequência inicial, ou seja, numa parte da lista de caracteres, juntamos esses caracteres numa cadeia de caracteres e guardamos o resultado na lista lst. Mas que parte guardamos? A que vai desde a posição j até (exclusivé) i+1. Admitamos que estamos na posição 1, de seq. Então, no ciclo interior vamos juntar as cadeias seq[0:2] e seq[1:2]. No final, o efeito é o de devolver uma lista de cadeias de caracteres formadas por todas as sub-cadeias de seq de comprimento 1,2, ... , len(seq).

4. Suponha que tem um ficheiro com endereços de correio electrónico,
um por linha. Pretendemos um programa que fabrique, a partir desses endereços,
a URL da página pessoal da Internet, de acordo com a regra:
ernesto@ dei.uc.pt dá origem a http://www.dei.uc.pt/ernesto. Os valores obtidos devem ser guardados num ficheiro, uma vez mais com um por linha.

Solução

Como cada endereço está numa linha, a nossa solução vai fazer uso desse facto. Vamos repetir a operação de leitura de uma linha, extracção da informação, fabricação da url, e guardar tudo num novo ficheiro. Daí uma solução simples:

def email_url(fich_in, fich_out):
""" URL a partir do endereço de email."""
f_in = open(fich_in, 'r')
f_out = open(fich_out, 'w')
linha = f_in.readline()
while linha != '':
nome,dominio = linha[:-1].split('@')
url = 'http://' + dominio + '/' + nome + '\n'
f_out.write(url)
linha = f_in.readline()
f_in.close()
f_out.close()

Repare-se como na linha 7, retiramos o ’\n’ que aparece no fim e separamos o resultado na parte anterior e posterior a ’@’, ou seja, respectivamente o nome e o domínio. Na linha 8 construímos o endereço da página web sem nos esquecermos de colocar no final a indicação de fim de linha (’\n’).


5. Suponha que quer ordenar um vector de inteiros positivos, todos diferentes, mas não pode usar nenhuma das primitivas de Python que o permitem
fazer de modo directo. Uma estratégia simples consiste no seguinte: para
cada elemento do vector conto o número de elementos que lhe são inferiores.
Com base nessa contagem, fico a saber a posição definitiva do elemento em
causa. Com efeito se, por exemplo, ele tiver 4 elementos inferiores, deverá ser
colocado na posição 5. Implemente um programa de acordo com a estratégia
de ordenamento indicada.

Solução

Vamos usar uma cópia da lista, onde efectuaremos as alterações. Como vamos precisar dos índices e dos elementos, usamos a função enumerate (linha 3).

def ord_conta(seq):
copia = seq[:]
for i,elem in enumerate(seq):
# Conta
conta = 0
for comp in seq:
if comp < elem :
conta = conta + 1
# Altera
copia[conta] = elem
return copia

sexta-feira, 8 de janeiro de 2010

Exame Normal

1.
(a)Identidade: referência para a zona da memória onde se encontra armazenado o objecto. Valor: o estado do objecto num dado momento, em termos simples,o seu conteúdo. Tipo: determina o conjunto de valores e as operações que podem ser efectuadas com os objectos do tipo.


(b)As chaves de um dicionário têm que ser objectos imutáveis.

2.
Nas duas primeiras linhas associamos nomes diferentes ao mesmo objecto, logo a identidade tem que ser a mesma. Na terceira linha criamos uma cópia desse objecto e associamos-lhe o nome c. Assim neste último caso a identidade tem que ser diferente. A figura ilustra a situação.





3.

Para a soma de uma constante a um vector apresentamos duas soluções.

def soma_constante(c, vector):
""" Soma a constante c a cada uma das componentes do vector v."""
return [ c + elem for elem in vector]

def soma_constante_b(c, vector):
""" Soma a constante c a cada uma das componentes do vector v."""
res= []
for elem in vector:
res.append(c+elem)
return res


4.
De um ficheiro a um dicionários de frequências. Apresenta-se primeiro uma solução simples, mas que é aceite como resposta correcta.

def fich_dic_a(ficheiro):
""" Lê o conteúdo do ficheiro e constrói um dicionário
com chave um inteiro e valor uma lista com as palavras de
comprimento igual ao valor da chave.
"""
f_in = open(ficheiro, 'r')
lista_pal = f_in.read().split()
dic = {}
for palavra in lista_pal:
comp = len(palavra)
dic[comp] = dic.get(comp,[]) + [palavra]
return dic

Agora uma solução que prevê o uso de símbolos especiais.

def fich_dic_b(ficheiro):
""" Lê o conteúdo do ficheiro e constrói um dicionário
com chave um inteiro e valor uma lista com as palavras de
comprimento igual ao valor da chave.
"""
f_in = open(ficheiro, 'r')
lista_pal = f_in.read().split()
dic = {}
especiais = ['\n','.',',','!','?']
for palavra in lista_pal:
if palavra[-1] in especiais:
palavra = palavra[:-1]
comp = len(palavra)
if comp:
dic[comp] = dic.get(comp,[]) + [palavra]
return dic

Notar o uso de uma lista onde guardamos caracteres especiais que não devem fazer parte da palavra. Notar ainda o teste ao comprimento: se um símbolo estiver isolado, ao ser retirado passa a ser uma cadeia sem caracteres e não deve ser incluída.

Finalmente, uma solução em que as palavras repetidas só são incluídas um vez.

def fich_dic_c(ficheiro):
""" Lê o conteúdo do ficheiro e constrói um dicionário
com chave um inteiro e valor uma lista com as palavras de
comprimento igual ao valor da chave.
"""
especiais = ['\n','.',',','!','?']
f_in = open(ficheiro, 'r')
lista_pal = f_in.read().split()
# filtra
lista_final = []
for palavra in lista_pal:
# símbolos especiais fora
if palavra in especiais:
continue
elif palavra[-1] in especiais:
palavra = palavra[:-1]
# repetições fora
if palavra not in lista_final:
lista_final.append(palavra)
# Constrói dicionário
dic = {}
for palavra in lista_final:
comp = len(palavra)
dic[comp] = dic.get(comp,[]) + [palavra]
return dic


5.
Nesta solução temos uma definição auxiliar (tri) para desenhar um triângulo incompleto. O programa main é responsável por usar este programa, tantas vezes quantos os triângulos incompletos que queremos desenhar, e ainda definir os valores do lado, do incremento e da rotação.

from cTurtle import *

def tri(tartaruga,lado, inc):
""" Desenha um triângulo incompleto."""
for i in range(3):
tartaruga.fd(lado)
tartaruga.rt(120)
lado = lado + inc
tartaruga.hideturtle()

def main(n):
""" Desenha n triângulos incompletos que vão rodando."""
tartaruga = Turtle()
lado = 30
inc_lado = 10
head = 0
for i in range(n):
tartaruga.setheading(heading() + head)
tri(tartaruga,lado, inc_lado)
lado = lado + 3 * inc_lado
head = head + 8

tartaruga.exitOnClick()

if __name__ =='__main__':
hideturtle()
main(5)

domingo, 3 de janeiro de 2010

É o máximo!

Acontece com frequência termos que calcular o máximo (ou o mínimo) de uma função f. Isto é, queremos saber qual o valor de x para o qual f(x) assume o valor máximo (ou o mínimo).

O princípio de solução para esta questão não é difícil: calculamos o valor de da função em todos os pontos, identificamos o valor mais elevado (ou mais baixo, no caso do mínimo) e com isso sabemos o valor de x. Trata-se de uma actividade que aponta claramente para o uso de um ciclo for.

A questão mais delicada reside no facto de os cálculos serem feitos com o valor de f(x), mas o que pretendemos identificar é o argumento x. Por isso vamos precisar de manter memorizado o valor melhor para a função e o correspondente valor do argumento. Quanto ao mais podemos assumir que os valores do domínio podem estar ou não ordenados.

Vejamos a implementação e o teste para o caso da função seno.

from math import sin,radians

def arg_max(dom,func):
"""Calcula o máximo da função no domínio dado."""
x_max = dom[0]
f_max = func(x_max)

for x in dom:
x_val = func(x)
if x_val > f_max:
x_max,f_max = x,x_val
return x_max


if __name__ == '__main__':
valores = [radians(g) for g in range(0,360,45)]
print arg_max(valores,sin)

Como se pode verificar usamos o facto de podermos passar como argumento uma função!

E se quisermos calcular o mínimo? Bem, o mínimo é o simétrico do máximo, certo? Então:

def arg_min(dom,func):
"""Calcula o mínimo da função."""
return arg_max(dom, lambda x: -func(x))


Mas porque é que não podemos fazer simplesmente:

def arg_min(dom,func):
"""Calcula o mínimo da função."""
return arg_max(dom, -func)

A resposta: assim não estaríamos a passar a referência para uma função mas a tentar fazer uma operação (unária neste caso) com um nome que não o permite. As funções lambda são também chamadas funções anónimas. Apenas existem durante o seu uso. Têm uma sintaxe simples:

lambda parâmetros : expressão

Os parâmetros podem existir ou não. Na expressão não podemos ter ciclos, nem return.Exemplos:

>>> func = lambda n : n + 3
>>> func(5)
8
>>> area_tri = lambda b,a: 0.5 * b * a
>>> area_tri(4,3)
6.0
>>> primo = lambda x: x[0]
>>> resto = lambda x: x[1:]
>>> lista = [1,2,3,4,5]
>>> primo(lista)
1
>>> resto(lista)
[2, 3, 4, 5]
>>> junta = lambda e,l: [e] + l
>>> junta(6,lista)
[6, 1, 2, 3, 4, 5]
>>> junta(primo(lista),resto(lista))
[1, 2, 3, 4, 5]
>>>

sábado, 2 de janeiro de 2010

A forma da tartaruga

Conhecemos as formas que uma tartaruga pode assumir. Bem, se não soubermos basta fazer um teste simples:




Com o método getshapes ficamos a saber as formas conhecidas para a tartaruga, e como método shape podemos definir o valor pretendido.


Mas também podemos criar novas formas e adicioná-las sem grande dificuldade. Para isso basta criar uma forma como se mostra no código abaixo.

from cTurtle import *

def add_ponteiro(tarta):
tarta.setheading(90)
tarta.polystart()
tarta.fd(100)
tarta.lt(90)
tarta.fd(20)
tarta.rt(120)
tarta.fd(40)
tarta.rt(120)
tarta.fd(40)
tarta.rt(120)
tarta.fd(20)
tarta.polyend()
ponteiro = tarta.getpoly()
tarta.addshape('meu_ponteiro',ponteiro)

if __name__ == '__main__':
tarta = Turtle()
add_ponteiro(tarta)
tarta.clear()
tarta.setheading(90)
tarta.shape('meu_ponteiro')
tarta.fillcolor('blue')
tarta.exitOnClick()

O método consiste na criação de um objecto definido entre polystart e polyend. Depois vamos buscá-lo (getpoly) e associá-lo a um nome (ponteiro). Finalmente adicionamos a nova forma (addshape), dando-lhe um nome (‘meu_ponteiro’). Depois é só usar:




Mas será possível manipular esta forma, como se de um objecto normal se tratasse? A resposta é: sim! Vejamos um exemplo. Aqui vamos usar o método onTimer. Tem dois argumentos: o primeiro, é o nome de uma função, o segundo, é um temporizador em milisegundos. Significa que a função vai ser activada de tantos em tantos milisegundos. No nosso exemplo a função a única coisa que faz é mudar a orientação da tartaruga. Eis o código:


from cTurtle import *

def add_ponteiro(tarta):
tarta.setheading(90)
tarta.polystart()
tarta.fd(100)
tarta.lt(90)
tarta.fd(20)
tarta.rt(120)
tarta.fd(40)
tarta.rt(120)
tarta.fd(40)
tarta.rt(120)
tarta.fd(20)
tarta.polyend()
ponteiro = tarta.getpoly()
tarta.addshape('ponteiro',ponteiro)

def segundos():
tarta.setheading((tarta.heading() - 6) % 360)
tarta.onTimer(segundos,500)

if __name__ == '__main__':
tarta = Turtle()
add_ponteiro(tarta)
tarta.clear()
tarta.setheading(90)
tarta.shape('ponteiro')
tarta.fillcolor('blue')
segundos()
tarta.exitOnClick()

O resultado pode ser visto neste vídeo caseiro, com um final à Charlot! E porque não fazer um relógio digital com base nesta ideia??



Fazer algo com pouco...

Suponhamos que temos uma definição que nos permite desenhar um segmento de recta:

from cTurtle import *

def segmento(tartaruga,pos1,pos2):
""" traça um segmento entre pos 1 e pos2."""
tartaruga.pu()
tartaruga.goto(pos1)
tartaruga.pd()
tartaruga.goto(pos2)

Executando o código:

if __name__ == '__main__':
tarta = Turtle()
tarta.setheading(90)
inicio = (0,0)
fim = (100,100)
segmento(tarta, inicio,fim)
tarta.exitOnClick()

obtemos a imagem:





Note-se como a orientação da caneta, que foi colocada a 90 graus, se manteve. Podemos aliás ver todas as propriedades da caneta socorrendo-nos do comando pen():


>>> print tarta.pen()
{'pensize': 1, 'shown': True, 'resizemode': 'auto', 'fillcolor': '', 'stretchfactor': 1, 'speed': 3,
'pendown': True, 'pencolor': 'black', 'outline': 1}
>>>

Como se pode ver as propriedades da caneta estão guardadas num dicionário ... e podem ser alteradas. Por exemplo, fazendo:

tarta.pen(pencolor='red',pensize = 5, outline = 3)
obtemos a imagem:





Com a capacidade de desenhar segmentos podemos obter formas. Por exemplo, um triângulo:

def tri(tartaruga,p1,p2,p3):
segmento(tartaruga,p1,p2)
segmento(tartaruga,p2,p3)
segmento(tartaruga,p3,p1)






E se quisermos um quadrado, ou um pentágono? Bom, uma solução é usar uma definição que envolve a lista dos pontos e através de um ciclo for ir juntado de modo ordenado os pontos dois a dois. Podemos também usar a notação *objecto. Deste modo podemos também passar como argumentos um número variável de pontos:

def formas(tartaruga, *pontos):
for i in range(len(pontos)-1):
segmento(tartaruga, pontos[i], pontos[i+1])
segmento(tartaruga,pontos[-1], pontos[0])

Para garantir que a figura é fechada temos que juntar no final o último ponto ao primeiro!

Podemos fazer muitas variações sobre este tema. Por exemplo, encher com cor:

def formas(tartaruga, cor, *pontos):
tarta.begin_fill()
tarta.fillcolor(cor)
for i in range(len(pontos)-1):
segmento(tartaruga, pontos[i], pontos[i+1])
segmento(tartaruga,pontos[-1], pontos[0])
tarta.end_fill()





Agora é só puxar pela imaginação para fazer mais coisas.