Herramientas Informaticas

Categoría: Sin categoría Página 35 de 51

Capítulo 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.

   1: import math

   2: e = math.exp(1.0)

   3: 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 área, que devuelve el area de un c³rculo con un radio dado:

   1: import math

   2: def area(radio):

   3: temporal = math.pi * radio**2

   4: return 

   5: temporal

Ya hemos visto antes la sentencia return, pero en una funcion productiva la sentencia return incluye un valor de retorno. Esta sentencia quiere decir “retorna inmediatamente de la funcion y usa la siguiente expresion como valor de retorno”. La expresion dada puede ser arbitrariamente complicada; así pues, podríamos haber escrito esta funcion mas concisamente:

   1: def area(radio):

   2:     return math.pi * radio**2

Por otra parte, las variables temporales como temporal suelen hacer mas fácil el depurado.

A veces es útil disponer de varias sentencias de retorno, una en cada rama de una condición:

   1: def valorAbsoluto(x):

   2:     if x < 0:

   3:         return -x

   4:     else:

   5:         return x

Puesto que estas sentencias return estan en una condicion alternativa, solo se ejecutara una de ellas. En cuanto se ejecuta una de ellas, la funcion termina sin ejecutar ninguna de las sentencias siguientes.

El codigo que aparece despues de una sentencia return o en cualquier otro lugar donde el flujo de ejecucion no pueda llegar, recibe el nombre de codigo muerto.

En una funcion productiva es una buena idea asegurarse de que cualquier posible recorrido del programa alcanza una sentencia return. Por ejemplo:

   1: def valorAbsoluto(x):

   2:     if x < 0:

   3:         return -x

   4:     elif x > 0:

   5:     return x

Este programa no es correcto porque si resulta que x vale 0, entonces no se cumple ninguna de ambas condiciones y la funcion termina sin alcanzar la sentencia return. En este caso, el valor de retorno es un valor especial llamado None:

   1: >>> print valorAbsoluto(0)

   2: None

Como actividad, escriba una funcion comparar que devuelva 1 si x>y , 0 si x == y , y -1 si x <y .

5.2. Desarrollo de programas

Llegados a este punto, tendría que poder mirar a funciones Python completas y adivinar que hacen. También, si ha hecho los ejercicios, habrá escrito algunas funcioncillas. Tal como vaya escribiendo funciones mayores puede empezar a experimentar mas 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 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.

Por ejemplo, supongamos que desea encontrar la distancia entre dos puntos, dados por las coordenadas (x1; y1) y (x2; y2). Por el teorema de Pitágoras, podemos escribir la distancia es:

Sin título

El primer paso es considerar que aspecto tendría una función distancia en Python. En otras palabras, ¿cuales son las entradas (parámetros) y cual 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:

   1: def distancia(x1, y1, x2, y2):

   2: return 0.0

Obviamente, la funcion no calcula distancias; siempre devuelve cero. Pero es sintacticamente correcta y se ejecutara, lo que signi¯ca que podemos probarla antes de complicarla mas.

Para comprobar la nueva funcion, tenemos que llamarla con valores de ejemplo:

   1: >>> def distancia(x1, y1, x2, y2):

   2: ... return 

   3: 0.0

   4: ...

   5: >>> distancia(1, 2, 4, 6)

   6: 0.0

   7: >>>

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 triangulo 3-4-5). Cuando se comprueba una funcion, es util saber la respuesta
correcta.

Hasta el momento, hemos comprobado que la funcion es sintacticamente correcta, así que podemos empezar a añadir líneas de codigo. Despues de cada cambio incremental, comprobamos de nuevo la funcion. Si en un momento dado aparece un error, sabremos donde esta exactamente: en la ultima l³nea que hayamos añadido.

El siguiente paso en el calculo es encontrar las diferencias entre x2¡x1 y y2¡y1.

Almacenaremos dichos valores en variables temporales llamadas dx y dy y las imprimiremos.

   1: def distancia(x1, y1, x2, y2):

   2:     dx = x2 - x1

   3:     dy = y2 - y1

   4:     print "dx es", 

   5:     dx

   6:     print "dy es", dy

   7:     return 0.0

Si la funcion funciona, valga la redundancia, las salidas deberían ser 3 y 4. Si es así, sabemos que la funcion recibe correctamente los parametros y realiza correctamente el primer calculo. Si no, solo hay unas pocas líneas que revisar.

Ahora calculamos la suma de los cuadarados de dx y dy:

   1: def distancia(x1, y1, x2, y2):

   2:     dx = x2 - x1

   3:     dy = y2 - y1

   4:     dalcuadrado = 

   5:     dx**2 + dy**2

   6:     print "dalcuadrado es: ", dalcuadrado

   7:     return 0.0

