Cap. 4 - Funciones

4.1) Funciones

En Python, una función es una secuencia de instrucciones que van juntas y tienen un nombre. Su principal objetivo es permitir organizar programas en trozos que corresponden a cómo pensamos sobre el problema.

La sintaxis para definir una función es:

	def NOMBRE( PARAMETROS ):
	    SENTENCIAS

Podemos utilizar cualquier nombre para una función, siempre que no sea una palabra reservada de Python, y que el nombre siga las reglas para identificadores legales.

Puede haber cualquier cantidad de sentencias en una función, pero deben estar indentadas respecto al def. En los ejemplos de este libro utilizaremos la indentación estándar de 4 espacios.

Las definiciones de funciones son el segundo ejemplo que estaremos viendo de sentencias compuestas, (el primero fue el bucle for) las cuales tienen todas el mismo patrón:

  1. Una línea de encabezado que comienza con una palabra clave y termina con dos puntos
  2. Un cuerpo (body) que consiste en una o varias sentencias Python, todas indentadas con la misma cantidad de espacios respecto a la línea de encabezado
    • (la Guía de Estilo para Python recomienda 4 espacios)

Mirando a la definición de la función, vemos que el encabezado comienza con la palabra clave def, seguida por el nombre de la función y parámetros entre paréntesis

Como ejemplo, definamos la función "dibujar_cuadrado" que dibuje un cuadrado utilizando una tortuga: (L4_dibujar_cuadrado)

import turtle

def dibujar_cuadrado(t, sz):
    """Dibujar con la tortuga t un cuadrado de lado sz"""
    for i in range(4):
        t.forward(sz)
        t.left(90)

wn = turtle.Screen()        #Setear la ventana y sus atributos
wn.bgcolor("lightgreen")
wn.title("Alex se topa con una función")

alex = turtle.Turtle()      #Crear a Alex
dibujar_cuadrado(alex, 50)  #Llamar a la función que dibuja el cuadrado
wn.mainloop()

La función se llama dibujar_cuadrado. Tiene 2 parámetros: el primero le dice con qué tortuga dibujar, y el segundo el tamaño del cuadrado que queremos dibujar. Dónde termina la función queda determinado por la indentación (y no tiene nada que ver con los espacios en blanco).

La primera línea de la función es un Docstring. Éstos se usan de un modo especial en Python y en ciertas herramientas de programación.

La definición de la función no hace que ésta se ejecute. Hay que llamar a la función para que esto ocurre. Ya vimos cómo llamar a ciertas funciones built-in como print, range e int. El llamado a la función contiene el nombre de la función seguido por una lista de valores, llamados argumentos, que son asignados a los parámetros de la definición de la función. Así, en la penúltima línea del programa pasamos a Alex como la tortuga a ser manipulada por la función, y 50 como el tamaño del cuadrado a dibujar. Mientras se ejecuta la función, sz se referirá al valor 50 y t a la instancia de tortuga a que se refiere al palabra Alex.

Una vez definida una función, podemos llamarla tantas veces como queramos, y se ejecutarán sus sentencias cada vez que lo hagamos. En el siguiente ejemplo, agregamos un par de detalles a la función dibujar_cuadrado y le pedimos a Tess que dibuje 15 cuadrados, como algunas variantes. (L4_dibujar_cuadrados)

import turtle

def dibujar_cuadrado_multicolor(t, sz):
    """Dibujar con la tortuga t un cuadrado multicolor de lado sz"""
    for i in ["red", "purple", "hotpink", "blue"]:
        t.color(i)
        t.forward(sz)
        t.left(90)

wn = turtle.Screen()        #Setear la ventana y sus atributos
wn.bgcolor("lightgreen")
wn.title("Tess dibuja 15 cuadrados multicolores")

tess = turtle.Turtle()      #Crear a Tess
tess.pensize(3)

size = 20                   #Tamaño del menor cuadrado
for i in range(15):
    dibujar_cuadrado_multicolor(tess, size)
    size = size + 10        #Incrementar el tamaño del cuadrado a cada paso
    tess.forward(10)        #Mover a Tess después de dibujar el cuadrado
    tess.right(18)          #Y también rotar a Tess

