sábado, 10 de dezembro de 2011

Problema Complementar 1.4

Um dos problemas complementares que vos foi proposto consistia em usar o módulo turtle para fazer um gráfico de barras. Vamos ilustrar como podemos resolver o problema.

O problema vai ser dividido em dois: primeiro desenvolvemos um programa para desenhar uma barra, a partir de uma dada posição e com um certo tamanho. Podemos também controlar a cor e a espessura da barra; depois, usamos este programa como auxiliar a ser usado pelo programa que desenha todas as barras contidas numa lista.

Vamos então ao primeiro sub-problema.

Também este pode ser dividido em três faes: posiciona a tartaruga; define os parâmetros (cor, espessura, etc.) e desenha.


def barra(pos,tamanho, cor,width):
""" Desenha uma barra na posição pos de altura tamanho."""
# posiciona
turtle.up()
turtle.goto(pos)
turtle.setheading(90)
# parâmetros
turtle.colormode(255)
turtle.pencolor(cor)
turtle.width(width)
# desenha
turtle.down()
turtle.forward(tamanho)
turtle.hideturtle()

Simples não é?

Agora o mais complicado. Ou será que não é assim tão difícil? Se analisar o código abaixo verá que não é assim tão complexo. Temos que inicialmente desenhar o eixo horizontal e defginir os parâmetros para esse eixo (cor, espessura,...) e desenhar o eixo. Como a tartaruga fica fora do local inicial temos a seguir de a reposicionar e preparar para desenhar. Finalmente um ciclo que se serve da nossa função auxiliar para desenhar de modo iterativo cada uma das barras.



def plot_barras(lista):
# Define o eixo dos Xs
numero = len(lista)
largura = 10
tamanho_recta = numero * largura
# Define parâmetros do eixo dos Xs
turtle.width(5)
turtle.pencolor('blue')
turtle.setheading(0)
turtle.goto((-tamanho_recta/2, 0))
# desenha eixo
turtle.down()
turtle.forward(tamanho_recta)
# reposiciona a tartaruga
turtle.up()
turtle.goto((-tamanho_recta/2,0))
turtle.hideturtle()
# Prepara para desenhar
turtle.down()
turtle.setheading(90)
pos = (-tamanho_recta/2) + largura/2
# desenha as barras
for indice in range(numero):
barra((pos,0), lista[indice], 'red',5)
pos = pos + largura

Falta testar e ver o efeito.

if __name__ == '__main__':
plot_barras([50,30,20,40,32,7,5,77,67])
turtle.exitonclick()




Moral da história: quando um problema aparenta ser complexo o melhor é pensar primeiro como o podemos dividir em sub-problemas mais simples!

sexta-feira, 25 de novembro de 2011

Teste 3 ( Turma 8)

Pergunta 1

Espaço de nomes é o local da memória onde ficam os nomes que foram associados a objectos através de instruções de atribuição existentes no código. Espaço dos objectos é o local onde se encontra a descrição dos objectos, nomeadamente o seu valor e o seu tipo. A identidade é um atributo dos objectos e indica o local da memória onde este se encontra. A relação entre ambos é feita através da identidade do objecto que fica a ser conhecida do nome.

Pergunta 2

Os erros eram dois. Primeiro, a variável res não era inicializada. Em segundo, o range devia ter como argumentos 1 e len(x)+1.

Eis o programa correcto.

def inverte(x):
res = [] # <-- retira = problema
for i in range(1,len(x)+1): # <-- range(len(x)) = problema
res.append(x[-i])
return res

Olhando para as operações sobre o objecto, nomeadamente o método append temos que ter uma lista para o resultado, embora para x possa ser qualquer objecto em que a operação de indexação exista: sequências (cadeias de caracteres, tuplos, listas) e dicionários.

Pergunta 3

Esta pergunta é parecida com várias que foram feitas com as árvores genealógicas, mais concretamente a que pedia a implementação da função avo.

def caminho_2(grafo, x, y):
suc_x = grafo[x]
for vert in suc_x:
if y in grafo.get(vert):
return True
return False

Basicamente, vamos buscar a lista dos vértices adjacentes do primeiro vértice (x). Depois procuramos saber se algum deles tem como vértice adjacente o segundo (y).

That’s it!

Teste 3 (Turma 7)

O novo teste colocava questões de natureza teórica, teórico-prática e prática. Muitos aluno@s continuam com problemas sérios nestes diversos aspectos. Como tenho afirmado várias vezes, só há uma maneira de aprender a programar que é programar. Mesmo a errar se aprende. Mas não é durante os testes que se aprende... Dito isto vamos às perguntas.

Pergunta 1

O conceito de aliasing foi várias vezes referido nas aulas. Na sua essências significa a atribuição de dois ou mais nomes a um mesmo objecto. Um exemplo simples


>>> a = [1,2,3]
>>> b = a
>>> b
[1, 2, 3]
>>> id(a)
4412212648
>>> id(b)
4412212648
>>>

a e b são nomes para o mesmo objecto. Um problema colocado por este mecanismo é a possibilidade, quando os objectos são mutáveis, ao alterarmos um objecto através de um dos seus nomes, todos os outros acompanham a mudança.

>>> a[1] = 4
>>> a
[1, 4, 3]
>>> b
[1, 4, 3]
>>>


Pergunta 2

Eis o programa errado.

def retira_dup(x):
for i in range(len(x)):
if x[i] in x[i+1:]:
x[i+1:].remove(x[i])
return x


Comecemos pelo tipo de objecto. Tem que ser uma sequência, devido à operação de fatiamento. De entre as sequências possíveis (lista de caracteres, tuplos, listas) terá que ser um lista pois trata-se do método remove.

Agora os erros. A ideia do programa é a verificar se o elemento na posiçãoi existe na parte da lista a partir de i+1. Se existe, então estamos perante uma repetição, pelo que precisamos retirar todas as suas ocorrências. Mas o método remove só tira uma. Logo aqui temos um problema. Por outro lado, ao retirar elementos estamos a alterar a lista que fica mais pequena e pode acontecer que por isso isso tome valores fora da lista e isso vai gerar um erro. Uma forma de concertar as coisas é dada no programa abaixo.

def retira_dup_good(x):
res = []
for i in range(len(x)):
if x[i] not in x[i+1:]:
res.append(x[i])
return res


Usamos a ideia ciclo-contador-acumulador.

Problema 3

A questão colocada pode ser resolvida procurando saber se é possível fazer a ligação entre todos os vértices do grafo que aparecem no caminho. Mal falhe um caso, devolve-se falso.

def percurso(grafo, caminho):
for ind in range(len(caminho)-1):
if caminho[ind+1] not in grafo.get(caminho[ind]):
return False
return True


Como se pode ver o problema resolve-se com as operações e métodos mais comuns dos dicionários. Caso não tenha conseguido resolver na aula, estude bem esta solução.

sexta-feira, 18 de novembro de 2011

Problemas 6.16 a 6.19

As árvores genealógicas são um bom pretexto para aprendermos algo sobre dicionários e sobre programação. No guião 6 aparece um conjunto de pequenos problemas que permitem satisfazer estes dois objectivos. Ficamos a saber alguns aspectos fundamentais sobre os dicionários:

- como das chaves chegamos aos valores
- como dos valores encontramos as chaves
- as consequências de os dicionários não serem ordenados

Do mesmo modo somos confrontados com a possibilidade de reutilizar código, o que nos leva ao conceito de programação por abstracções. Podemos chegar a estes resultados sejam por um processo da base para o topo (como quem brinca com um Lego), seja do topo para a base. Esta última observação é importante, porque nos dá um princípio fundamental da programação: construir soluções por etapas sucessivas, decompondo o problema inicial em sub-problemas mais simples, cuja solução vai sendo obtida por refinamento da solução corrente. Dizemos que usamos camadas de abstracção.

Mas comecemos as nossas soluções definindo os dois tijolos básicos: (1) a partir de um nome obtemos os seus descendentes e (2) a partir de um descendente obtemos o seu progenitor.

def filhos(dicio,progenitor):
""" Lista dos filhos/descendentes."""
return dicio.get(progenitor,[])

def progenitor(dic, nome):
""" Devolve o progenitor do nome."""
for chave, valor in dic.items():
if nome in valor:
return chave
return []

