Cibernetia > Manuales > Python
Búsqueda personalizada







Capítulo 4 Estudio de caso: diseño de una interfaz

4.1 TurtleWorld

Para acompañar este libro, he escrito un conjunto de módulos llamados Swampy. Uno de estos módulos es TurtleWorld, que proporciona un conjunto de funciones para la elaboración de líneas de dirección generadas por tortugas alrededor de la pantalla.

Puede descargar Swampy desde allendowney.com/swampy. Entra en el directorio que contiene TurtleWorld.py, inicia el intérprete de Python, y escribe:

>>> from TurtleWorld import *

Esta es una variación de la declaración import que vimos antes. En lugar de crear un objeto módulo, importa directamente las funciones del módulo, de modo que podemos acceder a ellas sin el uso de la notación punto. Por ejemplo, para crear TurtleWorld, escribe:

>>> TurtleWorld()

Una ventana aparecerá en la pantalla y el intérprete debería mostrar algo como esto:

Los delimitadores angulares (es decir, "<" y ">") indican que el valor de retorno de TurtleWorld es una instancia de una TurtleWorld tal como se define en el módulo TurtleWorld. En este contexto, una instancia forma parte de un conjunto; este TurtleWorld es un elemento del conjunto de posibles TurtleWorlds.

Para crear una tortuga, escribe:

>>> juan = Turtle()

En este caso, le asignamos el valor de retorno de Turtle a una variable llamada juan, para que podremos referirnos a ella más adelante (realmente no contamos con ninguna manera de referirnos a TurtleWorld).

Las funciones de dirección de la tortuga son fd y bk para ir hacia adelante y hacia atrás, y lt y rt para moverse hacia la izquierda y hacia la derecha.

Para dibujar un ángulo recto, escribe:

>>> fd (juan, 100)
>>> lt (juan)
>>> fd (juan, 100)

La primera línea (y tercera) le dice juan que ande 100 pasos adelante. La segunda línea, que gire a la derecha. En la ventana de TurtleWorld podrás ver a la tortuga moverse al este y luego al sur, dejando tras de sí dos líneas.

Antes de continuar, usa rt y bk para que la tortuga vuelva al punto de inicio.

4.2 Simple repetición

Si aún no lo has hecho, entra en el directorio que contiene TurtleWorld.py. Crea un archivo denominado polygon.py y escribe el código de la sección anterior:

from TurtleWorld import *
TurtleWorld ()
juan = Turtle ()

fd (juan, 100)
lt (juan)
fd (juan, 100)

Cuando ejecutes el programa, deberías ver a juan dibujar un ángulo recto, pero cuando el programa termine, la ventana desaparecerá. Añade la línea:

wait_for_user()

al final del programa, y ejecútalo de nuevo. Ahora, la ventana permanecerá hasta que la cierres.

Ahora modifica el programa para dibujar un cuadrado. ¡Y no sigas leyendo hasta que tu programa funcione!

Es muy probable que hayas escrito algo como esto (olvidándonos del código que crea TurtleWorld y espera a que el usuario interactúe):

fd (juan, 100)
lt (Bob)

fd (juan, 100)
lt (Bob)

fd (juan, 100)
lt (Bob)

fd (juan, 100)

Podemos hacer lo mismo con una declaración más concisa. Añade este ejemplo a polygon.py y ejecútalo de nuevo:

for i in range(4):
     print "¡Hola!"

Deberías ver algo como esto:

¡Hola!
¡Hola!
¡Hola!
¡Hola!

Este es el uso más simple de la declaración for, que veremos más adelante, pero debería ser suficiente para que seas capaz de reescribir tu programa para dibujar un cuadrado. No sigas leyendo hasta que lo logres.

He aquí una instrucción for que dibuja un cuadrado:

for i in range(4):
     fd (juan, 100)
     lt (juan)

La sintaxis de una instrucción for es similar a la definición de una función. Tiene una cabecera que termina con un signo ":", y el cuerpo, que se escribe sangrado, puede contener un número cualquiera de instrucciones de cualquier tipo.

A veces se llama bucle (o loop) a la instrucción for, puesto que el flujo de ejecución recorre el cuerpo del código para volver de nuevo al principio. En el caso anterior, se recorre el cuerpo cuatro veces.

