Você está aqui: Computação > Python: Pensando como um Cientista da Computação > Estudo de caso: design de interfaces.
Formato Livro
15/04/2011
Python: Pensando como um Cientista da Computação

Estudo de caso: design de interfaces

4.1   TurtleWorld

NT: O autor se inspira na linguagem Logo usada para ensinar os fundamentos da computação para crianças. Logo mostra uma tartaruga na tela e aceita comandos para mover a tartaruga, traçando ou não seu percurso.

Para acompanhar este livro, escrevi um conjunto de módulos chamados Swampy. Um desses módulos é TurtleWorld, que fornece um conjunto de funções para desenhar linhas através de orientações dadas a tartarugas para que caminhem na tela. Você pode baixar Swampy em thinkpython.com/swampy. Siga as instruções para instalar Swampy em seu sistema.

Vá até o diretório ou pasta que contém o arquivo TurtleWorld.py e crie um novo arquivo chamado polygon.py, digitando nele e o seguinte código:

from TurtleWorld import *

world = TurtleWorld()

bob = Turtle()

print bob


wait_for_user()

A primeira linha é uma variação da declaração import que já vimos. Ao invés de criar um objeto módulo esta declaração importa as funções diretamente do módulo para que possam ser acessadas sem o uso da notação de ponto.

As linhas seguintes criam um TurtleWorld atribuído ao world e uma Turtle atribuído a bob. Impressão de bob resultará em algo como:

<TurtleWorld.Turtle instance at 0xb7bfbf4c>

Isso significa que bob se refere a uma instância de uma Turtle (tartaruga), tal como definido no módulo TurtleWorld. Neste contexto, "instância" significa um membro de um conjunto; esta Turtle é um dos elementos de todos os objetos Turtle possíveis.

wait_for_user  é uma função usada para informar a TurtleWorld que espere até que o usuário faça alguma coisa, fornecendo comandos. TurtleWorld fornece diversas funções de manipulação de direção: fd e bk para frente e para trás, lt e rt para virar à esquerda ou direita (em inglês, fd: forward, bk: back, lt: left e rt: right). Além disso, cada tartaruga segura uma caneta que pode estar abaixada ou não. Se estiver para baixo a tartaruga deixa um rastro quando ele se move. As funções pu e pd significam "caneta para cima" e "caneta para baixo" (em inglês, pen up e pen down).

Para desenhar um ângulo reto, adicione estas linhas no programa (após a criação de bob e antes de chamar wait_for_user):

fd(bob, 100)

rt(bob)

fd(bob, 100)

A primeira linha manda  bob dar 100 passos a frente. A segunda manda virar à esquerda. Ao executar este programa você deverá ver bob se mover na direção leste e sul, deixando para trás dois segmentos de linha.

Em seguida modifique o programa para desenhar um quadrado. Não prossiga até que tenha conseguido!

4.2  Repetição simples

É provável que você tenha escrito algo assim (deixando de fora o código que cria TurtleWorld e espera para o usuário):

fd(bob, 100)

lt(bob)


fd(bob, 100)

lt(bob)


fd(bob, 100)

lt(bob)


fd (bob, 100)

Este código está correto e realizará a tarefa proposta. No entanto, podemos fazer a mesma coisa de forma mais concisa usando a declaração for. Experimente criar um script, ou simplesmente usar o comando de linha, com o seguinte código:

for i in range(4):

. . . . print 'Olá!'

# Isto produzirá a saída:

Olá!

Olá!

Olá!

Olá!

Este é o uso mais simples da declaração for. Veremos outros usos desta declação mais tarde. Agora você deverá ser capaz de reescrever seu programa para desenhar um quadrado.

Por exemplo, aqui está uma solução para este problema:

for i in range(4):

fd(bob, 100)

lt (bob)

A sintaxe de uma declaração for é semelhante a de uma definição de função. Ele tem um cabeçalho que termina com dois pontos e um corpo recuado. O corpo pode conter qualquer número de declarações. A declaração for é as vezes chamada de loop, porque o fluxo de execução passa através do corpo e volta ao início dando voltas (loops). Neste caso, corre-se o corpo quatro vezes.