Note-se como decidimos que na ausência de resposta (um pai sem filhos, alguém que não consta na árvore genealógica) o resultado a devolver é a lista vazia. Esta decisão tem consequências. Veja-se também que é mais natural resolver problemas em que a procura vai no sentido chave --> valor. Finalmente, atente-se no modo como percorremos o dicionário através dos seus elementos. Não havendo ordem não podemos aspirar a procura por aquilo que não existe: índices.

A partir destes elementos os outros problemas têm soluções fáceis.

Irmãos = filhos do mesmo progenitor

def irmaos(dic,nome1,nome2):
""" Têm o mesmo progenitor?"""
prog1 = progenitor(dic, nome1)
prog2 = progenitor (dic,nome2)
return prog1 == prog2

O que acontece se ambos os nomes não constarem no dicionário? Bem, vamos ter listas vazias a serem devolvidas... e o resultado será True, ou seja, são irmãos. Isto resulta do modo como implementámos a função progenitor. Pode ser corrigido:
def irmaos(dic,nome1,nome2):
""" Têm o mesmo progenitor?"""
prog1 = progenitor(dic, nome1)
prog2 = progenitor (dic,nome2)
if prog1 and prog2:
return prog1 == prog2
else:
return False


Netos = filhos dos filhos
def netos(dicio,progenitor):
""" Lista netos."""
net = []
for elem in filhos(dicio,progenitor):
net.extend(filhos(dicio,elem))
return net


Veja-se a necessidade de usar o método extend. É comum o erro de usar antes append.

Avo = progenitor do progenitor
def avo(dic,nome):
""" Quem é o/a avô/avó do nome."""
return progenitor(dic,progenitor(dic,nome))

Mais palavras para quê? Talvez para dizer de novo que perante um problema, a primeira coisa a fazer é ... pensar no problema, e definir uma estratégia para o resolver. Sem ter problemas em pensar assim: se eu tivesse esta funcionalidade então era fácil. Implementar uma solução provisória que depende da existência dessa funcionalidade. Pesquisar para ver se a linguagem a oferece. Em caso de resposta negativa, criar as funções auxiliares que a permitam implementar.

sexta-feira, 11 de novembro de 2011

Que sei eu?

Programar é uma arte. Mas também depende de regras de desenho muito precisas. E saber o que existe como funcionalidades e construções da linguagem que estamos a usar. Mas sendo uma arte o mais importante, mais importante ainda do que saber, é compreender. Essa sabedoria só se alcança com a experiência, isto é programando muito, cometendo erros e explorando novos caminhos.

Dito isto, este post tem mais que ver com o saber, pois saber é um pré-requisito a compreender. Temos introduzido construções da linguagem, que mos permitiram resolver problemas. Mas também falámos de conceitos. É bom que, de tempos a tempos, nos questionemos: que sei eu?

Algumas das perguntas, básicas, para testar o nosso conhecimento:

1- Que tipos de objectos conheço? O que os distingue? Para que serve cada um deles?
2- Quais as três características comuns a todos os objectos? Para que serve cada uma delas?
3- O que são objectos imutáveis? E mutáveis? Que consequências, cuidados a ter, com cada um destes tipos de objectos?
4- O que é o Espaço de Nomes? E o Espaço dos Objectos? Como é que (e quais) as entidades vão parar a cada um destes espaços?
5- O que são definições?
6- Qual a diferença entre definir e usar uma definição?
7- Qual a diferença entre função e método?
8- O que distingue uma definição com return de uma sem return?
9- Como são comunicados os dados a, e obtidos os resultados de, uma definição?



Não se esqueça: estas são apenas algumas perguntas para testar o nosso conhecimento. Há muitas mais...

quarta-feira, 9 de novembro de 2011

Erros Comuns (6): copy vs deepcopy: aliasing e mutabilidade

Está na altura de introduzir um erro comum, mas cuja origem é mais subtil.
Sabemos que um mesmo objecto pode ter associado diferentes nomes. Quando os objectos são imutáveis isso não coloca problemas. No exemplo abaixo o objecto 'abc' tem associado três nomes diferentes. Se alteramos algum deles o que acontece é que é criado um novo objecto que fica associado ao nome do objecto antigo. Lembre-se: as cadeias de caracteres são imutáveis!



>>> cadeia_1 = 'abc'
>>> cadeia_2 = 'abc'
>>> cadeia_3 = cadeia_1
>>> id(cadeia_1)
4357502896
>>> id(cadeia_2)
4357502896
>>> id(cadeia_3)
4357502896
>>> cadeia_2 = cadeia_2 + 'd'
>>> cadeia_3 = cadeia_3 + '3'
>>> cadeia_2
'abcd'
>>> cadeia_3
'abc3'
>>> cadeia_1
'abc'
>>> id(cadeia_2)
4361446864
>>> id(cadeia_3)
4361446720
>>> id(cadeia_1)
4357502896


Quando usamos objectos mutáveis o caso muda de figura.


>>> lista_1 = [1,2,3]
>>> lista_2 = [1,2,3]
>>> lista_3 = lista_2
>>> lista_1
[1, 2, 3]
>>> lista_2
[1, 2, 3]
>>> lista_3
[1, 2, 3]
>>> id(lista_1)
4515141032
>>> id(lista_2)
4515145344
>>> id(lista_3)
4515145344
>>> lista_1[1] = 'b'
>>> lista_1
[1, 'b', 3]
>>> lista_2
[1, 2, 3]
>>> lista_3
[1, 2, 3]
>>> lista_2[1] = 'X'
>>> lista_1
[1, 'b', 3]
>>> lista_2
[1, 'X', 3]
>>> lista_3
[1, 'X', 3]
>>>


Para melhor entender o que acontece vejamos a figura seguinte:




Como se pode ver a lista_2 e a lista_3 partilham a memória, e é isso que faz com que as modificações que afectam uma também afectem a outra. Um modo de resolver o problema consiste em fazer uma cópia do objecto em vez de partilharem a memória.


>>> lista_1 = [1,2,3]
>>> lista_2 = [1,2,3]
>>> lista_3 = lista_2[:] # <-- cópia!
>>> id(lista_1)
4515141536
>>> id(lista_2)
4515105464
>>> id(lista_3)
4515140816
>>> lista_3[1] = 'Y'
>>> lista_3
[1, 'Y', 3]
>>> lista_2
[1, 2, 3]
>>>


A figura abaixo ilustra a situação.





Parece que podemos ficar descansados com esta solução. Mas veja-se uma nova situação.


>>> lista_2 = [1,[2],3]
>>> lista_3 = lista_2[:] # <-- cópia?
>>> id(lista_2)
4515346624
>>> id(lista_3)
4515144696
>>> lista_3[1][0] = 'Z'
>>> lista_3
[1, ['Z'], 3]
>>> lista_2
[1, ['Z'], 3] # oops!
>>>


O que terá acontecido? Para entender vamos recorrer uma vez mais ao nosso diagrama de ligação dos nomes aos objectos. Como fica situação antes da alteração?





E depois da alteração da lista_3?






O método indicado de cópia apenas faz uma cópia de superfície, isto é, uma cópia ao primeiro nível. A única solução efectiva para esta questão é recorrer à função deepcopy do módulo copy.


>>> import copy
>>> lista_2 = [1,[2],3]
>>> lista_3 = copy.deepcopy(lista_2) #<-- cópia profunda!
>>> id(lista_2)
4515145344
>>> id(lista_3)
4515140816
>>> lista_3[1][0] = 'K'
>>> lista_3
[1, ['K'], 3]
>>> lista_2
[1, [2], 3] # <-- Sem problemas!
>>>


Vejamos graficamente o que acontece.





Agora nada é partilhado, a não ser os objectos primitivos (imutáveis). Por isso ao alteramos a ligação (profunda) na lista_3 nada se altera na lista_2.

Erros Comuns (5): return e print

Este tema já foi tratado em anterior post. Mas aqui vai na mesma de modo mais simplificado.

Os dois modos de comunicar resultados mais comuns, recorrem seja à instrução return, seja à instrução print. Só que elas têm comportamentos diferentes. Enquanto que a execução da instrução return faz terminar de imediato a execução do programa, a instrução print limita-se a imprimir o resultado. É um erro frequente colocar print pensando que programa termina.


