Capítulo 4. Funciones e interfaces

Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com

Este capítulo presenta un módulo llamado jupyturtle, que te permite crear dibujos sencillos dando instrucciones a una tortuga imaginaria. Utilizaremos este módulo para escribir funciones que dibujen cuadrados, polígonos y círculos, y para demostrar el diseño de interfaces, que es una forma de diseñar funciones que trabajen juntas.

El módulo jupyturtle

Para utilizar el módulo jupyturtle, podemos importarlo así:

import jupyturtle
       

Ahora podemos utilizar las funciones definidas en el módulo, como make_turtle y forward:

jupyturtle.make_turtle()
jupyturtle.forward(100)
       

make_turtle crea un lienzo, que es un espacio en la pantalla donde podemos dibujar, y una tortuga, que se representa con un caparazón circular y una cabeza triangular. El círculo muestra la ubicación de la tortuga y el triángulo indica la dirección hacia la que mira.

forward mueve la tortuga una distancia determinada en la dirección hacia la que mira, dibujando un segmento de línea por el camino. La distancia está en unidades arbitrarias; el tamaño real depende de la pantalla de tu ordenador.

Utilizaremos muchas veces funciones definidas en el módulo jupyturtle, por lo que estaría bien que no tuviéramos que escribir el nombre del módulo cada vez. Eso es posible si importamos el módulo así:

from jupyturtle import make_turtle, forward
       

Esta versión de la declaración de importación importa make_turtle y forward del módulo jupyturtle para que podamos llamarlos así:

make_turtle()
forward(100)
       

jupyturtle proporciona otras dos funciones que utilizaremos, llamadas left y right. Las importaremos de la siguiente manera:

from jupyturtle import left, right
       

left hace que la tortuga gire a la izquierda. Toma un argumento, que es el ángulo del giro en grados. Por ejemplo, podemos hacer un giro a la izquierda de 90 grados así:

make_turtle()
forward(50)
left(90)
forward(50)
       

Este programa mueve la tortuga hacia el este y luego hacia el norte, dejando dos segmentos de línea. Antes de continuar, comprueba si puedes modificar el programa para hacer un cuadrado.

Hacer un cuadrado

Aquí tienes una forma de hacer un cuadrado:

make_turtle()

forward(50)
left(90)

forward(50)
left(90)

forward(50)
left(90)

forward(50)
left(90)
       

Como este programa repite el mismo par de líneas cuatro veces, podemos hacer lo mismo de forma más concisa con un bucle for:

make_turtle()
for i in range(4):
    forward(50)
    left(90)
       

Encapsulación y generalización

Tomemos el código de trazado de cuadrados de la sección anterior y pongámoslo en una función llamada square:

def square():
    for i in range(4):
        forward(50)
        left(90)
       

Ahora podemos llamar a la función así

make_turtle()
square()
        

Envolver un trozo de código en una función se llama encapsulación. Una de las ventajas de la encapsulación es que asigna un nombre al código, que sirve como una especie de documentación. Otra ventaja es que, si reutilizas el código, ¡es más conciso llamar dos veces a una función que copiar y pegar el cuerpo!

En la versión actual, el tamaño del cuadrado es siempre 50. Si queremos dibujar cuadrados con tamaños diferentes, podemos tomar la longitud de los lados como parámetro:

def square(length):
    for i in range(4):
        forward(length)
        left(90)
        

Ahora podemos dibujar cuadrados de diferentes tamaños:

make_turtle()
square(30)
square(60)
        

Añadir un parámetro a una función se llama generalización porque hace que la función sea más general: con la versión anterior, el cuadrado siempre tiene el mismo tamaño; con esta versión puede tener cualquier tamaño.

Si añadimos otro parámetro, podemos hacerla aún más general. La siguiente función dibuja polígonos regulares con un número determinado de lados:

def polygon(n, length):
    angle = 360 / n
    for i in range(n):
        forward(length)
        left(angle)
        

En un polígono regular con n lados, el ángulo entre lados adyacentes es 360 / n grados.