Na verdade esta última versão é um pouco diferente do código de desenho anterior porque, depois de desenhar o último lado do quadrado ele ainda muda a direção da tartaruga. Por isto ela leva um pouco mais de tempo que a versão anterior mas simplifica bastante o código. Além disto ela deixa a tartaruga na mesma na posição em que estava na partida.

4.3  Exercícios

Seguem uma série de exercícios utilizando o TurtleWorld. Embora divertidos eles têm um propósito. Pense sobre este propósito enquanto você estiver trabalhando com eles.

As soluções para os exercícios estão nas seções seguintes. Não olhe até que você tenha completado (ou pelo menos tentado) suas próprias soluções .

  1. Escreva uma função chamada quadrado que recebe um parâmetro denominado t, que é uma Turtle. A função deve usar a tartaruga para desenhar um quadrado.
    Escreva uma chamada de função que passa bob como argumento para quadrado, e executar o programa novamente.
  2. Adicione outro parâmetro em quadrado, chamado comprimento. Modifique o corpo da função de modo que comprimento  seja o comprimento dos lados. Depois modifique a chamada de função para fornecer este argumento. Execute o programa novamente e teste seu programa para diversos  valores de comprimento.
  3. As funções de lt e rt fazem giros de 90 graus, por default. Você pode acrescentar um segundo argumento para especificar o número de graus. Por exemplo, lt(bob, 45) para girar bob 45 graus para a esquerda.
    Faça uma cópia do quadrado e mude o nome para poligono. Adicione outro parâmetro denominado n e modifique a função para que ela desenhe um polígono regular de  n lados. Dica: Os ângulos externos de um polígono regular de n lados são 360.0/ graus.
  4. Escreva uma função chamada circulo que tem uma tartaruga t e o raio r como parâmetros e que desenha um círculo aproximado chamando  poligono com um comprimento e número de lados adequados. Teste a sua função com diversos  valores para r.
    Dica: calcule a circunferência do círculo e certifique-se de que comprimento * n = circunferencia.
    Outra dica: se bob estiver muito lento, você pode apressá-lo alterando bob.delay, que é o tempo entre movimentos, em segundos. Um valor de bob.delay = 0,01 deve fazê-lo movimentar-se a contento.
  5. Faça uma versão mais geral da função circulo chamando-a de arco, que recebe o parâmetro adicional angulo para determinar que fração de um círculo deve ser desenhada. angulo pode ser fornecida em graus de forma que um círculo completo será desenhado quando angulo = 360.

4.4   Encapsulamento

O primeiro exercício pede que você coloque o código para desenhar quadrados dentro de uma função e depois chame a função, passando a tartaruga como parâmetro. Aqui está uma solução:

def quadrado(t):

for i in range(4):

fd(t, 100)

lt(t)


quadrado(bob)

As declarações  fd e lt, dentro do loop for são recuadas duas vezes, indicando que fazem parte do loop que, por sua vez, está dentro da definição da função. A linha seguinte quadrado(bob), está alinhada com a margem esquerda, representando o final do loop e da definição da função.

Dentro da função, t se refere à mesma tartaruga que bob, portanto as declarações  lt(t)  e  lt(bob) tem o mesmo efeito. Por que então não chamar o parâmetro de bob? A idéia aqui é que t pode ser qualquer tartaruga, e não apenas bob. Você pode criar outras tartarugas e passá-las como argumento para quadrado:

ray = Turtle()

quadrado(ray)

A prática de envolver um pedaço de código dentro de uma função é chamada de  encapsulamento. Uma das vantagens do encapsulamento é que ele atribui um nome para aquele porção de código, o que funciona como um tipo de documentação. Outra vantagem está na reutilização do código. Um bloco de código pode ser chamado várias vezes sem que se precise copiá-lo várias vezes!

4.5   Generalização

O próximo passo foi adicionar o parâmetro  comprimento na função quadrado. Aqui está uma solução:

def quadrado(t, comprimento):

for i in range(4):

fd(comprimento, t)

lt(t)


quadrado(bob, 100)