# print em vez de return
def primo_num(num):
"""Verifica se o número é primo."""
for i in range(2,num/2 + 1):
if (num % i) == 0:
print False
print True


Há um outro aspecto que não nos podemos esquecer. Quando uma definição não tem nenhum return ela devolve na mesma um objecto: None. Esse objecto denota a ausência de valor, mas pode causar muitos “estragos” num programa, como já mostrámos em post anterior.

Erros Comuns (4)

Trata-se de um erro clássico. O operador de divisão está sobrecarregado, pelo que faz divisão inteira ou em vírgula flutuante em função dos argumentos. Muitas vezes queremos a divisão em vírgula flutuante, mas não podemos garantir no momento da divisão, que pelo menos um dos números não seja inteiro. Uma solução passa por forçar um dos números a passar a vírgula flutuante antes da divisão.


>>> n = 1
>>> m = 3
>>> n / m
0
>>> float(n) / m
0.33333333333333331
>>>


Em Python 3 este problema foi resolvido. Passamos a ter dois operadores para a divisão: /, divisão entre números em vírgula flutuante, // para a divisão inteira.

domingo, 6 de novembro de 2011

Erros Vivos (2)

Consideremos a listagem seguinte:


>>> def toto(n):
... print n + 1
...
>>> def titi(n):
... return n + 1
...
>>> toto(4)
5
>>> titi(4)
5
>>> print toto(4)
?
>>> print titi(4)
?
>>> toto(4) + 1
?
>>> titi(4) + 1
?


O que vai parecer no lugar dos pontos de interrogação? Esta era uma das perguntas de um dos testes. O que estava em jogo essencialmente era saber em que medida se compreendia a diferença entre existir ou não a instrução de return e a diferença para um print. Pedia-se também a justificação e isso era muito importante para a aceitar a resposta.

Vejamos dois exemplos de resposta. São apenas indicativos dos problemas evidenciados por um número muito grande de alunos.

Resposta 1

Na linha 12 (primeiro ponto de interrogação) o resultado será 5 e na linha 14 (segundo ponto de interrogação) também será 5, a diferença dos dois é que na função toto, os valores serão retornados por um print e na função titi serão retornadas por um return. Na linha 16 (terceiro ponto de interrogação) irá dar um ewrro, pois não se pode somar +1 a uma função e na linha 18 (quarto ponto de interrogação) como a função retorna um valor pela instrução return o resultado será 6.

Resposta 2

Linha 12:
aparecerá
>>> 5
>>> 5
Porque há duas instruções de print, uma dentro da função e outra de fora dela.

Linha 14:
Aparecerá
>>> 5
Porque o resultado apenas é imprimido uma vez.

Linha 16:
Aparecerá
>>> 6
Porque mais uma vez a impressão é feita apenas uma vez.

Linha 18:
Aparecerá
>> None
Porque o resultado não foi mandado imprimir.

Vamos então ver os problemas. Comecemos por um facto: trata-se de uma sessão no interpretador. Assim sendo, todas as expressões que colocarmos estão sujeitas a um processo que se desdobra em três fases: leitura da expressão, cálculo do seu valor e impressão do resultado. Trata-se do ciclo READ-EVAL-PRINT já referido nas aulas. Repito: aplica-se a todas as expressões. E o que é uma expressão? Pode ser um objecto (com valor), um nome associado a um objecto, ou uma função/método aplicada/o aos seus argumentos. Vejamos um exemplo.


>>> a = 5
>>> 7
7
>>> a
5
>>> a + 5
10
>>> import math
>>> math.sin(3)
0.1411200080598672
>>> math.sin(a)
-0.9589242746631385
>>> math.sin(math.sin(a))
-0.8185741444617193
>>>

Quando damos um objecto (7) o respectivo valor é ecoado; quando introduzimos o nome a é calculado o valor do objecto associado e este é ecoado. Quando introduzimos uma expressão mais complexa ela é avaliada e o valor do objecto resultado é ecoado. Podemos ter expressões mais bizarras como a que envolve calcular o seno do seno de um número.

O leitor atento perguntará: mas porque é que no primeiro caso da listagem (a = 5), nada é ecoado? A razão é simples: porque não é uma expressão mas antes uma instrução. É por essa mesma razão que quando definimos no interpretador uma função, nada é ecoado. Estamos apenas a definir usando a instrução composta def. Mas quando usamos uma definição, ela tem a natureza de uma expressão e por isso o valor que devolve vai ser ecoado!

>>> def dobro(n): # <-- Definição
... return 2*n
...
>>> dobro(3) # <-- Uso
6
>>> dobro(dobro(2)) # <-- Uso duas vezes
8
>>> dobro(4) + 1 <-- Uso
9
>>>

Mas como sabemos qual é o valor que a chamada (o uso) de uma função devolve? Simples! É o valor da expressão associada ao primeiro return encontrado durante a execução da função. Mas, e se a função não tiver nenhum return ??? Já sabemos a resposta: devolve na mesma um objecto chamado None! Mas devemos ter em atenção que None denota a ausência de valor!!

>>> None
>>> print None
None
>>>

Daí que None não seja ecoado pelo interpretador mas seja impresso por print.

Percebido isto, já estamos em condições de responder com correcção à pergunta. Ou talvez não... Ora vamos lá a ver outra situação.


>>> a = 5
>>> a
5
>>> print a
5
>>>

Mas última situação retratada, não deveria aparecer duas vezes o valor 5? Uma pelo print e a outra porque o interpretador depois de ler, avaliar imprime o resultado? Não, não devia, porque print a não é uma expressão! Logo só há a impressão devido ao print. Então e se a instrução de print aparecer no interior de uma definição. O que é que acontece? Olhemos para a primeira situação da pergunta:

>>> def toto(n):
... print n + 1
...
>>> toto(4)
5
>>>

Expliquemos de novo: definir a função toto não faz ecoar nada. Mas quando usamos a função, chamando com o argumento 4 (toto(4)) porque aparece 5? Apenas 5! Afinal toto(4) é uma expressão. Sim, é verdade, mas na ausência de um return o objecto devolvido ao interpretador é None que, como dissemos denota a ausência de valor. Então o 5 que aparece resulta da instrução de print dentro da definição. UIff! Agora é que está tudo certo.

Regressemos então a sessão da pergunta.

>>> def toto(n):
... print n + 1
...
>>> def titi(n):
... return n + 1
...
>>> toto(4)
5
>>> titi(4)
5
>>> print toto(4)
?
>>> print titi(4)
?
>>> toto(4) + 1
?
>>> titi(4) + 1
?

As duas definições não ecoam nada. As duas chamadas seguintes fazem aparecer 5. Mas a primeira ocorrência de 5 é devida ao print , enquanto que a segunda resulta do interpretador imprimir o objecto devolvido pelo return da definição. Agora o primeiro ponto de interrogação. Vai aparecer:


>>> print toto(4)
5
None


O 5 resulta da instrução de print, enquanto que o None é ecoado pelo interpretador e resulta do facto de não havendo return esse é o objecto devolvido e que print tem que imprimir.

Terceiro ponto de interrogação:

>>> print titi(4)
5

Aqui é fácil: a função devolve 5 (devido ao return), que é ecoado pelo interpretador. Já sabemos que o print nem era preciso.

Terceiro ponto de interrogação.

>>> toto(4) + 1
5
Traceback (most recent call last):
File "", line 1, in
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
>>>

O 5 vem da instrução de print. O erro deriva do facto de que, na ausência de return, o valor devolvido é None, e este objecto não pode ser somado com um inteiro. Logo dá erro.

Quarto ponto de interrogação.

>>> titi(4) + 1
6

Aqui tudo é mais “normal”: a função devolve 5, este valor é somado a 1, e este é o valor da expressão que o interpretador ecoa.

That’s it!

sábado, 5 de novembro de 2011

Erros ao vivo (1)

Uma das perguntas que os alunos da segunda fase tiveram que responder, para o seu teste 1, era a seguinte:

O vencimento bruto de um trabalhador está sujeito a descontos: 25% para o IRS, 5% para a Segurança social e 10% para a Caixa Nacional de Aposentações. O vencimento líquido é o que resulta da subtracção destes descontos ao vencimento bruto. Desenvolva um programa que dado o vencimento bruto devolve o correspondente vencimento líquido.