wn.mainloop()

4.2) Las funciones pueden llamar a otras funciones

Supongamos que ahora queremos una función que dibuje un rectángulo. Necesitamos poder pasarle como argumentos el ancho y el alto. Y no podemos repetir el mismo paso 4 veces, como hacíamos para el cuadrado, porque los lados son distintos dos a dos.

Por lo tanto, desarrollamos este código para dibujar un rectángulo: (L4_dibujar_rectangulo_cuadrado)

		def dibujar_rectangulo(t, w, h):
		    """Dibujar con la tortuga t un rectángulo de ancho w y alto h."""
		    for i in range(2):
		        t.forward(w)
		        t.left(90)
		        t.forward(h)
		        t.left(90)

Los parámetros son de sólo una letra para evitar una confusión: una vez que desarrollemos programas con más fluidez, utilizaremos nombres más expresivos. Pero aquí queremos dejar claro que el programa no "entiende" que estamos dibujando un rectángulo, o que los parámetros representan ancho y alto. Conceptos como rectángulo, ancho y alto tienen significado para los humanos, no son conceptos que el programa o la computadora estén comprendiendo.

Pensar como un programador requiere visualizar los patrones y relaciones. En el código anterior, lo hicimos en parte: no dibujamos 4 lados uno tras otro (lo cual habría sido la solución más obvia), sino que comprendimos que se puede dibujar al rectángulo como dos mitades exactamente similares, y utilizamos un loop for para repetir dicho patrón dos veces.

En este momento podemos utilizar el hecho de que un cuadrado es un tipo particular de rectángulo, por lo cual podemos usar la función que dibuja rectángulos para dibujar un cuadrado, así:

		def dibujar_cuadrado(t, sz):
		    """Dibujar con la tortuga t un cuadrado de lado sz."""
		    dibujar_rectangulo(t, sz, sz)

Algunas observaciones importantes:

A estas alturas podría no ser claro cuál es la utilidad de definir estas funciones. Sin embargo, al menos 2 razones quedan pueden comprenderse ya mismo:

  1. Crear una nueva función nos da la oportunidad de asignar un nombre a un grupo de sentencias. Las funciones permiten simplificar un programa ocultando muchas computaciones detrás de un simple nombre. Y la función (incluyendo su nombre) puede sacar ventaja de nuestra "clasificación mental" del problema en partes (si pensamos en "dibujar un rectángulo", entonces es buena idea crear una función que haga exactamente eso mismo).
  2. Crear una nueva función puede reducir el tamaño de un programa, ya que elimina código repetitivo.

Como es obvio, uno tiene que crear la función antes de ejecutarla o llamarla. No se puede llamar a una función que aun no se ha definido. Por lo tanto, la definición de la función (la línea "def") debe estar antes del llamado a la función.

4.3) Flujo de ejecución

Se llama flujo de ejecución al orden en que se ejecutan las sentencias (ya hablamos un poco de esto en el capítulo anterior).

Las definiciones de funciones no alteran el flujo de ejecución del programa, pero cabe recordar que las sentencias en el cuerpo de la función no se ejecutan hasta que es llamada la función. No es frecuente, pero se puede definir una función dentro de otra: en este caso, la definición de la interna no se ejecuta sino cuando la función externa es llamada.

Los llamados a función son como un desvío en el flujo de ejecución. En vez de ir hacia la sentencia siguiente, el flujo salta a la primera línea de la función llamada, ejecuta todas sus sentencias y luego vuelve y continúa por la línea siguiente al llamado.

La moraleja: para leer un programa no hay que leer del principio al final, sino tratar de seguir el flujo de ejecución tal y como ocurrirá al ser ejecutado.

En PyScripter se puede observar el flujo de ejecución, porque tiene un modo en que resalta la siguiente línea a ser ejecutada.

También en PyScripter si ponemos el mouse sobre una variable, se nos mostrará el valor actual de la variable (en una ventana pop-up)

  • Lo cual facilita inspeccionar el "state snapshot" del programa en tiempo real