Esta versión es en realidad un poco diferente del anterior código para dibujar un cuadrado, porque hace un último giro a la izquierda después de dibujar el último lado del cuadrado. Este giro de más emplea algo de tiempo, pero simplifica el código, ya que hacemos exactamente lo mismo en cada iteración del bucle. Otro efecto de esta versión es dejar a la tortuga en la posición inicial mirando en la dirección inicial.

4.3 Ejercicios

La siguiente serie de ejercicios utiliza TurtleWorld. Los ejercicios tienen el propósito de divertir, pero también tienen su puntillo. Mientras trabajes con ellos, piensa en ese puntillo.

En las próximas secciones encontrarás las soluciones a los ejercicios; no mires las soluciones hasta completar los ejercicios (o al menos hasta haberlo intentado).

  1. Escribe una función llamada square que tome un parámetro llamado t, que es una tortuga. La función debería utilizar la tortuga para dibujar un cuadrado.
    Escribe una llamada a la función square que pase juan como argumento y, a continuación, vuelve a ejecutar el programa.
  2. Añade otro parámetro, de nombre length, a square. Modifica el cuerpo de la función para que la longitud de los lados sea lenght, y, en consecuencia, modifica la llamada a la función para que pase un segundo argumento. Vuelve a ejecutar el programa. Prueba tu programa jugando con diferentes valores de lenght.
  3. Las funciones lt y rt giran 90 grados por defecto, pero puedes crear un segundo argumento que especifique el número de grados. Por ejemplo, lt(juan, 45) haría girar a juan 45 grados a la izquierda.
    Haz una copia de square con el nombre de polygon. Añade otro parámetro llamado n y modifica el cuerpo de la función para que dibuje un polígono regular de n lados. Sugerencia: Los ángulos de un polígono regular de n lados tienen 360,0 / n grados.
  4. Escribe una función llamada circle que reciba una tortuga t y un radio r como parámetros y dibuje algo parecido a un círculo invocando polygon con valores adecuados de longitud (length) y número de lados (n). Prueba la función con diferentes valores de r.
    Sugerencia: halla la longitud de la circunferencia (llamemos circumference a esta longitud) y asegúrate de que length * n = circumference.
    Otra sugerencia: si juan te parece demasiado lento, puedes alterar su velocidad modificando juan.delay, que es el tiempo que se emplea entre movimiento y movimiento, expresado en segundos. juan.delay = 0.01 debería darle vidilla.
  5. Haz una versión más general de circle llamada arc, que tome un parámetro adicional angle para determinar qué fracción de circulo se debe dibujar. angle se expresa en grados, por lo que angle = 360, arc debería ser equivalente a trazar un círculo completo.

4.4 Encapsulación

El primer ejercicio pide meter en una definición de función el código para dibujar un cuadrado y llamar a esta función, pasando la tortuga como argumento. Aquí está la solución:

def square(t):
    for i in range(4):
        fd(t, 100)
        lt(t)

square(juan)

Las instrucciones que encontramos en el interior de la función, fd y lt tienen doble sangría, puesto que están en el interior del bucle for, que se encuentra a su vez dentro de la definición de la función. La siguiente línea, square(juan), está a ras del margen izquierdo, de modo que ahí acaban tanto el bucle for como la definición de la función.

Dentro de la función, t se refiere a la misma tortuga a la que se refiere juan, de manera que lt(t) tiene el mismo efecto que lt(juan). Entonces, ¿por qué no llamar al parámetro juan? La idea es que la función puede trabajar con cualquier tortuga, y no sólo con la tortuga juan, de modo que podríamos crear una segunda tortuga y pasarla como parámetro a square:

square(juan)

pepe = Turtle()
square(pepe)

Encerrar un porción de código en una función se llama "encapsular". Una de las ventajas de la encapsulación es que asigna un nombre al código, lo cual no deja de ser una forma de documentarlo, es decir, de explicar para qué sirve. Otra ventaja es que permite reutilizar el código sin necesidad de copiarlo, sino, sencillamente, realizando una segunda llamada a la función.

4.5 Generalización

El siguiente paso consiste en añadir un parámetro lenght a square. Aquí está la solución:
def square(t, length):
    for i in range(4):
        fd(t, length)
        lt(t)

square(juan, 100)

La adición de un parámetro a una función se llama "generalización", ya que (perdón por la obviedad) hace a la función más general: en la versión anterior, el cuadrado es siempre del mismo tamaño; en la nueva versión, el cuadrado puede ser de cualquier tamaño.