Este ato de adicionar um parâmetro em uma função é chamado de generalização, pois faz com que a função fique mais geral: na versão anterior, o quadrado tem sempre o mesmo tamanho; nesta versão ele pode ser de qualquer tamanho.

O passo seguinte também é uma generalização. Em vez de desenhar quadrados, poligonos desenha polígonos regulares com qualquer número de lados. Eis aqui uma solução:

def poligono(t, n, comprimento):

angulo = 360.0/n

for i in range(n):

fd(t, comprimento)

lt(t, angulo)


poligono(bob, 7, 70)

Isso faz com que seja desenhado um polígono de 7 lados com comprimento 70. Observe que, uma função com muitos argumentos, acaba sendo fácil esquecer quais são e em que ordem elas devem estar estes argumentos. Em Python é possível usar argumentos nomeados, (keyword arguments) incluindo, na definição da função, os nomes dos parâmetros:

poligono(bob, n = 7, comprimento = 70)

Esta sintaxe torna o programa mais legível e serve como lembrete do modo como argumentos e parâmetros funcionam: quando você chama uma função os argumentos são atribuídos aos parâmetros.

4.6   Design de interface

O próximo passo é escrever circulo, que tem um raio r como um parâmetro. Aqui está uma solução simples que usa poligonos para desenhar um polígono de 50 lados:

def circulo(t, r):

circunferencia = 2 * math.pi *r

n = 50

comprimento = circunferencia/n

poligono(t, n, comprimento)

A primeira linha calcula a circunferência de um círculo com raio r usando a fórmula 2 π r. Como usamos math.pi, temos que importar o módulo de funções matemáticas (import math). Por convenção as declarações  import são normalmente colocadas no início do script.

n é o número de segmentos de linha em nossa aproximação de um círculo, enquanto comprimento é o comprimento de cada segmento. Assim, poligonos desenha um polígono de 50 lados que se aproxima de um círculo com raio r.

Esta solução tem a seguinte limitação:  n é uma constante e isto significa que, para círculos muito grandes, os segmentos de linha são muito longos, e para círculos pequenos se desperdiça muito tempo desenhando segmentos pequenos. Uma solução seria a de generalizar a função tomando n como um parâmetro. O usuário (qualuer um que use circulo ) teria mais controle mas a interface ficaria um pouco menos limpa.

A interface de uma função é um resumo de como ela é usada: quais são os parâmetros, o que ela faz e qual é o valor de retorno. Uma interface é limpa "clean" se é "tão simples como possível, mas não mais simples que isto. (Einstein)"

Neste exemplo r faz parte da interface pois especifica o círculo a ser desenhado.  n seria menos adequado como parâmetro pois se refere aos detalhes de como o círculo deve ser desenhado.

Ao invés complicar a interface é melhor escolher um valor apropriado de n dependendo do valor de circunferencia:

def circulo(t, r):

circunferencia = 2 * math.pi * r

n = int(circunferencia/3) + 1

comprimento = circunferencia/n

poligono(t, n, comprimento)

Agora o número de segmentos é (aproximadamente) circunferência/3, de forma que o comprimento de cada segmento é (aproximadamente) 3. Este comprimento é suficientemente pequeno para que os círculos tenham boa aparência, mas é grande o bastante para ser eficiente e adequado para círculos de qualquer tamanho.

4.7  Refactoring

NT.: Mantenho esta palavra em inglês porque não há um consenso entre os usuários de Python no Brasil sobre uma tradução adequada. A palava refatoração tem sido rejeitada para este fim e, de fato, ela não parece ser uma tradução adequada.

Depois de escrever a função circulo nós a reutilizamos para poligonos porque um polígono de muitos lados é uma boa aproximação para um círculo. Mas a função arco não se mostra tão cooperativa. Não podemos usar poligono ou circulo para desenhar um arco.

Uma alternativa é começar com uma cópia do poligono e transformá-lo em arco. O resultado pode ser como este:

def arco(t, r, angulo):

arc_length = 2 * math.pi * r * angulo / 360

n = int (arc_length / 3) + 1

step_length = float(angulo)/n


for i in range(n):

fd(t, step_length)

lt(t, step_angle)