Es conveniente aprender a utilizar esta herramienta para comprender cómo es que avanza el flujo de ejecución en un típico programa con funciones

Dos preguntas que uno debería acostumbrarse a hacerse y a ser capaz de responder de antemano son:

  • ¿Qué efecto tendrá esta línea sobre las variables del programa, y sobre cuáles?
  • ¿A dónde va a ir luego el flujo de ejecución?

Veamos esto funcionando con el programa que dibujaba 15 rectángulos multicolores. Primero, agregaremos una línea __import__ que evitará que se haga el tracking dentro del módulo "turtle" (algo que no queremos hacer porque sólo nos interesa hacer el tracking de nuestro código). (L4_dibujar_cuadrados - con tracking)

		import turtle
		__import__("turtle").__traceable__ = False

(Nota: en mis tests no funcionó bien la instrucción __traceable__ = False - porque con el F7 se hace el trace de todas las funciones internas del módulo turtle, o tal vez sea que está haciendo el trace de funciones de otros módulo que son llamados por éste, pero interpreto que no debería pasar: revisarlo con tiempo y averiguar cómo corregir ese detalle)

Ahora podemos comenzar. Poner el mouse en la línea del programa en que creamos la ventana para la tortuga, y presionar F4 (menú Run > Run to cursor). Esto ejecutará el programa hasta la línea anterior a la señalada. Nuestro programa se corta (hace un "break") y nos muestra un highlight de la próxima línea a ser ejecutada.

En este punto podemos presionar F7 (menú Run > Step into) repetidamente para avanzar paso a paso en el código. Podemos ver cómo a medida que avanzamos se va creando la ventana de la tortuga, se cambia el color del canvas, cambia el título, la tortuga es creada dentro del canvas, el flujo de ejecución entra en el loop, y de ahí dentro de la función que dibuja el cuadrado multicolor, y de ahí dentro del loop de dicha función, y así sucesivamente a través del cuerpo (body) del loop.

En cualquier momento podemos hacer un mouse-over sobre cuaquiera de las variables del programa para observar su valor actual.

Luego de algunos loops, y antes de aburrirnos con tantas repeticiones podemos presionar F8 (menú Run > Step over) para salir de la función que estamos llamando. Esto ejecuta todas las instrucciones de dicha función, pero sin necesidad de ir una por una. Siempre podemos "ir por el detalle" o "ir por la imagen general".

Hay otras opciones, incluida una que nos permite retomar la ejecución normal sin ir más paso a paso. Se encuentran todas en el menú Run de PyScripter.

4.4) Funciones que requieren argumentos

La mayor parte de las funciones requieren argumentos, los cuales permiten escribir funciones generales. Por ejemplo, si queremos encontrar el valor absoluto de un número, debemos indicar cuál es el número. Python ya incluye la función abs que computa el valor absoluto:

		>>> abs(5)
		5
		>>> abs(-5)
		5

En este ejemplo, los argumentos pasados a abs son 5 y -5.

Algunas funciones reciben más de un argumento. Por ejemplo la función pow de Python recibe dos argumentos, uno para la base y otro para el exponente. Dentro de la función, los valores pasados son asignados a las variables que llamamos parámetros. Ejemplo:

		>>> pow(2, 3)
		8
		>>> pow(7, 4)
		2401

Es interesante observar que pow(-1, 1/2) devuelve i (bajo la forma de j) pero con una parte real que es del orden de 10^-17, lo que sugiere que la calculadora de Python no es demasiado confiable. Confirmarlo, o comprender cuál es la causa de este problema con las potencias que deberían dar resultados imaginarios.

Otra función de Python que toma más de un argumento es max:

		>>> max(7, 11)
		11
		>>> max(4, 1, 17, 2, 12)
		17
		>>> max(3 * 11, 5**3, 512 - 9, 1024**0)
		503

A max se le puede pasar cualquier cantidad de argumentos, los cuales pueden ser simples valores o expresiones.

4.5) Funciones que retornan valores

Todas las funciones en la sección previa retornan valores. Incluso, funciones como range, int o abs retornan valores que pueden utilizarse como parte de expresiones complejas.