El siguiente paso es también una generalización. En lugar de dibujar cuadrados, polygon dibuja polígonos regulares con cualquier número de lados. Aquí está la solución:

def polygon(t, length, n):
    angle = 360.0 / n
    for i in range(n):
        fd(t, length)
        lt(t, angle)

polygon(bob, 70, 7)

Este código dibuja un polígono de 7 lados de longitud 70. Si la llamada a la función tiene más de un par de argumentos numéricos, es fácil olvidar qué significa cada uno de ellos, o en qué orden hay que escribirlos. Es válido, y a veces útil, incluir los nombres de los parámetros en la lista de argumento

s:
polygon(bob, length=70, n=7)

Esta sintaxis hace que el programa se lea con mayor facilidad. Nos recuerda también como funcionan los argumentos y los parámetros: al llamar a una función, los argumentos se asignan a los parámetros.

4,6 Diseño de la intefaz

El siguiente paso consiste en programar circle, que toma un radio r como parámetro.

Una manera de hacerlo es copiar y modificar polygon. Presentamos una solución simple:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = 50
    length = circumference / n
    polygon(t, length, n)

La primera línea calcula la circunferencia de un círculo con radio r usando la fórmula 2 π r. Puesto que usmos math.pi, tenemos que importar math. Por convención, las declaraciones import se escriben por lo general al principio del script.

n es el número de segmentos en nuestra aproximación de un círculo, de modo que length es la longitud de cada segmento. Así, polygon dibuja un polígono de 50 lados, lo cual hace que la figura se aproxime a un círculo de radio r.

Una limitación de esta solución es que n es una constante, lo cual significa que para círculos muy grandes, los segmentos serán demasiado largos, y para los círculos pequeños, perderemos mucho tiempo dibujando segmentos demasiado pequeños. Una solución consistiría en generalizar la función tomando n como parámetro. Esto daría a los usuarios (es decir, a cualquiera que solicite dibujar un círculo) un mayor control, pero la interfaz sería menos clara.

La interfaz de una función es un resumen de cómo se utiliza: cuáles son los parámetros, qué hace la función de hacer y cuál es el valor de retorno. Una interfaz es "clara" (o "limpia"), si es "tan simple como sea posible, pero no más" (Einstein).

En este ejemplo, r pertenece a la interfaz, ya que especifica el círculo que hay que dibujar. n es menos adecuado, ya que se refiere a los detalles sobre cómo generar el círculo.

En lugar de recargar la interfaz, es mejor subordinar el valor de n a la circunferencia que queremos dibujar:

def circle(t, r):
    circumference = 2 * math.pi * r
    n = int(circumference / 4)
    length = circumference / n
    polygon(t, length, n)

Ahora el número de segmentos es (aproximadamente) circumference / 4, de modo que la longitud de cada segmento es (aproximadamente) 4, un número adecuado para cualquier tamaño de círculo: suficientemente pequeño para que cualquier círculo se vea bien, pero suficientemente grande para ser eficaz.

4,7 Refactorización

Al escribir el código de circle, hemos reutilizado polygon, puesto que un polígono de muchos lados se parece bastante a un círculo. Pero arc no se presta a ello; no podemos utilizar polygon o circle para dibujar un arco.

Una alternativa sería empezar con una copia del polygon y transformarla en arc. El resultado podría parecerse a esto:

def arc(t, r, angle):
    arclength = 2 * math.pi * r * angle / 360
    n = int(arclength / 4)
    length = arclength / n
    step_angle = angle / n
    
    for i in range(n):
        fd(t, length)
        lt(t, step_angle))

La segunda mitad de esta función se parece a polygon, pero no se puede volver a utilizar polygon sin cambiar la interfaz. Podríamos generalizar polígono para tomar un ángulo como tercer argumento, pero entonces polígono dejaría de ser un nombre apropiado. En lugar de eso, llamemos polyline a una función más general:

def polyline(t, length, n, angle):
    for i in range(n):
        fd(t, length)
        lt(t, angle)

Ahora podemos reescribir polygon y arc para reutilizar polyline:

def polygon(t, length, n):
    angle = 360.0 / n
    polyline(t, length, n, angle)

