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!