Una diferencia importante entre esas funciones y las funciones como dibujar_cuadrado es que esta última no fue ejecutada porque queríamos computar un valor: al contrario, la implementamos para que ejecutara una secuencia de pasos que provocaran que la tortuga completara un dibujo.

Una función que retorna un valor se llamará función fructífera en este curso. El concepto opuesto es una función void (una que no se ejecuta por el valor que devuelve, sino porque hace algo útil). Lenguajes como Java, C#, C o C++ también usan el término función void para referirse a éstas, mientras que en otros lenguajes, como Pascal, son llamadas procedimientos. Incluso aunque las funciones void no se utilicen por su valor de retorno, Python siempre quiere retornar algo en cada llamado a función. Por lo tanto, si el programador no se encarga de retornar un valor, Python retornará automáticamente el valor None.

Cómo podemos escribir nuestra propia función fructífera? En los ejercicios al final del capítulo 2 vimos la fórmula estándar para el interés compuesto, la que ahora implementaremos como una función fructífera.

M = C(1 + r/100n)^nt

Si consideramos que la tasa ya es dada como un decimal (es decir, en vez de ser r = 30% viene dada como r = 0.3), entonces tenemos esta versión de la misma fórmula:

M = C(1 + r/n)^nt

La fórmula anterior nos permite escribir el siguiente programa: (L4_interes_compuesto)

def monto_final(c, r, n, t):
    """Aplicar la fórmula del interés compuesto al capital c para obtener el monto final"""

    m = c * (1 + r/n) ** (n*t)
    return m

#Ahora que tenemos la función definida, llamémosla
capital = float(input("Cuánto quieres invertir?"))
montoFinal = monto_final(capital, 0.08, 12, 5)
print("Al final del período tendrás ", montoFinal)

Algunas observaciones:

Observar también este detalle importante: el nombre de la variable que pasamos como argumento (capital) no tiene nada que ver con el nombre del parámetro (c). No importa qué nombre le demos al argumento al llamar la función, ésta lo convertirá internamente en el nombre del argumento correspondiente (en este ejemplo, ese nombre es c) y lo utilizará así.

La función podría haberse escrito con nombres de argumentos más claros. Lo que siguen son dos versiones alternativas posibles: la primera, "v2", utiliza nombres realmente largos y descriptivos. Y la segunda, "v3", utiliza nombres no tan largos pero igualmente fáciles de comprender. Hay que usar el sentido común para decidir entre dos alternativas muchas veces contradictorias: por un lado que el nombre no sea muy largo y por el otro que su significado sea suficientemente explícito como para evitar confusiones. (L4_interes_compuesto)

def monto_final(c, r, n, t):
    """Aplicar la fórmula del interés compuesto al capital c para obtener el monto final"""

    m = c * (1 + r/n) ** (n*t)
    return m

def monto_final_v2(capitalInicial, tasaNominalEnDecimales, capitalizacionesAnuales, cantidadDeAños):
    m = capitalInicial * (1 + tasaNominalEnDecimales/capitalizacionesAnuales) ** (capitalizacionesAnuales * cantidadDeAños)
    return m

def monto_final_v3(cap, tasa, porAño, años):
    m = cap * (1 + tasa/porAño) ** (porAño * años)
    return m

#Ahora que tenemos la función definida, llamémosla
capital = float(input("Cuánto quieres invertir?"))
montoFinal = monto_final(capital, 0.08, 12, 5)
montoFinal_v2 = monto_final_v2(capital, 0.08, 12, 5)
montoFinal_v3 = monto_final_v3(capital, 0.08, 12, 5)
print("Al final del período tendrás ", montoFinal)
print("Al final del período tendrás (versión 2) ", montoFinal_v2)
print("Al final del período tendrás (versión 3) ", montoFinal_v3)

4.6) Las variables y parámetros son locales

Cuando creamos una variable local dentro de una función, sólo existe dentro de esa función, y no la podemos utilizar afuera. Por ejemplo, consideremos otra vez esta función:

		def monto_final(c, r, n, t):
		    """Aplicar la fórmula del interés compuesto al capital c para obtener el monto final"""

		    m = c * (1 + r/n) ** (n*t)
		    return m