A segunda metade desta função se parece com polígono, mas não podemos reutilizar poligonos sem alterar a interface. Poderíamos generalizar poligono inserindo um ângulo como terceiro argumento mas, neste caso o nome poligono já não seria apropriado! Em vez disso, vamos criar uma função mais geral e chamá-la polilinha:

def polilinha(t, n, comprimento, angulo):

for i in range(n):

fd(t, comprimento)

lt(t, angulo)

Agora podemos reescrever poligono e arco para usar polilinha:

def poligono(t, n, comprimento):

angulo = 360.0/n

polilinha(t, n, comprimento, angulo)


def arco(t, r, angulo):

arc_length = 2 * math.pi * r * angulo/360

n = int(arc_length / 3) + 1

step_length = arc_length/n

step_angle = float (angulo)/n

polilinha(t, n, step_length, step_angle)

Finalmente, podemos reescrever circulo  para usar arco :

def circulo(t, r):

arco(t, r, 360)

Este processo de reorganização de um programa para melhorar as interfaces de funções e facilitar a reutilização de código é chamado de refatoring. Neste caso notamos que havia um código semelhante em arco e poligono, de modo que "fatorado" em polilinha.

Se tivéssemos planejado com antecedência poderíamos ter escrito polilinha diretamente e evitar o refactoring. Mas, muitas vezes, não sabemos o suficiente no início de um projecto para projetar todas as interfaces. Na medida em que se avança na programação o problema se torna mais claro. Às vezes o refactoring simplesmente indica que você aprendeu alguma coisa nova sobre o problema que quer resolver.

4.8   Plano de desenvolvimento

Um plano de desenvolvimento é um processo para escrever programas. O processo que utilizamos neste estudo de caso foi de "encapsulamento e generalização." As etapas deste processo são:

  1. Comece escrevendo um pequeno programa sem definições de função.
  2. Quando o programa estiver funcionando, faça o encapsulamento deste código em uma função, atribuindo a ela um nome descritivo.
  3. Generalize a função adicionando parâmetros apropriados.
  4. Repita os passos de 1 a 3 até que você tenha um conjunto de funções funcionais. Copie e cole o código que está funcionando bem para evitar redigitação (e re-depuração).
  5. Esteja atento para oportunidades de melhorar o programa por meio de refactoring. Por exemplo, se você tiver um código similar em vários pontos do programa, considere fatorá-lo em uma função que generalize toda a tarefa necessária.

Esse processo tem alguns inconvenientes, e vamos ver alternativas mais tarde. Mas ele pode ser útil  se você não sabe, à princípio, como dividir o programa em funções. Esta abordagem permite realizar um projetar e aperfeiçá-lo na medida em que você o desenvolve.

4.9   Docstring

Uma docstring é uma sequência no início de uma função que explica a interface ("doc" é a abreviação de "documentação"). Aqui está um exemplo:

def polilinha(t, comprimento, n, angulo):

"""Desenha n segmentos de retas com comprimento dado, formando angulo (em graus) entre eles.

t é um objeto (Turtle)

"""

for i in range(n):

fd(t, comprimento)

lt(t, angulo)

Esta docstring é uma string entre aspas triplas - também conhecida como string multilinha porque as tês aspas permitem que a sequência de caracteres (string) ocupe  mais de uma linha. A docstring é bastante resumida mas contém a informação essencial para o uso desta função. Ela explica sucintamente o que a função faz (sem entrar em detalhes de como faz), que efeito cada parâmetro tem sobre o comportamento da função e de que tipo cada parâmetro deve ser (se isto não for óbvio).

Escrever esse tipo de documentação é uma parte importante do design de interface. Uma interface bem projetada deve ser simples de explicar: se você tem dificuldade em explicar uma função, este pode ser um sinal de que a interface deveria ser melhorada.

4.10   Depuração

Uma interface é como um contrato entre uma função e aquele que a chama. Quem chama, pode ser o usuário ou outra parte do programa, concorda em fornecer alguns parâmetros e a função aceita fazer o trabalho certo.