Eis algumas das soluções que me apresentaram.

Exemplo 1


Def. vencimento(vbruto).
virs = (vbruto) * 0,25
vss = (vbruto) * 0,05
vcna = (vbruto) * 0,1
vliquido = (virs) + (vss) + (vcna)
return vliquido

if ‘__main__’ == __name__:
print vencimento()

Exemplo 2

def venc_liquido(n):
total = 0
irs = (n * 25) / 100
sc = (n * 5) /100
cna = (n * 10) / 100
total = n - (irs + sc + cna)
return total

if __name__ == ‘__main__’:
print venc_liquido()

Exemplo 3

def vencimento(bruto):
liquido = bruto - (bruto * 0.25 + bruto * 0.05 + bruto * 0.1)
return liquido

if __name__ == ‘__main__’:
print vencimento(bruto)

O que todas estas soluções têm em comum é ... terem problemas. Há erros de sintaxe, há erros de lógica e há erros conceptuais graves. O primeiro exemplo é o pior de todos. Ttodos aqueles parênteses à volta dos nomes são desnecessários (interrogo-me se não os colocou levado pelo facto de quando se define a função termos que usar parênteses.). Nos números em vírgula flutuante usa a vírgula e não o ponto como separador entre a parte inteira e a parte fraccionária. Tem um erro de lógica: não subtrai ao vencimento bruto os descontos. No segundo exemplo, a lógica está correcta, só não se entendendo o porquê da inicialização de total a zero. Não é necessário. O terceiro exemplo, é o que está mais limpo. Nada a dizer sobre o modo como a definição é construída. Mas todos têm um erro conceptual grave, e isso manifesta-se no modo como usam a função que definiram, isto é, o que colocam depois do if. Nos dois primeiros casos a função é chamada sem argumento, enquanto que no terceiro caso a função é chamada tendo como argumento um nome que não está associado a nenhum objecto. O primeiro caso tem paralelo na situação de eu carregar numa tecla de função da minha calculadora, por exemplo sin, e não fazer mais nada ficando à espera que a máquina calcule qualquer coisa (embora não se saiba o quê).

Vamos ver se nos entendemos. Os problemas resolvem-se escrevendo programas. O programa é feito de definições e estas, para serem úteis, têm que ser usadas. Usar uma definição traduz-se por chamar a definição, indicando na altura da chamada quais os argumentos concretos a usar. Os argumentos das definições chamam-se parâmetros formais, e têm que ser nomes; os argumentos que usamos nas chamadas chamam-se parâmetros reais e têm que traduzir, directa ou indirectamente um objecto. Directamente se usamos o valor do objecto, indirectamente se usamos ou umaexpressão que quando calculada dá como resultado um objecto, ou um nome que está associado a um objecto. Os parâmetros formais, os que usamos nas definições, podem ter um nome qualquer, são mudos. Por exemplo, as duas definições abaixo definem exactamente a mesma coisa.

def dobro(n): # <-- n parâmetro formal
return 2 * n

def dobro(x): # <-- x parâmetro formal
return 2 * x

Usar n ou x como parâmetro formal é irrelevante. Vejamos agora com um exemplo a questão da definição e da chamada.


>>> def dobro(n): # <-- x parâmetro formal
... return 2 * n
...
>>> dobro(5) # <-- 5 parâmetro real
10
>>> a = 3
>>> dobro(a) # <-- a parâmetro real
6
>>> dobro(a * 3 + 5) # <-- a*3+5 parâmetro real
28
>>>

No primeiro caso, chamamos com um objecto (5), no segundo, com o nome (a) associado a um objecto (3), no terceiro com uma expressão associada a um objecto de valor 14.

O que é que acontece se chamamos, ou sem argumento ou usando como argumento real um nome que não está associado a nenhum objecto? Um erro, claro:

>>> dobro() # <-- falta parâmetro real
Traceback (most recent call last):
File "", line 1, in
TypeError: dobro() takes exactly 1 argument (0 given)
>>> dobro(z) # <-- parâmtero real sem objecto associado
Traceback (most recent call last):
File "", line 1, in
NameError: name 'z' is not defined
>>>


Mas se disse que o nome usado como parâmetro formal pode ser qualquer o que acontece no caso de parâmetro formal (ou da definição) e parâmetro real (o da chamada) forem o mesmo nome?

>>> def dobro(n):
... return 2 * n
...
>>> n = 6
>>> dobro(n)
12
>>> n
6

Como se pode ver, não acontece nada de anormal neste caso. Repito: o nome do parâmetro formal é arbitrário e só existe na realidade durante a execução da definição de que faz parte. Nessa altura é feita a associação do nome do parâmetro formal ao objecto passado como parâmetro real. No caso de o parâmetro real ser também um nome então o que é comunicado ao parâmetro formal é a referência (a identidade) do objecto a que está associado. Se o objecto correspondente ao parâmetro real for imutável nada de especial acontece, a não ser a definição ser executada e o resultado ser devolvido. Se o parâmetro real corresponder a um objecto mutável o caso muda de figura, pois as alterações ao parâmetro formal podem ser transferidas para o objecto associado ao parâmetro real. E nem é preciso os nomes dos parâmetros formal e real serem o mesmo!

>>> def aumenta(lista,elem):
... lista.append(elem)
... return lista
...
>>> aumenta(['a',2,'c'],4)
['a', 2, 'c', 4]
>>> minha_lista = [1,2,3]
>>> aumenta(minha_lista,4)
[1, 2, 3, 4] 3 # <-- lista alterada dentro do programa
>>> minha_lista
[1, 2, 3, 4] 3 <-- minha_lista alterada como resultado da alteração a lista
>>> lista = ['a','b','c']
>>> aumenta(lista,'d')
['a', 'b', 'c', 'd']
>>> lista
['a', 'b', 'c', 'd']
>>>

Para se certificar se entendeu veja se não tem dúvidas neste último exemplo:

>>> minha_lista = [1,2,3,4]
>>> aumenta(minha_lista + [5], 6)
[1, 2, 3, 4, 5, 6]
>>> minha_lista
[1, 2, 3, 4]
>>>


Enquanto não entender isto terá sempre dificuldades no futuro. Por isso batalhe até entender. Se as dúvidas se mantiverem, fale com o seu professor!

sexta-feira, 4 de novembro de 2011

Devagar se vai ao longe: polinómios

Nos testes recentes (teste 2) apareceram algumas questões relacionadas com polinómios. Uma delas pedia para calcular o valor do polinómio num ponto e, outra, para calcular a soma de dois polinómios.

Muitos alunos ficaram bloqueados com a questão de saber como é que se representava um polinómio. E acabaram por não responder à pergunta. O que a seguir se segue pretende mostrar como é possível responder parcialmente à questão mesmo sem saber como o polinómio seria representado.

Valor de um polinómio num ponto

Já conhecem, espero bem, a lenga lenga do costume: dado um problema, entendi o enunciado? consigo identificar os dados? consigo identificar o resultado? Creio que não terão problema em aceitar que da resposta a estas questões resulta um modelo de programa:


def valor_poli(polinomio,x):
"""Calcula o valor de um polinómio de grau n num ponto."""
pass
return valor

Agora é preciso pensar na solução. Para me facilitar a vida, vou supor um exemplo concreto:



Suponhamos ainda que quero saber o valor do polinómio no ponto x = 6. Então o que tenho que fazer é substituir na expressão do polinómio x por 6 e efectuar as contas:



Temos por isso que calcular uma soma de produtos. Por analogia, sabemos que a solução é simples, bastando usar o nosso velho conhecido padrão de ciclo-contador-acumulador. Quantas vezes vamos repetir o ciclo? Numa abordagem simplista, diremos tantas vezes quantas o grau do polinómio. Já agora: o grau de um polinómio é a maior potência de x cujo coeficiente é diferente de zero. No exemplo acima será por isso 2. Mas sem saber mais nada podemos imaginar a seguinte solução.



def valor_poli(polinomio,x):
"""Calcula o valor de um polinómio de grau n num ponto."""
g = grau(polinomio)
valor = 0 # <-- Acumulador
for i in range(g + 1): # <-- i é o contador implícito
parcela = coeficiente(polinomio, i) * (x ** i)
valor = valor + parcela
return valor