Si intentamos utilizar m fuera de la función, obtenemos un error:

		>>> m
		NameError: name 'm' is not defined

La variable m es local a la función monto_final y no es visible fuera de la función.

Adicionalmente m sólo existe mientras la función se está ejecutando (su tiempo de vida o lifetime). Cuando la ejecución de la función termina, las variables locales son destruidas.

Los parámetros también son locales, y actúan como variables locales. Por ejemplo, el tiempo de vida de c, r, n, t empieza en el momento en que monto_final es llamado y termina cuando la función completa su ejecución.

Por lo tanto, una función no puede setear una variable local a cierto valor, completar su ejecución, y más adelante cuando vuelve a ser llamada, recuperar el valor de la variable. Cada llamado a la función crea nuevas variables locales, y sus tiempos de vida terminan cuando la función devuelve su valor de retorno.

4.7) Tortugas revisitadas

Ahora que tenemos funciones fructíferas podemos reorganizar nuestro código para que encaje mejor con nuestros esquemas mentales. A este proceso de reorganización se le llama refactoring (refactorización).

Dos cosas que siempre haremos al trabajar con tortugas es crear la ventana para la tortuga y crear una o más tortugas. Podemos escribir funciones para facilitar esas tareas a futuro: (L4_funciones_hacer_tortuga)

import turtle

def hacer_ventana(color, titulo):
    """
        Setea la ventana con el color de fondo y titulo dados como parámetros
        Devuelve la nueva ventana
    """

    w = turtle.Screen()
    w.bgcolor(color)
    w.title(titulo)
    return w

def hacer_tortuga(color, sz):
    """
        Setea una tortuga con el color y ancho de lápiz dados como parámetros
        Devuelve la nueva tortuga
    """

    t = turtle.Turtle()
    t.color(color)
    t.pensize(sz)
    return t

wn = hacer_ventana("lightgreen", "Tess y Alex danzando")
tess = hacer_tortuga("hotpink", 5)
alex = hacer_tortuga("black", 1)
dave = hacer_tortuga("yellow", 2)

La clave para hacer un buen refactoring es anticipar cuáles cosas van a ser necesarias cada vez que llamemos a la función: esas se convertirán en los parámetros (es decir, las partes que pueden cambiar).

4.8) Glosario

4.9) Ejercicios

1) Escribir una función void que dibuje un cuadrado. Usarla en un programa para dibujar la imagen que se muestra a continuación. Asumir que cada lado tiene 20 unidades (pista: observar que la tortuga se apartó del último cuadrado en el momento en que el programa terminó) (hacerlo)

2) Escribir un programa que haga este dibujo. Asumir que el cuadrado interior es de 20 unidades y que cada cuadrado sucesivo mide 20 unidades más. (hacerlo)

3) Escribir una función void dibujar_poly(t, n, sz) que haga que una tortuga dibuje un polígono. Cuando se llama con dibujar_poly(tess, 8, 50) dibuja una figura así: (hacerlo)

4) Dibujar este patrón: (hacerlo)

5) Estas dos espirales sólo difieren por el ángulo de giro: dibujar ambas: (hacerlo)

6) Escribir una función void dibujar_equitriangulo(t, sz) que llame a dibujar_poly de la pregunta anterior para hacer que la tortuga dibuje un triángulo equilátero. (hacerlo)

7) Escribir una función fructífera sumar_hasta(n) que devuelva la suma de los enteros hasta n. Así, sumar_hasta(10) devolvería 55. (hacerlo)

8) Escribir una función area_del_circulo(r) que devuelva el área de un círculo de radio r. (hacerlo)

9) Escribir una función void que dibuje una estrella de 5 puntas, en que el largo de cada lado sea de 100 unidades (pista: habría que rotar a la tortuga 144° en cada esquina) (hacerlo)

10) Extender el programa anterior así: dibujar 5 estrellas, pero entre cada una, tomar el lápiz, avanzar 350 unidades, rotar 144° hacia la derecha y apoyando el lápiz volver a dibujar otra estrella. De modo que se obtiene algo así: (hacerlo)