Next Up Previous Hi Index

Chapter 5

Funciones productivas

5.1 Valores de retorno

Algunas de las funciones internas que hemos usado, como las funciones math o funciones matemáticas, han producido resultados. Llamar a la función genera un nuevo valor, que normalmente asignamos a una variable pasa usar como parte de una expresión.

    import math

    e = math.exp(1.0)
    altura = radio * math.sin(angulo)

Pero hasta ahora, ninguna de las funciones que hemos escrito ha devuelto un valor.

En este capítulo escribiremos funciones que devuelvan valores, que llamaremos funciones productivas, a falta de un nombre mejor. El primer ejemplo es \texttt{area, que devuelve el área de un círculo con un radio dado: \begin{verbatim} import math def area(radio): temporal = math.pi * radio**2 return temporal \end{verbatim} Ya hemos visto antes la sentencia \texttt{return}, pero en una función productiva la sentencia \texttt{return} incluye un valor de retorno. Esta sentencia quiere decir {}``retorna inmediatamente de la función y usa la siguiente expresión como valor de retorno''. La expresión dada puede ser arbitrariamente complicada; así pues, podríamos haber escrito esta función más concisamente: \begin{verbatim} def area(radio): return math.pi * radio**2 \end{verbatim} Por otra parte, las \textbf{variables temporales} como \texttt{temporal} suelen hacer más fácil el depurado. \index{variables temporales} \index{temporales!variables} A veces es útil disponer de varias sentencias de retorno, una en cada rama de una condición: \begin{verbatim} def valorAbsoluto(x): if x < 0: return -x else: return x \end{verbatim} Puesto que estas sentencias \texttt{return} están en una condición alternativa, sólo se ejecutará una de ellas. En cuanto se ejecuta una de ellas, la función termina sin ejecutar ninguna de las sentencias siguientes. El código que aparece después de una sentencia \texttt{return} o en cualquier otro lugar donde el flujo de ejecución no pueda llegar, recibe el nombre de \textbf{código muerto}. \index{código muerto} En una función productiva es una buena idea asegurarse de que cualquier posible recorrido del programa alcanza una sentencia \texttt{return}. Por ejemplo: \begin{verbatim} def valorAbsoluto(x): if x < 0: return -x elif x > 0: return x \end{verbatim} Este programa no es correcto porque si resulta que \texttt{x} vale 0, entonces no se cumple ninguna de ambas condiciones y la función termina sin alcanzar la setencia \texttt{return}. En este caso, el valor de retorno es un valor especial llamado \textbf{None}: \index{None} \begin{verbatim} >>> print valorAbsoluto(0) None \end{verbatim} \begin{quote} \emph{Como actividad, escriba una función} \texttt{\emph{comparar}} \emph{que devuelva} \texttt{\emph{1}} \emph{si} \texttt{\emph{x > y}}\emph{,} \texttt{\emph{0}} \emph{si} \texttt{\emph{x == y}}\emph{, y} \texttt{\emph{-1}} \emph{si} \texttt{\emph{x < y}}\emph{.} \end{quote} \section{Desarrollo de programas} \label{program development} \index{andamiaje} Llegados a este punto, tendría que poder mirar a funciones Python completas y adivinar qué hacen. También, si ha hecho los ejercicios, habrá escrito algunas funcioncillas. Tal como vaya escribiendo funciones mayores puede empezar a experimentar más dificultades, especialmente con los errores en tiempo de ejecución y los semánticos. Para lidiar con programas de complejidad creciente, vamos a sugerirle una técnica que llamaremos \textbf{desarrollo incremental}. El objetivo del desarrollo incremental es sustituir largas sesiones de depuración por la adición y prueba de pequeñas porciones de código en cada vez. \index{desarrollo incremental} \index{métodos de desarrollo!incremental} Por ejemplo, supongamos que desea encontrar la distancia entre dos puntos, dados por las coordenadas \( (x_{1},y_{1}) \) y \( (x_{2},y_{2}) \). Por el teorema de Pitágoras, podemos escribir la distancia es: \begin{equation} distancia=\sqrt{(x_{2}-x_{1})^{2}+(y_{2}-y_{1})^{2}} \end{equation} El primer paso es considerar qué aspecto tendría una función \texttt{distancia} en Python. En otras palabras, ¿cuáles son las entradas (parámetros) y cuál es la salida (valor de retorno)? En este caso, los dos puntos son los parámetros, que podemos representar usando cuatro parámetros. El valor de retorno es la distancia, que es un valor en coma flotante. Ya podemos escribir un bosquejo de la función: \begin{verbatim} def distancia(x1, y1, x2, y2): return 0.0 \end{verbatim} Obviamente, la función no calcula distancias; siempre devuelve cero. Pero es sintácticamente correcta y se ejecutará, lo que significa que podemos probarla antes de complicarla más. Para comprobar la nueva función, tenemos que llamarla con valores de ejemplo: \begin{verbatim} >>> def distancia(x1, y1, x2, y2): ... return 0.0 ... >>> distancia(1, 2, 4, 6) 0.0 >>> \end{verbatim} Elegimos estos valores de tal forma que la distancia horizontal sea igual a 3 y la distancia vertical sea igual a 4; de esa manera el resultado es 5 (la hipotenusa del triángulo 3-4-5). Cuando se comprueba una función, es útil saber la respuesta correcta. Hasta el momento, hemos comprobado que la función es sintácticamente correcta, así que podemos empezar a añadir líneas de código. Después de cada cambio incremental, comprobamos de nuevo la función. Si en un momento dado aparece un error, sabremos dónde está exactamente: en la última línea que hayamos añadido. El siguiente paso en el cálculo es encontrar las diferencias entre \( x_{2}-x_{1} \) y \( y_{2}-y_{1} \). Almacenaremos dichos valores en variables temporales llamadas \texttt{dx} y \texttt{dy} y las imprimiremos. \begin{verbatim} def distancia(x1, y1, x2, y2): dx = x2 - x1 dy = y2 - y1 print "dx es", dx print "dy es", dy return 0.0 \end{verbatim} Si la función funciona, valga la redundancia, las salidas deberían ser 3 y 4. Si es así, sabemos que la función recibe correctamente los parámetros y realiza correctamente el primer cálculo. Si no, sólo hay unas pocas líneas que revisar. Ahora calculamos la suma de los cuadarados de \texttt{dx} y \texttt{dy}: \begin{verbatim} def distancia(x1, y1, x2, y2): dx = x2 - x1 dy = y2 - y1 dalcuadrado = dx**2 + dy**2 print "dalcuadrado es: ", dalcuadrado return 0.0 \end{verbatim} Fíjese en que hemos eliminado las sentencias \texttt{print} que escribimos en el paso anterior. Este código se llama \textbf{andamiaje} porque es útil para construir el programa pero no es parte del producto final. De nuevo querremos ejecutar el programa en este estado y comprobar la salida (que debería dar 25). Por último, si hemos importado el módulo math, podemos usar la función \texttt{sqrt} para calcular y devolver el resultado: \begin{verbatim} def distancia(x1, y1, x2, y2): dx = x2 - x1 dy = y2 - y1 dalcuadrado = dx**2 + dy**2 resultado = math.sqrt(dalcuadrado) return resultado \end{verbatim} Si esto funciona correctamente, ha terminado. Si no, podría ser que quisiera usted imprimir el valor de \texttt{resultado} antes de la sentencia return. Al principio, debería añadir solamente una o dos líneas de código cada vez. Conforme vaya ganando experiencia, puede que se encuentre escribiendo y depurando trozos mayores. Sin embargo, el proceso de desarrollo incremental puede ahorrarle mucho tiempo de depurado. Los aspectos fundamentales del proceso son: \begin{enumerate} \item Comience con un programa que funcione y hágale pequeños cambios incrementales. Si hay un error, sabrá exactamente dónde está. \item Use variables temporales para mantener valores intermedios, de tal manera que pueda mostrarlos por pantalla y comprobarlos. \item Una vez que el programa esté funcionando, tal vez prefiera eliminar parte del andamiaje o aglutinar múltiples sentencias en expresiones compuestas, pero sólo si eso no hace que el programa sea difícil de leer. \end{enumerate} \begin{quote} \emph{Como actividad, utilice el desarrollo incremental para escribir una función de nombre} \texttt{\emph{hipotenusa}} \emph{que devuelva la longitud de la hipotenusa de un triángulo rectángulo, dando como parámetros los dos catetos. Registre cada estado del desarrollo incremental según vaya avanzando.} \end{quote} \section{Composición} \index{composición} \index{funciones!composición} Como seguramente a estas alturas ya supondrá, se puede llamar a una función desde dentro de otra. Esta habilidad se llama \texttt{\emph{composición}}. Como ejemplo, escribiremos una función que tome dos puntos, el centro del círculo y un punto del perímetro, y calcule el área del círculo. Supongamos que el punto central está almacenado en las variables \texttt{xc} e \texttt{yc}, y que el punto del perímetro lo está en \texttt{xp} e \texttt{yp}. El primer paso es hallar el radio del círculo, que es la distancia entre los dos puntos. Afortunadamente hay una función, \texttt{distancia}, que realiza esta tarea: \begin{verbatim} radio = distancia(xc, yc, xp, yp) \end{verbatim} El segundo paso es encontrar el área de un círculo con ese radio y devolverla: \begin{verbatim} resultado = area(radio) return resultado \end{verbatim} Envolviendo todo esto en una función, obtenemos: \begin{verbatim} def area2(xc, yc, xp, yp): radio = distancia(xc, yc, xp, yp) resultado = area(radio) return resultado \end{verbatim} Hemos llamado a esta función \texttt{area2} para distinguirla de la función \texttt{area} definida anteriormente. Sólo puede haber una única función con un determinado nombre dentro de un módulo. Las variables temporales \texttt{radio} y \texttt{area} son útiles para el desarrollo y el depurado, pero una vez que el programa está funcionando, podemos hacerlo más conciso integrando las llamadas a las funciones en una sola línea: \begin{verbatim} def area2(xc, yc, xp, yp): return area(distancia(xc, yc, xp, yp)) \end{verbatim} \begin{quote} \emph{Como actividad, escriba una función} \texttt{\emph{pendiente(x1, y1, x2, y2)}} \emph{que devuelva la pendiente de la línea que atraviesa los puntos (x1, y1) y (x2, y2). Luego use esta función en una función que se llame} \texttt{\emph{intercepta(x1, y1, x2, y2)}} \emph{que devuelva la {[}{[}y-intercepta{]}{]} de la línea a través de los puntos (x1, y1) y (x2, y2).} \end{quote} \section{Funciones booleanas} \label{boolean} \index{funciones booleanas} \index{booleanas!funciones} Las funciones pueden devolver valores booleanos, lo que a menudo es conveniente para ocultar complicadas comprobaciones dentro de funciones. Por ejemplo: \begin{verbatim} def esDivisible(x, y): if x % y == 0: return 1 # it's true else: return 0 # it's false \end{verbatim} La función lleva por nombre \texttt{esDivisible}. Es habitual dar a las funciones booleanas nombres que suenan como preguntas sí/no. Devuelve \texttt{1} ó \texttt{0} para indicar si la \texttt{x} es o no divisibelo por \texttt{y}. Podemos reducir el tamaño de la función aprovechándonos del hecho de que la sentencia condicional que hay después del \texttt{if} es en sí misma una expresión booleana. Podemos devolverla directamente, evitando a la vez la sentencia \texttt{if:} \begin{verbatim} def esDivisible(x, y): return x % y == 0 \end{verbatim} La siguiente sesión muestra a la nueva función en acción: \begin{verbatim} >>> esDivisible(6, 4) 0 >>> esDivisible(6, 3) 1 \end{verbatim} El uso más común para las funciones booleanas es dentro de sentencias condicionales: \begin{verbatim} if esDivisible(x, y): print "x es divisible entre y" else: print "x no es divisible entre y" \end{verbatim} Puede parecer tentador escribir algo como: \beforeverb \begin{verbatim} if esDivisible(x, y) == 1: \end{verbatim} \afterverb % Pero la comparación extra es innecesaria. \begin{quote} \emph{Como actividad, escriba una función} \texttt{\emph{estaEntre(x, y, z)}} \emph{que devuelva} \texttt{\emph{1}} \emph{en caso de que} \texttt{\emph{y <= x <= z}} \emph{y que devuelva 0 en cualquier otro caso.} \end{quote} \section{Más recursividad} \index{recursividad} \index{lenguaje completo} \index{lenguaje!completo} \index{Turing, Alan} \index{tesis de Turing} Hasta ahora, usted ha aprendido solamente un pequeño subconjunto de Python, pero puede que le interese saber que ese subconjunto es ya un lenguaje de programación \emph{completo}; con esto queremos decir que cualquier cosa que pueda computarse se puede expresar en este lenguaje. Cualquier programa que se haya escrito alguna vez puede reescribirse utilizando únicamente las características del lenguaje que ha aprendido hasta el momento (de hecho, necesitaría algunas órdenes para controlar dispositivos como el teclado, el ratón, los discos, etc, pero eso es todo). Probar tal afirmación es un ejercicio nada trivial, completado por primera vez por Alan Turing, uno de los primeros científicos informáticos (algunos argumentarán que era un matemático, pero muchos de los científicos informáticos pioneros comenzaron como matemáticos). En correspondencia, se la conoce como la tesis de Turing. Si estudia un curso de Teoría de la Computación, tendrá oportunidad de ver la prueba. Para darle una idea de lo que puede hacer con las herramientas que ha aprendido hasta ahora, evaluaremos una serie de funciones matemáticas que se definen recursivamente. Una definición recursiva es semejante a una definición circular, en el sentido de que la definición contiene una referencia a lo que se define. Una definición verdaderamente circular no es muy útil: \begin{description} \item [frangoso:] adjetivo que describe algo que es frangoso \end{description} \index{frangoso} \index{definición circular} \index{definición!circular} Si usted viera esa definición en el diccionario, se quedaría confuso. Por otra parte, si ha buscado la definición de la función matemática factorial, habrá visto algo sejemante a lo siguiente: \begin{eqnarray*} & & 0!=1\\ & & n!=n\cdot (n-1)! \end{eqnarray*} Esta definición establece que el factoral de 0 es 1, y que el factorial de cualquier otro valor, \( n \), es \( n \) multiplicado por el factorial de \( n-1 \). Así pues, \( 3! \) es 3 veces \( 2! \), que es 2 veces \( 1! \), que es una vez \( 0! \). Juntándolos todos, , \( 3! \) es igual a 3 veces 2 veces 1 vez 1, que es 6. \index{función factorial} \index{función!factorial} Si puede escribir una definición recursiva de algo, normalmente podrá escribir un programa de Python para evaluarlo. El primer paso es decidir cuáles son los parámetros para esta función. Con poco esfuerzo llegará a la conclusión de que \texttt{factorial} toma un único parámetro: \begin{verbatim} def factorial(n): \end{verbatim} Si resultase que el argumento fuese 0, todo lo que hemos de hacer es devolver 1: \begin{verbatim} def factorial(n): if n == 0: return 1 \end{verbatim} En otro caso, y he aquí la parte interesante, tenemos que hacer una llamada recursiva para hallar el factorial de \( n-1 \) y luego multiplicarlo por \( n \): \begin{verbatim} def factorial(n): if n == 0: return 1 else: recursivo = factorial(n-1) resultado = n * recursivo return resultado \end{verbatim} El flujo de ejecución de este programa es similar al de \texttt{cuenta\_atras} de la Sección~\ref{recursion}. Si llamamos a \texttt{factorial} con el valor 3: Puesto que 3 no es 0, tomamos la segunda rama y calculamos el factorial de \texttt{n-1}... \begin{quote} Puesto que 2 no es 0, tomamos la segunda rama y calculamos el factorial de \texttt{n-1}... \begin{quote} Puesto que 1 no es 0, tomamos la segunda rama y calculamos el factorial de \texttt{n-1}... \begin{quote} Puesto que 0 \emph{es} 0, tomamos la primera rama y devolvemos el valor 1 sin hacer más llamadas recursivas. \end{quote} El valor de retorno (1) se multiplica por \( n \), que es 1, y se devuelve el resultado. \end{quote} El valor de retorno (1) se multiplica por \( n \), que es 2, y se devuelve el resultado. \end{quote} El valor de retorno (2) se multiplica por \( n \), que es 3, y el resultado 6, se convierte en el valor de retorno de la llamada a la función que comenzó todo el proceso. He aquí el aspecto que tiene el diagrama de pila para esta secuencia de llamadas a función: \vspace{0.1in} \centerline{\includegraphics{../illustrations/stack3.eps} } \vspace{0.1in} Los valores de retorno se muestran según se pasan hacia la parte superior de la pila. En cada marco, el valor de retorno es el valor de \texttt{resultado}, que es el producto de \texttt{n} por \texttt{recursivo}. Nótese que en el último marco las variables locales \texttt{recursivo} y \texttt{resultado} no existen porque la rama que las crea no se ejecuta. \section{Acto de fe} \index{recursividad} \index{acto de fe} Seguir el flujo de ejecución es una de las maneras de leer programas; pero puede volverse rápidamente una tarea laberínitca. La alternativa es lo que llamamos el {}``acto de fe''. Cuando llegamos a una función, en lugar de seguir el flujo de ejecución, \emph{damos por sentado} que la función trabaja correctamente y devuelve el valor apropiado. De hecho, usted ya practica dicho salto de fe cuando usa funciones internas. Cuando llama a \texttt{math.cos} o a \texttt{math.exp}, no examina la implementación de dichas funciones. Simplemente da por sentado que funcionan porque los que escribieron las bibliotecas internas de Python son buenos programadores. Lo mismo se aplica cuando llama a una de las funciones programadas por usted. Por ejemplo en la Sección~\ref{boolean}, escribimos una función llamada \texttt{esDivisible} que determina si un número es divisible por otro. Una vez que nos hayamos convencido de que dicha función es correcta, comprobando y examinando el código, podremos usar la función sin tener siquiera que volver a mirar el código otra vez. Lo mismo vale para los programas recursivos. Cuando llegue a la llamada recursiva, en lugar de seguir el flujo de ejecución, tendría que dar por supuesto que la llamada recursiva funciona (es decir, devuelve el resultado correcto) y luego preguntarse: ``suponiendo que puedo hallar el factorial de \( n-1 \), ¿puedo hallar el factorial de \( n \)?'' En este caso, está claro que sí puede, multiplicándolo por \( n \). Por supuesto, es un tanto extraño dar por supuesto que la función está bien cuando ni siquiera ha acabado de escribirla, pero precisamente por eso se llama acto de fe. \section{Un ejemplo más} \label{one more example} En el ejemplo anterior, usamos variables temporales para ir apuntando los resultados y para hacer que el código fuese más fácil de depurar, pero podríamos habernos ahorrado unas cuantas líneas: \begin{verbatim} def factorial(n): if n == 0: return 1 else: return n * factorial(n-1) \end{verbatim} De ahora en adelante, tenderemos a usar la versión más concisa, pero le recomendamos que utilice la versión más explícita mientras se halle desarrollando código. Cuando lo tenga funcionando, lo podrá acortar, si se siente inspirado. \index{Fibonacci function} Después de \texttt{factorial}, el ejemplo más común de una función matemática recursivamente definida es \texttt{fibonacci}, que presenta la siguiente definición: \begin{eqnarray*} & & fibonacci(0)=1\\ & & fibonacci(1)=1\\ & & fibonacci(n)=fibonacci(n-1)+fibonacci(n-2); \end{eqnarray*} Traducido a Python, es como sigue: \begin{verbatim} def fibonacci (n): if n == 0 or n == 1: return 1 else: return fibonacci(n-1) + fibonacci(n-2) \end{verbatim} Si intenta seguir el flujo de ejecución aquí, incluso para valores relativamente pequeños de \( n \), le puede dar un dolor de cabeza. Pero si confiamos en el acto de fe, si da por supuesto que las dos llamadas recursivas funcionan correctamente, entonces estará claro que obtiene el resultado correcto al sumarlas juntas. \section{Comprobación de tipos} \index{tipos!comprobación} \index{comprobación de errores} \index{función factorial} ¿Qué sucede si llamamos a \texttt{factorial} y le damos 1.5 como argumento? \begin{verbatim} >>> factorial (1.5) RuntimeError: Maximum recursion depth exceeded \end{verbatim} Tiene todo el aspecto de una recursión infinita. Pero, ¿cómo ha podido ocurrir? Hay una condición de salida o caso base: cuando \texttt{n == 0}. El problema es que el valor de \texttt{n} yerra el caso base. \index{recursividad infinita} \index{recursividad!infinita} En la primera llamada recursiva, el valor de \texttt{n} es 0.5. En la siguiente vez su valor es -0.5. A partir de ahí, se vuelve más y más pequeño, pero nunca será 0. Tenemos dos opciones. Podemos intentar generalizar la función \texttt{factorial} para que trabaje con números de coma flotante, o podemos hacer que \texttt{factorial} compruebe el tipo de su parámetro. La primera opción se llama función gamma, y está más allá del objetivo de este libro. Así pues, tomemos la segunda. \index{función gamma} Podemos usar la función \texttt{type} para comparar el tipo del parámetro con el tipo de un valor entero conocido (por ejemplo 1). Ya que estamos en ello, podemos asegurarnos de que el parámetro sea positivo: \begin{verbatim} def factorial (n): if type(n) != type(1): print "El factorial está definido sólo para enteros." return -1 elif n < 0: print "El factorial está definido sólo para enteros\ positivos." return -1 elif n == 0: return 1 else: return n * factorial(n-1) \end{verbatim} Ahora tenemos tres condiciones de salida o casos base. El primero filtra los números no enteros. El segundo evita los enteros negativos. En ambos casos, se muestra un mensaje de error y se devuelve un valor especial, -1, para indicar a quien hizo la llamada a la función que algo fue mal: \begin{verbatim} >>> factorial (1.5) El factorial esta definido solo para enteros. -1 >>> factorial (-2) El factorial esta definido solo para enteros positivos. -1 >>> factorial ("paco") El factorial esta definido solo para enteros. -1 \end{verbatim} Si pasamos ambas comprobaciones, entonces sabemos que \( n \) es un entero positivo y podemos probar que la recursión termina. Este programa muestra un patrón que se llama a veces \textbf{guardián}. Las primeras dos condicionales actúan como guardianes, protegiendo al código que sigue de los valores que pudieran causar errores. Los guardianes hacen posible demostrar la corrección del código. \section{Glosario} \begin{description} \item [función~productiva:]Función que devuelve un valor de retorno. \item [valor~de~retorno:]El valor obtenido como resultado de una llamada a una función. \item [variable~temporal:]Variable utilizada para almacenar un valor intermedio en un cálculo complejo. \item [código~muerto:]Parte de un programa que no podrá ejecutarse nunca, a menudo debido a que aparece tras una sentencia de \texttt{return}. \item [None:]Valor especial de Python que devuelven funciones que o bien no tienen sentencia de \texttt{return} o bien tienen una sentencia de \texttt{return} sin argumento. \item [desarrollo~incremental:]Un método de desarrollo de programas que busca evitar el depurado añadiendo y probando una pequeña cantidad de código en cada paso. \item [andamiaje:]El código que se usa durante el desarrollo del programa pero que no es parte de la versión final. \item [guardián:]Una condición que comprueba y maneja circunstancias que pudieran provocar un error. \index{variable!temporal} \index{temporal!variable} \index{valor de retorno} \index{código muerto} \index{None} \index{desarrollo incremental} \index{andamiaje} \index{guardián}\end{description} %\end{document}


Next Up Previous Hi Index