El siguiente ejemplo dibuja un polígono de 7 lados con una longitud lateral de 30:

make_turtle()
polygon(7, 30)
        

Cuando una función tiene más de unos pocos argumentos numéricos, es fácil olvidar cuáles son o en qué orden deben ir. Puede ser una buena idea incluir los nombres de los parámetros en la lista de argumentos:

make_turtle()
polygon(n=7, length=30)
        

A veces se denominan "argumentos con nombre" porque incluyen los nombres de los parámetros. Pero en Python se llaman más a menudo argumentos de palabra clave (no confundir con palabras clave de Python como for y def).

Este uso del operador de asignación, =, es un recordatorio de cómo funcionan los argumentos y los parámetros: cuando llamas a una función, los argumentos se asignan a los parámetros.

Aproximación a un círculo

Supongamos ahora que queremos dibujar un círculo. Podemos hacerlo, aproximadamente, dibujando un polígono con un gran número de lados, de modo que cada lado sea lo suficientemente pequeño como para que sea difícil de ver. Aquí tienes una función que utiliza polygon para dibujar un polígono de 30 lados que se aproxima a un círculo:

import math

def circle(radius):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    polygon(n, length)
        

circle toma el radio del círculo como parámetro. Calcula circumference, que es la circunferencia de un círculo con el radio dado. n es el número de lados, por lo que circumference / n es la longitud de cada lado.

Esta función puede tardar mucho en ejecutarse. Podemos acelerarla llamando a m⁠a⁠k⁠e⁠_​t⁠u⁠r⁠t⁠l⁠e con un argumento de palabra clave llamado delay que establece el tiempo, en segundos, que la tortuga espera después de cada paso. El valor por defecto es 0.2 segundos; si lo fijamos en 0.02, se ejecutará unas 10 veces más rápido.

make_turtle(delay=0.02)
circle(30)
        

Una limitación de esta solución es que n es una constante, lo que significa que para círculos muy grandes, los lados son demasiado largos, y para círculos pequeños, perdemos tiempo dibujando lados muy cortos. Una opción es generalizar la función tomando n como parámetro. Pero de momento vamos a simplificarlo.

Refactorización

Ahora escribamos en una versión más general de circle, llamada arc, que toma un segundo parámetro, angle, y dibuja un arco de círculo que abarca el ángulo dado. Por ejemplo, si angle es 360 grados, dibuja un círculo completo. Si angle es 180 grados, dibuja medio círculo.

Para escribir circle, pudimos reutilizar polygon, porque un polígono de muchos lados es una buena aproximación a un círculo. Pero no podemos utilizar polygon para escribir arc.

En su lugar, crearemos la versión más general de polygon, llamada polyline:

def polyline(n, length, angle):
    for i in range(n):
        forward(length)
        left(angle)
        

polyline toma como parámetros el número de segmentos de línea a dibujar, n; la longitud de los segmentos, length; y el ángulo entre ellos, angle.

Ahora podemos reescribir polygon para utilizar polyline:

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

Y podemos utilizar polyline para escribir arc:

def arc(radius, angle):
    arc_length = 2 * math.pi * radius * angle / 360
    n = 30
    length = arc_length / n
    step_angle = angle / n
    polyline(n, length, step_angle)
        

arc es similar a circle, salvo que calcula arc_length, que es una fracción de la circunferencia de un círculo.

Por último, podemos reescribir circle para utilizar arc:

def circle(radius):
    arc(radius,  360)
        

Para comprobar que estas funciones funcionan como es debido, las utilizaremos para dibujar algo parecido a un caracol. Con delay=0, la tortuga corre lo más rápido posible.

make_turtle(delay=0)
polygon(n=20, length=9)
arc(radius=70, angle=70)
circle(radius=10)
        

En este ejemplo, partimos de un código que funcionaba y lo reorganizamos con diferentes funciones. Los cambios de este tipo, que mejoran el código sin cambiar su comportamiento, se denominan refactorización.