Se as funções grau e coeficiente fizerem o que o seu nome promete temos a questão resolvida. Quem fizesse até aqui já teria uma boa cotação na pergunta!

Mas agora precisamos falar na representação dos polinómios. Olhando para dois polinómios o mesmo grau, o que os distingue são os respectivos coeficientes. Por isso um polinómio pode ser representado apenas por estes. Mas como? Uma ideia simples é usar uma lista de tal modo que na posição i está o coeficiente de ordem i. Por exemplo, o caso anterior seria representado por:





E se alguns coeficientes forem zero como no caso de:



Usam-se zeros!










Baseados nesta representação vamos implementar o que nos falta.

def grau(polinomio):
"""Polinomio representado por uma lista de coeficientes, mesmo os negativos!."""
return len(polinomio) - 1

def coeficiente(polinomio,k):
"""Polinomio representado por uma lista de coeficientes, mesmo os negativos!."""
return polinomio[k]

Fácil, não acham?

Claro que seu tiver um polinómio na forma.



é um desperdício de espaço. Outra solução para a representação seria ter uma lista em que cada elemento é um par (expoente, coeficiente). O exemplo anterior daria:




E agora como calcular o grau, o coeficiente e o expoente? Mas para que queremos saber o grau? Afinal ele é dado no interior da nova representação, e só era preciso saber o seu valor para podermos determinar o número de vezes em que repetimos o ciclo. Mas agora esse número depende do comprimento da lista que representa o polinómio. Logo não precisamos dele:

def valor_poli_b(polinomio,x):
"""Calcula o valor de um polinómio de grau n num ponto."""
valor = 0 # <-- Acumulador
for i in range(len(polinomio)): # <-- i é o contador implícito
parcela = coeficiente(polinomio, i) * (x ** expoente(polinomio,i))
valor = valor + parcela
return valor


Questão (quase) resolvida. Notar que agora precisamos, como já referimos de saber o expoente associado a cada coeficiente. Para terminar (ou talvez não...):


def coeficiente(polinomio,k):
return polinomio[k][1]

def expoente(polinomio,k):
return polinomio[k][0]


Olhando para o código acima até podemos ficar orgulhosos. Mas, a mudança de representação, deve levar-nos a pensar se não podemos simplificar as coisas. E se o fizermos, chegaremos com naturalidade a outra solução, baseada em percorrer a lista pelo conteúdo e não pelos índices.

def valor_poli_c(polinomio,x):
"""Calcula o valor de um polinómio de grau n num ponto."""
valor = 0 # <-- Acumulador
for par in polinomio: # <-- i é o contador implícito
valor = valor + par[1] * (x ** par[0])
return valor

Afinal a mudança de representação levou-nos a rever a nossa solução inicial.

Depois disto, achamos que podemos deixar a soma de dois polinómios como exercício para o leitor. Mas fica a moral desta história: programar (às vezes) tem muito de arte, e só treinando nos aperfeiçoamos.

Ah, já agora. Se quiserem mesmo saber como calcular o grau do polinómio para esta segunda representação, aqui fica. até porque pode ser preciso para outro tipo de problema...

def grau(polinomio):
polinomio.sort()
return polinomio[-1][0]

terça-feira, 1 de novembro de 2011

Erros Comuns (3): confusão entre “=” e “==”

Em Python o sinal de = é usado como operador de atribuição, enquanto o teste para igualdade de valores, e que devolve um valor booleano, utiliza ==, isto é dois sinais de igual. Com frequência os programadores usam o primeiro no lugar do segundo.


x = 'abcdef'
for car in x:
if car = 'a':
...


Muitos editores, por exemplo o WingIDE, dão conta do erro e mostram uma linha quebrada a vermelho na zona do erro.

Erros Comuns (2): alinhamento do código

Em Python o alinhamento do código é uma marca fundamental da linguagem e consubstancia o conceito de bloco. Outras linguagem, como C/C++ ou Java usam chavetas. Tal facto dá muita liberdade ao programador para escrever o seu código, traduzindo-se muitas vezes em código pouco legível e, também por isso, difícil de corrigir e de manter. Em Python somos forçados a indicar a estrutura pelo alinhamento do código.



# return mal alinhado
def primo_num(num):
"""Verifica se o número é primo."""
for i in range(2,num/2 + 1):
if (num % i) == 0:
return False
return True # <-- Problema!


O exemplo acima mostra como ao colocar mal alinhada a instrução return, o programa termina após executar uma única vez o ciclo for.

Pode-se colocar a questão de se saber quando se inicia um bloco, e, por isso, o código tem que ser indentado. A resposta mais simples é: a seguir a um cabeçalho, isto é quando somos obrigados a colocar dois pontos : por razões sintáticas. Alguns exemplos.


for i in range(n):
<bloco>

def toto(n):
<bloco>

if a > x:
<bloco>
else:
<bloco>

Erros Comuns (1)

Inicio aqui uma série de post sobre erros frequentes em programação. Alguns têm que ver com a linguagem Python, outros com a programação em geral.

Programar é uma arte que se domina tanto mais quanto mais se exercitar. Mas enquanto o momento da sabedoria não chega, e pode demorar algum tempo, é natural que se cometam alguns erros de modo sistemático. Muitos resultam de interferências de outras linguagens, seja de programação, seja da linguagem matemática. Outros, são consequência de os métodos de resolução de problemas pelos humanos e pelo computador não serem necessariamente idênticos. Outros ainda, resultam de equívocos conceptuais. Identificar os erros mais frequentes dos programadores novatos é o objectivo primeiro deste texto. Existem erros silenciosos, aqueles em que o programa corre e termina sem problemas, e erros ruidosos, quando a execução normal do programa termina com uma mensagem de erro. Ao longo destes post falaremos de ambos, e da importância de saber interpretar a mensagem de erro. Aliás, se calhar esse é o primeiro erro dos programadores novatos: não olhar sequer para as, ou não saber que existem, mensagens de erro! Eis dois exemplos simples.


Python 2.7.1 (r271:86832, Jul 31 2011, 19:30:53)
[GCC 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2335.15.00)]
Type "help", "copyright", "credits" or "license" for more information.
>>> # silencioso
>>> x = 4
>>> for i in range(5):
... if x % 2 == 0:
... print i
...
0
1
2
3
4
>>> # ruidoso
>>> lista = range(3)
>>> lista
[0, 1, 2]
>>> lista[3]
Traceback (most recent call last):
File "", line 1, in
IndexError: list index out of range
>>>


Consegue perceber de que erro se trata em cada um dos casos?\\
Keep watching!

segunda-feira, 31 de outubro de 2011

Problema 5.6

Para resolver o problema de deslizar n posições para a esquerda ou para a direita vamos abordar a questão por etapas. Admitindo que sabemos deslizar isoladamente n elementos para a direita ou para a esquerda a solução gloabla é fácil de escrever.


def desliza_n_d_e(n, lista):
"""Desliza n posições para a direita ou esquerda,de modo circular, os elementos de uma lista."""
if n > 0:
return desliza_d_n(n,lista)
else:
return desliza_e_n(abs(n), lista)

Temos agora que resolver cada um dos dois sub-problemas quer introduzimos. Eles são muito semelhantes. Comecemos pelo deslizar para a direita. A silução consiste em dividir a lista em dois pedaços, em função do valor de n. Guardamos a parte final numa variável auxiliar e depois acrescentamos à sua frente a parte inicial da lista de que partimos. A figura ilustra a estratégia.




O programa tem apenas que ter o cuidado de testar se a lista é vazia ou não.

def desliza_d_n(n, lista):
"""Desliza n posições para a direita,de modo circular, os elementos de uma lista."""
if lista == []:
return lista
else:
aux = lista[-n:]
aux.extend(lista[:len(lista) - n])
return aux



Para o deslizar pata a esquerda usamos o mesmo mecanismo.

def desliza_e_n(n, lista):
"""Desliza n posições para a esquerda,de modo circular, os elementos de uma lista."""
if lista == []:
return lista
else:
aux = lista[n:]
aux.extend(lista[:n])
return aux


Como é lógico podíamos ter resolvido o problema só com uma função, cobrindo os dois casos. Fica para o leitor resolver.

Problema 5.2