Fíjese en que hemos eliminado las sentencias print que escribimos en el paso anterior. Este codigo se llama andamiaje porque es util 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 ultimo, si hemos importado el modulo math, podemos usar la funcion sqrt para calcular y devolver el resultado:

   1: def distancia(x1, y1, x2, y2):

   2: dx = x2 - x1

   3: dy = y2 - y1

   4: dalcuadrado = 

   5: dx**2 + dy**2

   6: resultado = math.sqrt(dalcuadrado)

   7: return resultado

Si esto funciona correctamente, ha terminado. Si no, podría ser que quisiera usted imprimir el valor de resultado antes de la sentencia return.

Al principio, deber³a a~nadir solamente una o dos l³neas de codigo 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:

1. Comience con un programa que funcione y hagale pequeños cambios in-
crementales. Si hay un error, sabra exactamente donde esta.
2. Use variables temporales para mantener valores intermedios, de tal manera
que pueda mostrarlos por pantalla y comprobarlos.
3. Una vez que el programa este funcionando, tal vez prefiera eliminar parte
del andamiaje o aglutinar multiples sentencias en expresiones compuestas,
pero solo si eso no hace que el programa sea difícil de leer.
Como actividad, utilice el desarrollo incremental para escribir una
funcion de nombre hipotenusa que devuelva la longitud de la hipo-
tenusa de un triangulo rectangulo, dando como parametros los dos
catetos. Registre cada estado del desarrollo incremental segun vaya
avanzando.

5.3. Composición

Como seguramente a estas alturas ya supondrá, se puede llamar a una función desde dentro de otra. Esta habilidad se llama 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 esta almacenado en las variables xc e yc, y que el punto del perímetro lo esta en xp e yp. El primer paso es hallar el radio del círculo, que es la distancia entre los dos puntos. Afortunadamente hay una función, distancia, que realiza esta tarea:

   1: radio = distancia(xc, yc, xp, yp)

El segundo paso es encontrar el area de un círculo con ese radio y devolverla:

   1: resultado = area(radio)

   2: return resultado

Envolviendo todo esto en una funcion, obtenemos:

   1: def area2(xc, yc, xp, yp):

   2:     radio = distancia(xc, yc, xp, yp)

   3:     resultado = area(radio)

   4:     return resultado

Hemos llamado a esta funcion area2 para distinguirla de la funcion area definida anteriormente. Solo puede haber una unica funcion con un determinado nombre dentro de un modulo.

Las variables temporales radio y area son utiles para el desarrollo y el depurado, pero una vez que el programa esta funcionando, podemos hacerlo mas conciso integrando las llamadas a las funciones en una sola línea:

   1: def area2(xc, yc, xp, yp):

   2: return area(distancia(xc, yc, xp, yp))

Como actividad, escriba una funcion pendiente(x1, y1, x2, y2) que devuelva la pendiente de la l³nea que atraviesa los puntos (x1,y1) y (x2, y2). Luego use esta funcion en una funcion que se llame intercepta(x1, y1, x2, y2) que devuelva la [[y-intercepta]] de la línea a traves de los puntos (x1, y1) y (x2, y2).

5.4. Funciones booleanas

Las funciones pueden devolver valores booleanos, lo que a menudo es conveniente para ocultar complicadas comprobaciones dentro de funciones. Por ejemplo:

   1: def esDivisible(x, y):

   2:     if x % y == 0:

   3:         return 1 # it's  true

   4:     else:

   5:         return 0 # it's false

La funcion lleva por nombre esDivisible. Es habitual dar a las funciones booleanas nombres que suenan como preguntas s³/no. Devuelve 1 o 0 para indicar si la x es o no divisibelo por y.

Podemos reducir el tama~no de la funcion aprovechandonos del hecho de que la sentencia condicional que hay despues del if es en s³ misma una expresión booleana. Podemos devolverla directamente, evitando a la vez la sentencia if:

   1: def esDivisible(x, y):

   2:     return x % y == 0

La siguiente sesion muestra a la nueva funcion en acción:

   1: >>> esDivisible(6, 4)

   2: 0

   3: >>> esDivisible(6, 3)

   4: 1

El uso mas comun para las funciones booleanas es dentro de sentencias condicionales:

   1: if esDivisible(x, y):

   2:     print "x es divisible entre y"

   3: else:

   4:     print "x no es divisible entre y"

Puede parecer tentador escribir algo como:

   1: if esDivisible(x, y) == 1:

Pero la comparacion extra es innecesaria.

Como actividad, escriba una funcion estaEntre(x, y, z) que devuelva 1 en caso de que y <= x <= z y que devuelva 0 en cualquier otro caso.

5.5. Más recursividad

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 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 ordenes 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 argumentaran 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: fangoso: adjetivo que describe algo que es fangoso.

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 semejante a lo siguiente:

0! = 1
n! = n ¢ (n ¡ 1)!

Esta definición establece que el factorial 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.

Si puede escribir una definición recursiva de algo, normalmente podrá escribir un programa de Python para evaluarlo. El primer paso es decidir cuales son los parámetros para esta función. Con poco esfuerzo llegara a la conclusión de que factorial toma un único parámetro:

   1: def factorial(n):

   2:     Si resultase que el argumento fuese 0, todo lo que hemos de hacer es devolver 1:

   3: def factorial(n):

   4:     if n == 0:

   5:     return 1

   6:     

 

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:

   1: def factorial(n):

   2:     if n == 0:

   3:         return 1

   4:     else:

   5:         recursivo = 

   6:         factorial(n-1)

   7:         resultado = n * recursivo

   8:         return resultado

El flujo de ejecucion de este programa es similar al de cuenta atras de la Sección 4.9. Si llamamos a factorial con el valor 3:

Puesto que 3 no es 0, tomamos la segunda rama y calculamos el factorial de n-1…

Puesto que 2 no es 0, tomamos la segunda rama y calculamos el factorial de n-1…

Puesto que 1 no es 0, tomamos la segunda rama y calculamos el factorial de n-1…

Puesto que 0 es 0, tomamos la primera rama y devolvemos el valor 1 sin hacer mas llamadas recursivas.

El valor de retorno (1) se multiplica por n, que es 1, y se devuelve el resultado.

El valor de retorno (1) se multiplica por n, que es 2, y se devuelve el resultado.

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 funcion que comenzo todo el proceso.

He aquí el aspecto que tiene el diagrama de pila para esta secuencia de llamadas a función:

Sin título

Los valores de retorno se muestran segun se pasan hacia la parte superior de la pila. En cada marco, el valor de retorno es el valor de resultado, que es el producto de n por recursivo.

Nótese que en el ultimo marco las variables locales recursivo y resultado no existen porque la rama que las crea no se ejecuta.

5.6. 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íntica. 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, 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 math.cos o a 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 5.4, escribimos una función llamada esDivisible que determina si un numero 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, esta claro que sí puede, multiplicándolo por n.

Por supuesto, es un tanto extraño dar por supuesto que la función esta bien cuando ni siquiera ha acabado de escribirla, pero precisamente por eso se llama acto de fe.”

6.7. Variables locales

Quizá se este preguntando como podemos usar la misma variable tanto en imprimeMultiplos como en imprimeTablaMult.

¿No habrá problemas cuando una de las funciones cambie los valores de la variable?

La respuesta es no, ya que la variable i en imprimeMultiplos y la variable i in imprimeTablaMult no son la misma variable.

Las variables creadas dentro de una función son locales. No puede acceder a una variable local fuera de su función “huésped”. Eso significa que es posible tener múltiples variables con el mismo nombre, siempre que no estén en la misma función.

El diagrama de pila de esta función muestra claramente que las dos variables llamadas i no son la misma variable. Pueden referirse a diferentes valores, y cambiar uno no afecta al otro.

Sin título

El valor de i en imprimeTablaMult va desde 1 hasta 6. En el diagrama, resulta ser 3. El próximo recorrido del bucle será 4. En cada recorrido del bucle,imprimeTablaMult llama a imprimeMultiplos con el valor actual de i como argumento. Ese valor se asigna al parámetro n.

Dentro de imprimeMultiplos, el valor de i va desde 1 hasta 6. En el diagrama, resulta ser 2. Los cambios en esta variable no tienen ningún efecto sobre el valor de i en imprimeTablaMult.

Es habitual y perfectamente legal tener diferentes variables locales con el mismo nombre. En especial, los nombres i, j y k se suelen usar como variables de bucle. Si evita usarlas en una función porque las utilizo en algún otro lugar, probablemente consiga que el programa sea mas difícil de leer.

6.8. Más generalización

Como otro ejemplo de generalización, imagine que desea un programa que imprima una tabla de multiplicación de cualquier tamaño, y no solo la tabla de 6×6. Podría añadir un parámetro a imprimeTablaMult:

   1: def imprimeTablaMult(mayor):

   2: i = 1

   3: while i <= 

   4: mayor:

   5: imprimeMultiplos(i)

   6: i = i + 1

Hemos sustituido el valor 6 con el parametro mayor. Si ahora se llama a imprimeTablaMult con el argumento 7, obtenemos:

   1: 1 2 3 4 5 6

   2: 2 4 6 8 10 12

   3: 3 6 9 12 15 18

   4: 4 8 12 16 20 24

   5: 5 10 15 20 25 30

   6: 6 12 18 24 30 36

   7: 7 14 21 28 35 42