Si lo hubiéramos planificado con antelación, podríamos haber escrito primero polyline y evitar la refactorización, pero a menudo no sabes lo suficiente al principio de un proyecto para diseñar todas las funciones. Una vez que empiezas a codificar, comprendes mejor el problema. A veces, refactorizar es señal de que has aprendido algo.

Diagrama de pila

Cuando llamamos a circle, éste llama a arc, que a su vez llama a polyline. Podemos utilizar un diagrama de pila para mostrar esta secuencia de llamadas a funciones y los parámetros de cada una:

Observa que el valor de angle en polyline es diferente del valor de angle en arc. Los parámetros son locales, lo que significa que puedes utilizar el mismo nombre de parámetro en diferentes funciones; es una variable diferente en cada función, y puede referirse a un valor diferente.

Un plan de desarrollo

Un plan de desarrollo es un proceso para escribir programas. El proceso que utilizamos en este capítulo es "encapsulación y generalización". Los pasos de este proceso son

  1. Empieza escribiendo un pequeño programa sin definiciones de funciones.

  2. Una vez que consigas que el programa funcione, identifica un trozo coherente del mismo, encapsula el trozo en una función y dale un nombre. Copia y pega el código que funcione para evitar volver a escribirlo (y volver a depurarlo).

  3. Generaliza la función añadiendo los parámetros adecuados.

  4. Repite los pasos del 1 al 3 hasta que tengas un conjunto de funciones que funcionen.

  5. Busca oportunidades para mejorar el programa mediante la refactorización. Por ejemplo, si tienes código similar en varios lugares, considera la posibilidad de factorizarlo en una función adecuadamente general.

Este proceso tiene algunos inconvenientes -más adelante veremos alternativas-, pero puede ser útil si no sabes de antemano cómo dividir el programa en funciones. Este enfoque te permite diseñar sobre la marcha.

El diseño de una función tiene dos partes:

interfaz

Cómo se utiliza la función, incluyendo su nombre, los parámetros que toma y lo que se supone que debe hacer la función

aplicación

Cómo hace la función lo que se supone que debe hacer

Por ejemplo, aquí tienes la primera versión de circle que escribimos, que utiliza polygon:

def circle(radius):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    polygon(n, length)
        

Y aquí está la versión refactorizada que utiliza arc:

def circle(radius):
    arc(radius,  360)
        

Estas dos funciones tienen la misma interfaz -toman los mismos parámetros y hacen lo mismo-, pero tienen implementaciones diferentes.

Docstrings

Una 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(n, length, angle):
    """Draws line segments with the given length and angle between them.
            
    n: integer number of line segments
    length: length of the line segments
    angle: angle between segments (in degrees)
    """    
    for i in range(n):
        forward(length)
        left(angle)
        

Por convención de , las docstrings son cadenas entre comillas triples, también conocidas como cadenas multilínea porque las comillas triples permiten que la cadena abarque más de una línea.

Un docstring debe:

  • Explica de forma concisa lo que hace la función, sin entrar en los detalles de cómo funciona,

  • Explica qué efecto tiene cada parámetro en el comportamiento de la función, y

  • Indica de qué tipo debe ser cada parámetro, si no es obvio.

Escribir este tipo de documentación es una parte importante del diseño de la interfaz. Una interfaz bien diseñada debe ser sencilla de explicar; si te cuesta explicar una de tus funciones, tal vez la interfaz podría mejorarse.

Depurando

Una interfaz es como un contrato entre una función y un llamante. La persona que llama se compromete a proporcionar ciertos argumentos y la función se compromete a realizar cierto trabajo.

Por ejemplo, polyline requiere tres argumentos: n tiene que ser un número entero, length debe ser un número positivo y angle tiene que ser un número, que se entiende en grados.

Estos requisitos se llaman precondiciones porque se supone que deben ser verdaderos antes de que la función empiece a ejecutarse. Por el contrario, las condiciones al final de la función son postcondiciones. Las postcondiciones incluyen el efecto previsto de la función (como dibujar segmentos de línea) y cualquier efecto secundario (como mover la tortuga o realizar otros cambios).