Estes problemas são muito elementares e a sua função é apenas obrigar a conhecer as operações que podemos fazer sobre listas. Vamos apenas apresentar a solução para o problema de calcular e devolver a soma de todos os valores (números) contidos numa lista. Permite-nos ilustrar os diferentes modos de iterar com listas e, ainda, o que podemos fazer quando conhecemos melhor a linguagem.

Pecorrer por índice

def idades_4a(lista):
soma=0
for indice in range(len(lista)):
soma = soma + lista[indice]
return soma

Perocorrer pelo conteúdo

def idades_4b(lista):
soma = 0
for elem in lista:
soma = soma + elem
return soma

Recurso à função sum

def idades_4c(lista):
return sum(lista)

Problema 5.1

Implementar os métodos referidos pode ser feito de várias maneiras. Aqui apenas apresento a maneira mais básica. Como tempo aprenderão a fazer de outro modo recorrendo a outras construções da linguagem.

Contagem


def my_count(item, lista):
""" Número de ocorrências do item na lista."""
conta = 0
for elem in lista:
if elem == item:
conta = conta + 1
return conta

Inversão

def my_reverse(lista):
"""Inverte os elementos de uma lista. Cria uma cópia."""
return lista[::-1]

# - variante
def my_reverse_b(lista):
"""Inverte os elementos de uma lista. Cria uma cópia."""
aux = []
for elem in lista:
aux = [elem] + aux
return aux

Procura

def my_find(item,lista):
for i in range(len(lista)):
if lista[i] == item:
return i
return -1

# Variante
def my_find_b(item,lista):
for ind, elem in enumerate(lista):
if elem == item:
return ind
return -1

Insere

def my_insert(elem, pos,lista):
""" Insere o elem na posição. Assume posição correcta."""
return lista[:pos] + [elem] + lista[pos:]

domingo, 30 de outubro de 2011

Problema 4.7

O facto de este ser o último problema da folha não significa que seja o mais difícil. com efeito, e até em função do que já vimos no problema 4.6 a solução apenas depende do correcto uso do módulo random e de saber como ir retirando caracteres ao alfabeto de modo aleatório.

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

Problema 4.6

Este problema obriga a alguns cuidados. Mas também é um pretexto para percebermos como podemos construir um programa por aproximações sucessivas, não tentando resolver tudo de uma só vez.

De acordo com o algoritmo dado no enunciado uma solução possível para o nosso problema poderá ser:

def cria_chave(palavra_chave):
"""Gera uma chave a partir de uma palavra de passe."""
alfabeto = 'abcdefghijklmnopqrstuvwxyz '
# retira duplicações da palavra chave
palavra_chave = retira_dup(palavra_chave)
# divide em duas partes
ultimo_car = palavra_chave[-1]
indice = alfabeto.find(ultimo_car)
antes = alfabeto[:indice]
depois = alfabeto[indice+1:]
# retira caracteres da palavra chave
antes = retira_car(palavra_chave, antes)
depois = retira_car(palavra_chave, depois)
# junta tudo
chave = palavra_chave + depois + antes
return chave

Se olharmos para os comentários torna-se claro que o algoritmo foi correctamente implementado. Claro, que isto só é verdade se as duas funções auxiliares, retira_dup e retira_car, funcionarem bem. É disso que vamos agora tratar. Comecemos pelo problema de retirar os duplicados.

def retira_dup(cadeia):
"""Retira caracteres duplicados na cadeia."""
nova_cadeia = ''
for car in cadeia:
if car not in nova_cadeia:
nova_cadeia = nova_cadeia + car
return nova_cadeia

Esta solução, constrói uma nova cadeia, percorrendo os elementos da cadeia antiga uma a um, e só introduzindo os caracteres caso não provoquem uma duplicação.

Uma alternativa é o programa seguinte.

def retira_dup_2(cadeia):
"""Retira caracteres duplicados na cadeia."""
nova_cadeia = ''
for ind, car in enumerate(cadeia):
if car not in cadeia[ind+1:]:
nova_cadeia = nova_cadeia + car
return nova_cadeia

Aqui fazemos uso da função enumerate que nos permite percorrer a cadeia simultaneamente pela índices e pelo conteúdo. Veja-se como no teste dentro do if determinamos se um caractere é ou não repetido.

Agora a parte de retirar carateres de um texto.

def retira_car(caracteres, texto):
"""retira do texto os caracteres."""
novo_texto = ''
for car in texto:
if car not in caracteres:
novo_texto = novo_texto + car
return novo_texto

Ainda e sempre a mesma ideia: percorrer o texto (ciclo), e sempre que o caractere em análise não pertencer aos caracteres proibidos ele é adicionado (acumulador). A contagem é implícita.

A lição que podemos tirar deste exemplo é a de que vale a pena usar mecanismos de abstracção (neste caso as duas funções auxiliares) para melhor dominar a complexidade da busca da solução para o nosso problema.

Problema 4.2

O problema do cálculo do valor numérico associado a um nome formado por uma única palavra não levanta dificuldades de maior. O padrão habitual -- ciclo, contador e acumulador, chega para resolver a questão. Devido à natureza do problema vamos percorrer a cadeia de caracteres pelo seu conteúdo, pelo que a contagem é implícita. Atente-se no modo como conseguimos estabelecer a relação entre cada caractere e o seu valor numérico. Só precisamos saber o que faz a função ord e não qual o valor no código ASCII associado a cada caractere.
def valor_numerico(nome):
""" Calcula o valor numérico de um nome. 'a' = 1, ..., 'z' = 26."""
valor = 0
for car in nome:
valor = valor + (ord(car) - ord('a') + 1)
return valor

segunda-feira, 24 de outubro de 2011

Este problema é muito semelhante ao 3.7. Agora trata-se de desenhar quadrados concêntricos. Ao contrário do exemplo anterior, agora o que muda é a posição inicial já que a orientação se mantém a mesma. É preciso saber no início o incremento do lado a cada repetição.




def quad_concent(n,lado,d):
"""Desenha n quadrados concêntricos.

"""
x0 = xcor()
y0 = ycor()
for i in range(n):
quad(lado+i*2*d, x0-i*d, y0+i*d, 0)



A solução apresentada não é a única. O leitor é convidado a encontrar a sua abordagem e a compará-la com a apresentada.

Problema 3.7

Neste exemplo é-nos pedido para fazer um desenho como o da figura.


Este problema permite mostrar como se pode dominar a complexidade de um problema reutilizando código. Com efeito o que nos é pedido não é mais do que desenhar sucessivos quadrados em que o que vai mudando é o comprimento do lado e a orientação. Assim, usaremos o programa já anteriormente definido que permite desenhar um quadrado, conhecidos o comprimento do lado, a posição inicial e a orientação.


def quad(lado,xcor,ycor,orient):
"""Desenha um quadrado em que o lado, a posição inicial e a orientação inicial podem variar.

"""
penup()
goto(xcor,ycor)
setheading(orient)
pendown()
for i in range(4):
forward(lado)
right(90)
hideturtle()


Com este programa a funcionar como auxiliar tudo se torna mais simples.

def nautilus(n, lado, xcor,ycor, angulo):
"""Desenha n quadrados com lados e orientações variáveis.
Desenha uma forma semelhante a um Nautilus.

"""
reset()
for conta in range(n):
quad(lado,xcor,ycor,angulo)
lado = lado + 10
angulo = angulo + 15
hideturtle()

Problema 3.6

Este programa mostra como se pode pôr a tartaruga a fazer um passeio aleatório (random walk).


from random import randint

def alea(num_lados):
"""Desenha num_lados aleatoriamente.

"""
for conta in range(num_lados):
novo_x = randint(-100,100)
novo_y = randint(-100,100)
goto(novo_x,novo_y)
hideturtle()

def alea_cor(num_lados):
"""Desenha num_lados aleatoriamente.

"""
colormode(255)
for conta in range(num_lados):
novo_x = randint(-200,200)
novo_y = randint(-200,200)
r = randint(0,255)
g = randint(0,255)
b = randint(0,255)
color((r,g,b))
goto(novo_x,novo_y)
hideturtle()

O primeiro programa determina as coordenadas entre dois valores fixos. No segundo exemplo, introduzimos a possibilidade de os traços serem coloridos (também aqui de modo aleatório.).

Problema 3.4