lo que es correcto, excepto por el hecho de que seguramente queremos que la tabla este cuadrada, con el mismo numero de filas que de columnas. Para hacerlo, añadimos otro parametro a imprimeMultiplos, a fin de especificar cuantas columnas tendría que tener la tabla.

Solo para fastidiar, llamaremos tambien a este parametro mayor, para demostrar que diferentes funciones pueden tener parametros con el mismo nombre (al igual que las variables locales). Aquí tenemos el programa completo:

   1: def imprimeMultiplos(n, mayor):

   2:     int i = 1

   3:     while i <= mayor:

   4:         print 

   5:         n*i, 't',

   6:         i = i + 1

   7:         print

   8:     def imprimeTablaMult(mayor):

   9:         int i = 

  10:         1

  11:         while i <= mayor:

  12:         imprimeMultiplos(i, mayor)

  13:         i = i + 1

  14:         

Nótese que al añadir un nuevo parametro, tuvimos que cambiar la primera línea de la funcion (el encabezado de la funcion), y tuvimos tambien que cambiar el lugar donde se llama a la función en imprimeTablaMult.

Según lo esperado, este programa genera una tabla cuadrada de 7×7:

   1: 1 2 3 4 5 6 7

   2: 2 4 6 8 10 12 14

   3: 3 6 9 12 15 18 21

   4: 4 8 12 16 20 24 

   5: 28 5 10 15 20 25 30 35

   6: 6 12 18 24 30 36 42

   7: 7 14 21 28 35 42 49

Cuando generaliza correctamente una funcion, a menudo se encuentra con que el programa resultante tiene capacidades que Usted no pensaba. Por ejemplo, quizá observe que la tabla de multiplicacion es simetrica, porque ab = ba, de manera que todas las entradas de la tabla aparecen dos veces. Puede ahorrar tinta imprimiendo solo la mitad de la tabla. Para hacerlo, solo tiene que cambiar una línea de imprimeTablaMult. Cambie imprimeMultiplos(i, mayor) por imprimeMultiplos(i, i) y obtendra

   1: 1

   2: 2 4

   3: 3 6 9

   4: 4 8 12 16

   5: 5 10 15 20 25

   6: 6 12 18 24 30 36

   7: 7 14 21 28 35 42 49

Como actividad, siga o trace la ejecucion de esta nueva version de imprimeTablaMult para hacerse una idea de como funciona.

6.9. Funciones

Hasta el momento hemos mencionado en alguna ocasión “todas las cosas para las que sirven las funciones”. Puede que ya se este preguntando que cosas son exactamente. He aqu³ algunas de las razones por las que las funciones son útiles:

  • Al dar un nombre a una secuencia de sentencias, hace que su programa sea mas fácil de leer y depurar.
  • Dividir un programa largo en funciones le permite separar partes del programa, depurarlas aisladamente, y luego recomponerlas en un todo.
  • Las funciones facilitan tanto la recursividad como la iteración.
  • Las funciones bien dise~nadas son generalmente útiles para mas de un programa. Una vez escritas y depuradas, pueden reutilizarse.

6.10. Glosario

asignación múltiple: Hacer mas de una asignación a la misma variable durante la ejecución de un programa.

iteración: La ejecución repetida de un conjunto de sentencias por medio de una llamada recursiva a una función o un bucle.

bucle: Sentencia o grupo de sentencias que se ejecutan repetidamente hasta que se cumple una condición de terminación.

bucle infinito: Bucle cuya condición de terminación nunca se cumple.

cuerpo: Las sentencias que hay dentro de un bucle.

variable de bucle: Variable que se usa para determinar la condición de terminación de un bucle.

tabulador: Carácter especial que hace que el cursor se mueva hasta la siguiente marca de tabulación en la línea actual.

nueva línea: Un carácter especial que hace que le cursor se mueva al inicio de la siguiente línea.

cursor: Un marcador invisible que sigue el rastro de donde se imprimirá el siguiente carácter.

secuencia de escape: Carácter de escape (n) seguido por uno o mas caracteres imprimibles, que se usan para designar un carácter no imprimible.

encapsular: Dividir un programa largo y complejo en componentes (como las funciones) y aislar los componentes unos de otros (por ejemplo usando variables locales).

generalizar: Sustituir algo innecesariamente específico (como es un valor constante) con algo convenientemente general (como es una variable o parámetro). La generalización hace el código mas versátil, mas apto para reutilizarse y algunas veces incluso mas fácil de escribir.

plan de desarrollo: Proceso para desarrollar un programa. En este capítulo, hemos mostrado un estilo de desarrollo basado en desarrollar código para hacer cosas simples y específicas, y luego encapsularlas y generalizarlas.

Página 35 de 51

Creado con WordPress & Tema de Anders Norén