Las precondiciones son responsabilidad de quien las invoca. Si el autor de la llamada incumple una condición previa y la función no funciona correctamente, el fallo está en el autor de la llamada, no en la función.

Si las precondiciones se cumplen y las postcondiciones no, el fallo está en la función. Si tus precondiciones y postcondiciones son claras, pueden ayudar a depurar.

Glosario

diseño de la interfaz: Proceso de diseño de la interfaz de una función, que incluye los parámetros que debe tomar.

Lienzo: Ventana utilizada para mostrar elementos gráficos, como líneas, círculos, rectángulos y otras formas.

encapsulación: El proceso de transformar una secuencia de sentencias en una definición de función.

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

argumento de palabra clave: Un argumento que incluye el nombre del parámetro.

refactorización: El proceso de modificar un programa en funcionamiento para mejorar las interfaces de las funciones y otras cualidades del código.

plan de desarrollo: Un proceso para redactar programas.

docstring: Cadena que aparece en la parte superior de la definición de una función para documentar su interfaz.

cadena multilínea: Cadena encerrada entre comillas triples que puede abarcar más de una línea de un programa.

precondición: Requisito que debe satisfacer el invocador antes de que se inicie una función.

postcondición: Requisito que debe satisfacer la función antes de que termine.

Ejercicios

Para estos ejercicios, hay algunas funciones más de tortuga que quizá quieras utilizar:

penup

Levanta el bolígrafo imaginario de la tortuga para que no deje rastro cuando se mueva.

pendown

Vuelve a dejar el bolígrafo.

La siguiente función utiliza penup y pendown para mover la tortuga sin dejar rastro:

from jupyturtle import penup, pendown

def jump(length):
    """Move forward length units without leaving a trail.
            
    Postcondition: Leaves the pen down.
    """
    penup()
    forward(length)
    pendown()
        

Ejercicio

Escribe una función llamada rectangle que dibuje un rectángulo con unas longitudes de lado dadas. Por ejemplo, aquí tienes un rectángulo que mide 80 unidades de ancho y 40 unidades de alto:

Ejercicio

Escribe una función llamada rhombus que dibuje un rombo con una longitud lateral y un ángulo interior dados. Por ejemplo, aquí tienes un rombo con una longitud lateral de 50 y un ángulo interior de 60 grados:

Ejercicio

Escribe ahora una función más general llamada parallelogram que dibuje un cuadrilátero con lados paralelos. Luego reescribe rectangle y rhombus para utilizar parallelogram.

Ejercicio

Escribe un conjunto de funciones adecuadamente generales que puedan dibujar formas como ésta.

Pista: escribe una función llamada triangle que dibuje un segmento triangular, y luego una función llamada draw_pie que utilice triangle.

Ejercicio

Escribe un conjunto de funciones adecuadamente generales que puedan dibujar flores como ésta.

Sugerencia: utiliza arc para escribir una función llamada petal que dibuje un pétalo de flor.

Pregunta a una Asistente Virtual

Varios módulos como jupyturtle en Python, y el que utilizamos en este capítulo, han sido personalizados para este libro. Así que si pides ayuda a un asistente virtual, no sabrá qué módulo utilizar. Pero si le das algunos ejemplos con los que trabajar, probablemente pueda averiguarlo. Por ejemplo, prueba con esta instrucción y comprueba si puede escribir una función que dibuje una espiral:

The following program uses a turtle graphics module to draw a circle:

from jupyturtle import make_turtle, forward, left
import math

def polygon(n, length):
    angle = 360 / n
    for i in range(n):
        forward(length)
        left(angle)
        
def circle(radius):
    circumference = 2 * math.pi * radius
    n = 30
    length = circumference / n
    polygon(n, length)
    
make_turtle(delay=0)
circle(30)

Write a function that draws a spiral.

Ten en cuenta que el resultado podría utilizar funciones que aún no hemos visto, y podría tener errores. Copia el código del asistente virtual y comprueba si puedes hacerlo funcionar. Si no has conseguido lo que querías, prueba a modificar el aviso.

Get Piensa en Python, 3ª Edición now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.