def arc(t, r, angle):
    arclength = 2 * math.pi * r * angle / 360
    n = int(arclength / 4)
    length = arclength / n
    polyline(t, length, n, angle/n)

Por último, podemos reescribir circle para que utilice arc:

def circle(t, r):
    arc(t, r, 360.0))

Este proceso de reorganización del código de un programa para mejorar las interfaces de las funciones y facilitar la reutilización de código, se llama refactorización. En este caso, hemos observado que hay similitudes entre el código de arc y el de polygon, de modo que lo hemos refactorizado en polyline.

Si haberlo planeado antes, podríamos haber escrito polyline primero y evitar refactorizar, pero a menudo no se sabe lo suficiente al inicio de un proyecto para deseñar todas las interfaces. Cuando se inicie el proceso de codificación, se entendrá mejor el problema. A veces, refactorizar es la prueba de que se ha empezado a entender lo que se está haciendo.

4.8 Un plan de desarrollo

Un plan de desarrollo es un proceso para escribir programas. El proceso que hemos utilizado en este caso es lo que llamamos EGR, siglas de "encapsulación, generalización y refactorización". Las pasos delEGR son:

  1. Empezar a escribir un pequeño programa sin definiciones de función.
  2. En cuanto el programa funcione, se encapsula en una función y se le da un nombre.
  3. Se generaliza la función añadiendo los parámetros adecuados.
  4. Se repiten los tres primeros pasos hasta obtener un conjunto de funciones válidas. Se copia y pega el código correcto para evitar reescribir (y volver a depurar).
  5. Se buscan oportunidades para mejorar el programa mediante refactorización. Por ejemplo, si existe código similar en varios puntos del programa, se considera la posibilidad de refactorizarlo mediante una función general.

El EGR tiene algunos inconvenientes (presentaremos alternativas más adelante), pero puede ser útil si no sabe de antemano cómo dividir el programa en funciones. Este enfoque permite diseñar a medida que se avanza.

4.9 docstring

Un docstring es una cadena al principio de una función que explica la interfaz ("doc" es la abreviatura de "documentación"). He aquí un ejemplo:

def polyline(t, length, n, angle):
    """Dibuja n segmentos de longitud (length) dada y
    un ángulo entre ellos (angle) expresado en grados.  t is una tortuga.
    """    
    for i in range(n):
        fd(t, length)
        lt(t, angle)

Este docstring es una cadena con triples comillas, también conocida como una cadena de múltiples líneas, porque las triples comillas permiten a la cadena abarcar más de una línea.

Es sencillo, pero contiene la información esencial necesaria para utilizar la función. En él se explican concisamente qué necesita la función para hacer su trabajo (sin entrar en los detalles sobre cómo lo hace). Explica qué efecto tiene cada parámetro en el comportamiento de la función y el tipo de cada parámetro (siempre que no sea evidente).

Generar este tipo de documentación es una parte importante del diseño de interfaces. Una interfaz bien diseñada debe ser fácil de explicar; si existen dificultades para explicar una de las funciones, es muy probablemente que la función sea susceptible de mejora.

4.10 Glosario

instancia: un miembro de un conjunto. La TurtleWorld de este capítulo es miembro del conjunto de TurtleWorlds.

bucle: un declaración compuesta cuyo cuerpo puede ser ejecutado más de una vez.

encapsulación: el proceso de transformación de una secuencia de declaraciones en una definición de función.

generalización: el proceso de sustitución de algo innecesariamente específico (como un número) por algo adecuadamente general (como una variable o un parámetro).

interfaz: una descripción de cómo usar una función, incluyendo el nombre, la descripción de los argumentos y el valor de retorno. plan de desarrollo: un proceso para escribir program

as.

docstring: una cadena que aparece en la definición de una función, cuyo propósito es documentar la interfaz de la función.

4.11 Ejercicios

  1. Escribe docstrings adecuados para el polígono y el círculo.
  2. Dibuja una diagrama de pila que muestre el estado del programa durante la ejecución de circle(juan, 100). Puedes hacer los cálculos a mano o puedes añadir instrucciones print al código.
  3. Escribe un conjunto "adecuadamente" general de funciones para dibujar flores de este tipo:
  4. Escribe un conjunto "adecuadamente" general de funciones para dibujar formas de este tipo:
Manuales | Tesis: Ordenadores, Circuitos integrados...
english
Cibernetia