O módulo turtle permite controlar tartarugas que fazem desenhos num tela. Durante as aulas colocámos vários problemas com um grau de dificuldade pequeno. Por exemplo, efectuar desenhos de polígonos regulares. Têm uma estrutura muito semelhante. A listagem que se segue ilustra como se pode fazer de dois modos distintos o desenho de um quadrado, conhecido apenas o lado.


def quadrado_2(lado):
"""Desenha um quadrado de lado lado.

"""
conta = 0
while conta < 4:
forward(lado)
right(90)
conta = conta + 1

def quadrado_3(lado):
"""Desenha um quadrado de lado lado.

"""
for i in range(4):
forward(lado)
right(90)

Aqui o importante a reter é que é mais adequado usar o ciclo for: conhecemos quantas vezes vamos ter que executar o ciclo.

Mas podemos querer uma solução em que a posição e a orientação iniciais também possam ser variáveis. Esta questão resolve-se considerando esses elementos como parâmetros.


def quad(lado,xcor,ycor,orient):
"""Desenha um quadrado em que o lado, a posição inicial e a orientação inicial podem variar.

"""
penup()
goto(xcor,ycor)
setheading(orient)
pendown()
for i in range(4):
forward(lado)
right(90)
hideturtle()

Podemos generalizar o programa do quadrado para o caso de um polígono regular, desenhado a partir do conhecimento do número de lados e do seu comprimento.


def poligono(num_lados,tam_lado):
"""Desenha um polígono regular.

"""
angulo = 360.0 / num_lados
for i in range(num_lados):
forward(tam_lado)
right(angulo)
hideturtle()


Uma vez mais se pode ver como se trata a questão de ter mais elementos variáveis: são acrescentados como parâmetros. Este programa pode ajudar-nos a desenhar uma circunferência, conhecido o seu raio. A localização inicial pode ser fixa ou variável.


def circunf(raio):
"""Desenha uma circunferência conhecido o raio.

"""
perimetro = 2 * math.pi * raio
tam_lado = int(perimetro / 360.0)
poligono(360,tam_lado)
hideturtle()


def circunf_centro(raio, xcor_centro,ycor_centro):
"""Desenha uma circunferência conhecido o raio e a posição do centro.

"""
perimetro = 2 * math.pi * raio
tam_lado = int(perimetro / 360.0)
penup()
goto(xcor_centro, ycor_centro)
pendown()
poligono(360,tam_lado)
hideturtle()

quinta-feira, 13 de outubro de 2011

Calcular o valor aproximado de PI

Pi é um número irracional e, por isso, o melhor a que podemos aspirar é calcular o seu valor aproximado. Ao longo dos séculos foram aparecendo várias formas de o fazer. Uma delas foi proposta por Leibniz:



Outra por Wallis:



Se as fórmulas são distintas do ponto de vista informático são muito semelhantes, uma vez que remetem para um mesmo padrão de programação, baseado no recurso a um ciclo, uma variável de contagem e outra variável de acumulação.

A ideia da solução consiste em ir acrescentando ao longo do ciclo um termo (caso de Leibniz) ou um factor (caso de Wallis) ao resultado parcial.

Comecemos com o caso da abordagem de Leibniz:

def leibniz_pi_1(num_termos):
""" Calcula valor de pi segundo fórmula de Leibniz.
"""
conta = 0
acum = 0.0
den = 1
while conta < num_termos:
acum = acum + (1.0/den) * (-1)**conta
den = den +2
conta = conta + 1
return 4 * acum

Nesta solução é claro que conta faz o papel de contador, e acum o de acumulador. Notar o modo como geramos o termo de ordem i, com o auxílio da variável den. Esta solução pode ser simplificada se tornarmos o contador implícito recorrendo a um ciclo for.

def leibniz_pi_2(num_termos):
""" Calcula valor de pi segundo fórmula de Leibniz.
"""
acum = 0.0
den = 1
for i in range(num_termos):
acum = acum + (1.0/den) * (-1)**i
den = den +2
return 4 * acum

Mas a fórmula de Leibniz também pode ser apresentada na sua forma compacta:



Se a usarmos directamente chegamos ao programa:

def leibniz_pi_3(num_termos):
""" Calcula valor de pi segundo fórmula de Leibniz.
"""
acum = 0.0
for i in range(num_termos):
acum = acum + ((-1)**i) * (1.0/(2 * i + 1))
return 4 * acum

Passemos agora para o caso da aproximação pela fórmula de Wallis:

def wallis_1(num_fact):
"""
Calcula o valor de pi usando a fórmula de Wallis.
"""
acum = 1.0
for i in range(2, num_fact,2):
esquerda = i / float((i-1))
direita = i /float((i+1))
acum = acum * esquerda * direita
return 2 * acum

Este modo de resolver já tem o contador implícito e baseia-se no facto de podermos agrupar os factores aos pares, pois.



Assim, em cada passagem pelo ciclo, calculamos dois factores.

Mas podemos também fazer uso da forma compacta:



Daqui decorre a nova versão do programa:

def wallis_2(num_fact):
"""
Calcula o valor de pi usando a fórmula de Wallis.
"""
acum = 1.0
for i in range(1, num_fact/2):
factor = float((2 * i) ** 2) / ((2 * i) ** 2 - 1)
acum = acum * factor
return 2 * acum

Se compararmos esta versão com a última versão de Leibniz, fica claro o que queremos dizer quando falamos de semelhanças.

sexta-feira, 7 de outubro de 2011

Problema 2.8

Retirar as vogais de uma cadeia de caracteres e substituir as vogais por um espaço em branco. Para resolver esta questão percebemos que vamos ter que repetir para todos os caracteres um teste para saber se é ou não uma vogal, substituindo no caso afirmativo por um espaço em branco. A solução vai assim ter que combinar um ciclo (for é a nossa opção e esperamos que perceba porquê), com um filtro implementado graças a uma instrução if de duas vias (alternativas).


def tira_vogais(cadeia):
"""Retira as vogais e substitui por um espaço em branco.
"""
vogais ='aeiou'
nova_cadeia =''
for conta in range(len(cadeia)):
if cadeia[conta] in vogais:
nova_cadeia = nova_cadeia + ' '
else:
nova_cadeia = nova_cadeia + cadeia[conta]
return nova_cadeia


Notar, na solução acima, como conseguimos evitar testes complexos com todos os caracteres, substituindo esses teste por uma única instrução de pertença.

Ainda se pode melhorar o programa introduzindo uma ideia nova: podemos percorrer a cadeia de caracteres pelos valores em vez de ser pelos índices! Durante o curso aprofundaremos esta ideia.


def tira_vogais_b(cadeia):
"""Retira as vogais e substitui por um espaço em branco.
"""
vogais ='aeiou'
nova_cadeia =''
for car in cadeia:
if car in vogais:
nova_cadeia = nova_cadeia + ' '
else:
nova_cadeia = nova_cadeia + car
return nova_cadeia

Problema 2.3

O problema de obter todas as sub-cadeias de um dado comprimento depende da nossa compreensão da operação de fatiamento e do conceito de repetição (e da sua implementação através de um ciclo). Como as repetições são em número fixo e o seu valor pode ser determinado antes da execução do código, vamos optar pela instrução for. A ideia da solução abaixo apresentada é a de ir percorrendo a cadeia posição a posição, retirando em cada momento uma fatia de tamanho n. Temos que ter ainda em atenção o facto de que devemos terminar o programa mal não seja possível ter maias cadeias de tamanho n.

def sub_cadeias(pal, n):
"""
Todas as subcadeias de comprimento n.
"""
for i in range(len(pal) - n + 1):
print pal[i:i + n]

Problema 2.2

Neste problema é-nos pedido para apresentar todos os prefixos de uma cadeia de caracteres, um por linha. Podemos apresentar o de comprimento 1 primeiro, o de comprimento 2 a seguir, e assim sucessivamente. Temos pois uma acção repetitiva, o que indica que necessitamos de recorrer a um ciclo. Em cada etapa do ciclo é preciso saber onde termina o prefixo, visto sabermos que o início é sempre na primeira posição. Existem duas questões então a resolver: a contagem do número de vezes que vamos andar a repetir e, outra questão, controlar a cada momento o fim do prefixo. Acontece que estas duas questões se podem resolver de modo articulado. Olhemos então para uma solução possível.