Por exemplo, polilinha requer quatro argumentos. A primeira tem de ser uma Turtle ou algum outro objeto que aceita as funções fd  e lt . O segundo é um número que provavelmente deve ser positivo, apesar de se verificar que a função funciona mesmo se for negativo. O terceiro argumento deve ser um inteiro pois a função range retorna um erro de seu argumento não for um inteiro (dependendo de qual versão de Python você está executando). O quarto tem que ser um número, compreendido como a medida do ângulo, em graus.

Estes requisitos são chamados de pré-condições, pois devem estar satisfeitos antes que a função inicie sua execução. Por outro lado, as condições quanto ao resultado da função são chamados de  pós-condições. Entre estas condições posteriores estão o efeito que se deseja obter com aquela  função (tal como desenhar segmentos de reta) e eventuais efeitos colaterais (como mover a Turtle ou fazer outras alterações no TurtleWorld).

Pré-condições são de responsabilidade do solicitante. Se ele viola uma condição (que se espera esteja devidamente documentada!) e a função não funciona corretamente, o erro está na chamada, e não na função.

4.11   Glossário

argumento nomeado:
Um argumento que inclui o nome do parâmetro como uma "palavra-chave".
docstring:
Uma sequência de caracteres que aparece na definição da função para documentar a interface desta função.
encapsulamento:
O processo de transformar uma sequência de instruções em uma definição de função.
generalização:
O processo de substituir algo desnecessariamente específico (como um número) por algo mail geral (como uma variável ou parâmetro).
instância (instance):
Um membro de um conjunto. O TurtleWorld neste capítulo é um membro do conjunto de TurtleWorlds.
interface:
Uma descrição de como usar a função, incluindo nome e descrição dos argumentos e valor de retorno.
loop:
Uma parte de um programa que pode ser executada várias vezes.
plano de desenvolvimento:
Um processo para se escrever programas.
pós-condição:
Um requisito que deve ser satisfeito pela função antes de terminar.
pré-condição:
Um requisito que deve ser satisfeito pelo chamador antes de uma função se inicie.

4.12  Exercícios

Exercício 1  
  1. Escreva docstrings apropriadas para as funções poligono, arco e circulo.
  2. Desenhe um diagrama de pilha que mostra o estado do programa durante a execução de circulo(bob, ray). Você pode fazer os cálculos a mão ou acrescentar declarações print ao longo do código para ver resultados parciais.
  3. A versão do arco no ponto  4,7 não é muito precisa porque a aproximação linear do círculo é sempre fora do círculo verdadeiro. Como resultado, a tartaruga acaba por algumas unidades de distância do destino correto. Minha solução mostra uma maneira de reduzir o efeito desse erro. Leia o código e ver se faz sentido para você. Se você desenhar um diagrama, você pode ver como ele funciona.

Exercício 2

Escreva um conjunto geral de funções capazes de desenhar flores como as mostradas na figura 1:


Figura 1

Exercício 3

Escreva um conjunto geral de funções capazes de desenhar formas como estas, na figura 2:


Figura 2

Exercício 4

As letras do alfabeto podem ser construídas a partir de um pequeno número de elementos básicos, como linhas verticais e horizontais e algumas curvas. Projete uma fonte que pode ser desenhada com um número mínimo de elementos básicos. Depois escreva funções para desenhar as letras do alfabeto.

Escreva uma função para cada letra, com nomes draw_a, draw_b, etc, e coloque suas funções em um arquivo chamado letters.py. Você pode baixar uma "máquina de escrever tartaruga" para ajudá-lo a testar o código.


Você pode ver as soluções dadas pelo autor nas seguintes páginas:

  • Solução para o exercício 1, desenho do polígono: polygon.py.
  • Exercício 2, desenho das flores, figura 1 : flower.py.
  • Exercício 3, formas da figura 2: pie.py.
  • Exercício 4, desenho das letras do alfabeto: letters.py.
  • Exercício 4, "máquina de escrever tartaruga": typewriter.py.
Envie seu comentário!

Leia no site: | Devemos Acreditar na Ciência? | Hipótese, Modelo e Teoria em Física | Cosmologia - Estrutura do Universo | História da Pessoa com Deficiência |