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:
- Una línea de encabezado que comienza con una palabra clave y termina con dos puntos
- 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
- La lista de parámetros puede estar vacía o tener uno o varios separados por comas (en todos los casos se requieren los paréntesis)
- Los parámetros especifican qué información (de ser necesaria) se provee a la función para su ejecución
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.
- Por ejemplo, si escribimos el nombre de la función con un paréntesis abierto, una tooltip nos mostrará qué argumentos tiene la función así como el texto contenido en el docstring
- Los Docstrings son fundamentales para documentar nuestra función. Porque a quien llama la función no le interesa cómo funciona por dentro, pero sí qué hace, qué devuelve y qué parámetros requiere
- (es decir, lo suficiente como para poder usar la función sin tener que mirar por dentro - corresponde al concepto de abstracción que veremos más adelante)
- Los Docstrings se escriben entre triples comillas
- Diferencia con los comentarios: el Docstring se puede obtener durante el tiempo de ejecución (mediante herramientas de Python), mientras que los comentarios son completamente ajenos a la ejecució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:
- Las funciones pueden llamar a otras funciones
- Al escribir dibujar_cuadrado de este modo aprovechamos la relación existente entre rectángulos y cuadrados
- Quien llame a la función dibujar_cuadrado habrá de pasarle dos parámetros (una tortuga y un entero que indica el tamaño del cuadrado)
- En el cuerpo de la función los parámetros se tratan como cualquier otra variable
- Cuando se llama a dibujar_rectangulo, los argumentos t, sz, sz del llamado se ejecutan en el lugar de los parámetros t, w, h de la función llamada
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:
- 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).
- 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 ejecuciones siempre comienzan en la primera línea del programa. Se ejecutan de a uno, en orden de arriba a abajo (top to bottom).
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.
- Importante: como "ejecutar" una definición es guardar la definición de la función para su uso posterior, es perfectamente posible que una función B que llama a otra A sea definida antes de la definición de A, siempre y cuando el llamado efectivo a B no ocurra sino hasta después de que ambas hayan sido definidas. Se puede por ejemplo definir B (que llama a A), luego definir A y luego llamar a B. En cambio, no se podría definir B (que llama a A), luego llamar a B y más tarde definir A, porque A no estaría definida para el momento en que B intenta llamarla. También se puede definir una A que llame a B y una B que llame a A, si el primer llamado a cualquiera de las dos ocurre después de ambas definiciones.
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.
- Lo anterior suena simple, hasta que recordamos que una función puede llamar a otra. Y ésta a otra, y ésta a otra, etc. Por lo cual el flujo puede ir saltando de función en función de un modo que no es obvio de predecir de antemano.
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- C = capital inicial; r = tasa nominal anual en porcentaje
- n = cantidad de veces que se capitaliza por año; t = cantidad de años
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- C = capital inicial; r = tasa nominal anual como valor decimal
- n = cantidad de veces que se capitaliza por año; t = cantidad de años
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:
- Tras el return hay una expresión (en este caso la variable m en que computamos el monto). Esta expresión será evaluada y retornada al que llama, como el "fruto" devuelto por la función.
- Le preguntamos al usuario cuál es el capital que invertirá. El tipo devuelto por la función input es un string, el cual convertimos a un float para poder trabajar con él (numéricamente)
- Observar que el interés es del 8%, el interés es acumulable 12 veces por año (mensual) y el plazo de la inversión es de 5 años.
- Al final obtenemos un mensaje que muestra muchas cifras decimales, porque el programa no entiende que estamos trabajando con dinero y sólo devuelve un número a ciegas
- Más adelante veremos cómo formatear el monto final para que se vea con dos cifras decimales, como es habitual
- La línea
capital = float(input("Cuánto quieres invertir?"))
es otro ejemplo de composición: podemos hacer que el valor devuelto por la función input sea utilizado como argumento por la función float.
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)
4.8) Glosario
- argumento, cuerpo (es la segunda parte de una sentencia compuesta: puede consistir en varias sentencias indentadas respecto al header; por defecto se indenta con 4 espacios)
- sentencia compuesta (una sentencia que consiste en 2 partes: header y body o cabeza y cuerpo), header (primera parte de la sentencia compuesta: comienza con una palabra clave y termina con dos puntos)
- docstring (un string especial que se asocia a una función como su atributo __doc__ - herramientas como PyScripter los usan para proveer documentación y consejos al programador)
- (veremos más adelante que también pueden utilizarse al definir módulos, clases y métodos)
- flujo de ejecución, marco (frame: una caja en un diagrama que representa las variables locales y parámetros de una función), función, llamado a función, composición de funciones
- definición de función, función fructífera, función void, sentencia import, tiempo de vida (las variables y objetos tienen tiempos de vida: se crean en un punto del flujo de ejecución y se destruyen en otro)
- variable local, parámetro, refactorización, stack diagram (representación gráfica de una pila de funciones, sus variables, y los valores a los que se refieren - el concepto no fue dado en el capítulo)
- traceback (lista de funciones que se estaban ejecutando en el momento en que ocurre un error en tiempo de ejecución
- también se conoce como stack trace, Escribamos un primer programa, con una tortuga a la que llamaremos "alex", ya que lista las funciones en el orden en que fueron guardadas en el runtime stack)
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)
- Pregunta: ¿qué pasaría si no levantáramos el lápiz?