def prefixos(cadeia):
"""Determina todos os prefixos de uma cadeia de caracteres.
"""
conta = 0
while conta < len(cadeia):
print cadeia[:conta+1]
conta = conta + 1

Este padrão de contagem é tão comum que pode ser substituído de modo muito simples recorrendo à instrução de controlo for.


def pref(cadeia):
for conta in range(len(cadeia)):
print cadeia[:len(cadeia) - conta]

Mas também podemos apresentar os prefixos por ordem decrescente do seu tamanho.

Podemos, finalmente, mudar o problema, porv forma a que o resultado sejam os sufixos

def sufixos(cadeia):
for conta in range(len(cadeia)):
print cadeia[- (conta + 1):]
O problema original é muito simples. Vamos por isso acrescentar apenas o requisito de o podermos usar para diferentes valores do lado do quarado e do raio da circunferência.

import math

def hamburger(lado, raio):
"""Que forma de hamburger tem a área maior?.
"""
area_quad = lado ** 2
area_circ = math.pi * raio ** 2
if area_quad > area_circ:
print 'quadrado'
elif area_quad < area_circ:
print 'redondo'
else:
print 'indiferente'


Notar o modo como usámos a condicional if e a necessidade de importar o módulo math para ter acesso ao valor de pi.

Problema 1.1

Trata-se de uma questão trivial. Repare que a solução para o problema depende de um número de constantes pelo que não é preciso estar a usar definições.

print 2.9e6 * (9.459 * 10 ** 12)


Note que usamos de propósito dois modos de representar números muito grandes.

Se quisermos resolver um problema mais geral de determinar a distância em quilómetros para qualquer corpo celeste então temos que definir uma função em que o argumento é o valor em anos-luz.

def dist_quilo(anos_luz):
"""Calcula a distância equivalente em quilómetros.
"""
return anos_luz * 9.459E12

Bug no WingIDE

O WindIDE é um ambiente integrado de desenvolvimento muito interessante. Acresce que existe uma versão gratuita! Mas todos sabemos que nada é perfeito... Então qual é o problema?


Quando carregamos numa tecla geramos um código interno. No passado esse código dava pelo nome de código ASCII (para American Standard Code for Information Interchange). Graças à uniformização que decorre de todos usarem o mesmo código, o que se escreve numa máquina (aplicação) é entendido por outra máquina qualquer (aplicação). O código ASCII permite representar os dígitos, as letras do alfabeto (maiúsculas e minúsculas) e alguns sinais (maior, menor, espaço,...). Não admira que fossem apenas estes. Afinal isto foi inventado para a língua inglesa que não sabe o que é, por exemplo, um til ou um c com cedilha. Para que outras línguas possam ser usadas plenamente foram desenvolvidos outros standards. O utf-8 é um deles de uso cada vez mais generalizado.\\


Por defeito o WingIDE salva os ficheiros de código em utf-8 e, por isso, estamos à vontade com a língua portuguesa. Precisamos no entanto indicar no início do ficheiro que estamos a usar essa codificação. O modo de o fazer é usar:


#-*- encoding: utf-8 -*-


Acontece porém que, devido a um erro lamentável no IDE se colocarmos esta linha, quando mandamos correr o programa obtemos uma mensagem de erro! A solução passa por retirar a dita linha. Mas atenção!!! Se estiver a importar o ficheiro sem a linha ... dá erro. E terá que acrescentar a dita. Bizarro, não?


Enquanto não tiverem resolvido o problema teremos que saber se colocamos ou não a indicação expressa da codificação em função do modo como vamos usar o ficheiro: correr (não) ou importar (sim).

sexta-feira, 30 de setembro de 2011

Modelo de Funcionamento

O modelo de funcionamento das aulas foi completamente alterado. Antigamente havia aulas teóricas (T), aulas teórico-práticas (TP) e aulas de práticas laboratoriais (PL). Chegou-se à conclusão que esse modelo não é efectivo. Agora existem apenas aulas. Isto tem fortes consequências. Desde logo a redução das aulas de contacto de 6 para 4. Por outro lado, as aulas passarão a ser obrigatórias. Em terceiro lugar, as aulas serão mais guiadas pelos problemas, com chamadas de atenção para aspectos da linguagem e da programação relevantes. Finalmente, o que é dado nas aulas não esgota o que os alunos devem saber, e as aulas só serão efectiva se os alunos as prepararem previamente e praticarem fora delas.

Para se aprender a programar, no entanto, não basta alterar a metodologia de ensino e aprendizagem. É preciso uma nova atitude dos alunos e, em primeiro lugar, que interiorizem que só se aprende a programar ... programando.

A chave do sucesso é pois praticar, praticar, praticar!

Mais um ano que começa

Vamos dar início a mais um ano. EAs primeiras palavras são de boas vindas para todos. Neste blogue irão aparecer respostas a muitos dos problemas e desafios que ao longo do ano vão ser chamados a resolver. É também um lugar de interacção, estando disponível para responder às vossas dúvidas deixadas nas mensagens. Por isso não se esqueçam de passar por aqui por regularidade.

Boas Programações!


terça-feira, 25 de janeiro de 2011

Problema 5: Exame Normal

Este problema, não sendo difícil, obriga a ter alguma disciplina. Identificamos três questões: (1) ler um ficheiro e extrair a informação relevante; (2) usar essa informação como chave de um dicionário para ir buscar os dados que nos são pedidos; (3) ordenar os dados. Vejamos uma hipótese de solução.


def nomes(fich, dicio):
"""
Usa um ficheiro para extrair nomes
e um dicionário para encontrar nome completo. Ordenamento simples.
""”
# Ler informação
ficheiro = open(fich)
dados = ficheiro.readlines()
ficheiro.close()
# Extrair dados
lista = []
for elem in dados:
pos = elem.find('~')
nome = elem[pos+1:-1]
if nome in dicio:
completo = dicio[nome][0]
lista.append(completo)
# ordenar
lista.sort()
return lista

Porque a informação está logicamente organizada por linhas é assim que a devemos ler. Vamos depois iterar para cada linha na busca do nome do utilizador, o que fazemos usando o método find. Encontrada a posição o nome tem que estar a partir dessa posição até ao final, mas devemos ter o cuidado de retirar o código correspondente à mudança de linha. O teste dentro do ciclo serve apenas para garantir que só extraímos informação se ela estiver efectivamente no dicionário. Finalmente o método sort é usado para ordenar o resultado.

Problema 4: Exame Normal

Pretendemos obter a lista com todas as posições iniciais em que um dado padrão ocorre num texto. Texto e padrão são cadeias de caracteres. Vamos ter que percorrer todo o texto à procura do padrão. Cada vez que encontramos o padrão guardamos a posição inicial, e recomeçamos. Vamos usar os métodos sobre cadeias de caracteres que nos permitem procurar cadeias de caracteres noutras cadeias. Segue uma solução.



def procura_pad(texto, padrao):
"""
Lista com as posições iniciais de todas as ocorrências
do padrão no texto.
"""
lista = []
pos = texto.find(padrao)
while pos != -1:
lista.append(pos)
pos = texto.find(padrao,pos + 1)
return lista

Esta solução faz uso do método find, com dois argumentos, e do conhecimento que temos de que, caso o padrão não ocorra, nos é devolvido o valor -1. Notar que avançamos posição a posição e não por saltos igual ao tamanho do padrão. Assim não perdemos nenhuma ocorrência.

Problema 3: Exame Normal

Trata-se de um problema muito simples. A primeira questão a resolver é como representar a matriz. A escolha óbvia é por recurso a uma lista de listas. Depois apenas precisamos de percorrer a matriz e alterar cada um dos seus elementos retirando o valor de referência. como não podemos ter valores negativos, nos casos em que isso acontece o valor a ser guardado é zero. Dito isto uma solução possível é a seguinte.


def dif(matriz,numero):
"""
Retira a cada elemento da matriz o valor igual ao número. Não podem
existir valores negativos.
"""
for lin in range(len(matriz)):
for col in range(len(matriz[0])):
matriz[lin][col] = max(matriz[lin][col] - numero, 0)
return matriz


Veja-se como evitamos o teste para saber se o resultado é negativo. Também não assumimos, e não o devíamos fazer, que a matriz é quadrada.