Herramientas Informaticas

Mes: agosto 2012 Página 5 de 22

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.”

5.7. Un ejemplo más

En el ejemplo anterior, usamos variables temporales para ir apuntando los resultados y para hacer que el código fuese mas fácil de depurar, pero podríamos habernos ahorrado unas cuantas l³neas:

   1: def factorial(n):

   2:     if n == 0:

   3:         return 1

   4:     else:

   5:         return n * factorial(n-1)

De ahora en adelante, tenderemos a usar la version mas concisa, pero le recomendamos que utilice la version mas explícita mientras se halle desarrollando código. Cuando lo tenga funcionando, lo podra acortar, si se siente inspirado.

Después de factorial, el ejemplo mas comun de una funcion matematica recursivamente de¯nida es fibonacci, que presenta la siguiente definición:
fibonacci(0) = 1
fibonacci(1) = 1
fibonacci(n) = fibonacci(n ¡ 1) + fibonacci(n ¡ 2);
Traducido a Python, es como sigue:

   1: def fibonacci (n):

   2:     if n == 0 or n == 1:

   3:         return 1

   4:     else:

   5:         return 

   6:         fibonacci(n-1) + fibonacci(n-2)

   7:     

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 estara claro que obtiene el resultado correcto al sumarlas juntas.

5.8. Comprobación de tipos

¿Que sucede si llamamos a factorial y le damos 1.5 como argumento?

   1: >>> factorial (1.5)

   2: RuntimeError: Maximum recursion depth 

   3: exceeded

Tiene todo el aspecto de una recursion infinita. Pero, ¿como ha podido ocurrir?

Hay una condicion de salida o caso base: cuando n == 0. El problema es que el valor de n yerra el caso base.
En la primera llamada recursiva, el valor de n es 0.5. En la siguiente vez su valor es -0.5. A partir de ahí, se vuelve mas y mas peque~no, pero nunca sera 0.

Tenemos dos opciones. Podemos intentar generalizar la funcion factorial para que trabaje con numeros de coma flotante, o podemos hacer que factorial compruebe el tipo de su parametro. La primera opcion se llama funcion gamma, y esta mas alla del objetivo de este libro. Así pues, tomemos la segunda.

Podemos usar la funcion type para comparar el tipo del parametro con el tipo de un valor entero conocido (por ejemplo 1). Ya que estamos en ello, podemos asegurarnos de que el parametro sea positivo:

   1: def factorial (n):

   2:     if type(n) != type(1):

   3:         print "El factorial esta 

   4:         definido solo para enteros."

   5:         return -1

   6:     elif n < 0:

   7:         print "El 

   8:         factorial esta definido solo para enteros

   9:         positivos."

  10:         return -1

  11:     elif n == 0:

  12:         return 1

  13:         else:

  14:         return n * factorial(n-1)

  15:     

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 funcion que algo fue mal:

   1: >>> factorial (1.5)

   2: El factorial esta definido solo para 

   3: enteros.

   4: -1

   5: >>> factorial (-2)

   6: El factorial esta definido solo 

   7: para enteros positivos.

   8: -1

   9: >>> factorial ("paco")

  10: El factorial 

  11: esta definido solo para enteros.

  12: -1

Si pasamos ambas comprobaciones, entonces sabemos que n es un entero positivo y podemos probar que la recursion termina.

Este programa muestra un patron que se llama a veces guardian. Las primeras dos condicionales actuan como guardianes, protegiendo al código que sigue de los valores que pudieran causar errores. Los guardianes hacen posible demostrar la correccion del código.

5.9. Glosario

función productiva: Función que devuelve un valor de retorno.

valor de retorno: El valor obtenido como resultado de una llamada a una función.

variable temporal: Variable utilizada para almacenar un valor intermedio en un calculo complejo.

código muerto: Parte de un programa que no podrá ejecutarse nunca, a menudo debido a que aparece tras una sentencia de return.

None: Valor especial de Python que devuelven funciones que o bien no tienen sentencia de return o bien tienen una sentencia de return sin argumento.

desarrollo incremental: Un metodo de desarrollo de programas que busca evitar el depurado añadiendo y probando una pequeña cantidad de código en cada paso.

andamiaje: El código que se usa durante el desarrollo del programa pero que no es parte de la versión final.

Capítulo 6

Iteración

6.1. Asignación múltiple

Es posible que haya descubierto que es posible hacer mas de una asignación a una misma variable. El efecto de la nueva asignación es redirigir la variable de manera que deje de remitir al valor antiguo y empiece a remitir al valor nuevo.

   1: bruno = 5

   2: print bruno,

   3: bruno = 7

   4: print bruno

La salida del programa es 5 7, ya que la primera vez que imprimimos Bruno su valor es 5, y la segunda vez su valor es 7. La coma al final de la primera sentencia print impide que se imprima una nueva l³nea en ese punto, por eso ambos valores aparecen en la misma línea.

He aquí el aspecto de una asignacion multiple en un diagrama de estado:

Sin título

Cuando hay asignaciones multiples a una variable, es especialmente importante distinguir entre una sentencia de asignacion y una sentencia de igualdad. Puesto que Python usa el s³mbolo = para la asignacion, es tentador interpretar una sentencia como a = b como sentencia de igualdad. Pero no lo es.

Para empezar, la igualdad es commutativa, y la asignacion no lo es. Por ejemplo en matematicas si a = 7 entonces 7 = a. Pero en Python la sentencia a = 7 es legal, y 7 = a no lo es.

Y lo que es mas, en matematicas, una sentencia de igualdad es verdadera todo el tiempo. Si a = b ahora, entonces a siempre sera igual a b. En Python, una sentencia de asignacion puede hacer que dos variables sean iguales, pero no tienen por que quedarse así.

   1: a = 5

   2: b = a # a y b son ahora iguales

   3: a = 3 # a y b ya no son iguales

La tercera línea cambia el valor de a pero no cambia el valor de b, y por lo tanto ya dejan de ser iguales. En algunos lenguajes de programacion, se usa para la asignación un símbolo distinto, como <- o como :=, para evitar la confusion.

Aunque la asignacion multiple es util a menudo, debe usarla con cuidado. Si los valores de las variables van cambiando constantemente en distintas partes del programa, podría suceder que el codigo sea difícil de leer y mantener.

6.2. La sentencia while

Una de las tareas para las que los computadores se usan con frecuencia es la automatización de tareas repetitivas. Repetir tareas similares o idénticas es algo que los computadores hacen bien y las personas no hacen tan bien.

Hemos visto dos programas, nLineas y cuenta atrás, que usan la recursividad para llevar a cabo la repetición, que también se llama iteración.

Por ser la iteración tan habitual, Python proporciona como lenguaje varias características que la hacen mas fácil.

La primera característica que vamos a considerar es la sentencia while.

Este es el aspecto de cuenta atrás con una sentencia while:

   1: def cuenta_atras(n):

   2:     while n > 0:

   3:         print n

   4:         n = n-1

   5:         print "Despegando!"

Como eliminamos la llamada recursiva, esta funcion no es recursiva.
Casi podía leer una sentencia while como si fuera ingles (castellano “mientras”). Quiere decir que “Mientras n sea mayor que cero, continua mostrando el valor de n y despues restandole 1 al valor de n. Cuando llegues a cero, muestra la palabra “¡Despegando!”.

Mas formalmente, el flujo de ejecucion de una sentencia while es el siguiente:

  • Evaluar la condicion, devolviendo 0 o 1.
  • Si la condicion es falsa (0), salir de la sentencia while y continuar la ejecución en la siguiente sentencia.
  • Si la condicion es verdadera (1), ejecutar cada una de las sentencias en el cuerpo del bucle while, y luego volver al paso 1.

El cuerpo esta formado por todas las sentencias bajo el encabezado que tienen el mismo sangrado.

Este tipo de flujo de llama bucle porque el tercer paso vuelve de nuevo arriba.

Nótese que si la condicion es falsa la primera vez que se atraviesa el bucle, las sentencias del interior del bucle no se ejecutan nunca.

El cuerpo del bucle debe cambiar el valor de una o mas variables de manera que, llegado el momento, la condicion sea falsa y el bucle termine. En caso contrario, el bucle se repetira para siempre, que es lo que se llama bucle infinito. Una infinita fuente de diversion para los científicos informaticos es la observacion de que las instrucciones del champu lavar, aclarar, repetir”, son un bucle infinito.

En el caso de cuenta atras, podemos probar que el bucle terminara porque sabemos que el valor de n es finito, y podemos ver que el valor de n disminuye cada vez que se atraviesa el bucle (cada iteracion), de manera que ea la larga tenemos que llegar a cero. En otros casos no es tan facil decirlo:

   1: def secuencia(n):

   2:     while n != 1:

   3:         print n,

   4:         if n%2 == 0: # n es par

   5:             n = 

   6:             n/2

   7:         else: # n es impar

   8:             n = n*3+1

   9:         

La condicion de este bucle es n != 1, de manera que el bucle continuara hasta que n sea 1, que hara que la condicion sea falsa.
En cada iteracion, el programa muestra como salida el valor de n y luego comprueba si es par o impar. Si es par, el valor de n se divide entre dos. Si es impar, el valor se sustituye por 3n+1. Por ejemplo, si el valor de comienzo (el argumento pasado a la secuencia) es 3, la secuencia resultante es 3, 10, 5, 16, 8, 4, 2, 1.

Puesto que n a veces aumenta y a veces disminuye, no hay una prueba obvia de que n alcance alguna vez el valor 1, o de que el programa vaya a terminar. Para algunos valores particulares de n, podemos probar la terminacion. Por ejemplo, si el valor de inicio es una potencia de dos, entonces el valor de n sera par cada vez que se pasa a traves del bucle, hasta que lleguemos a 1. El ejemplo anterior acaba con dicha secuencia, empezando por 16.

Dejando aparte valores particulares, la pregunta interesante es si podemos probar que este programa terminara para todos los valores de n.

Hasta la fecha, nadie ha sido capaz de probarlo o negarlo.

Como actividad, reescriba la funcion nLines de la seccion 4.9 utilizando iteracion en lugar de recursividad.

6.3. Tablas

Una de las cosas para las que resultan buenos los bucles es para generar datos tabulares. Antes de que los computadores estuvieran disponibles de forma masiva, la gente tenía que calcular a mano logaritmos, senos, cosenos y otras funciones matemáticas. Para facilitarlo, los libros de matemáticas contenían largas tablas donde aparecían los valores de estas funciones. Confeccionar estas tablas era una tarea lenta y pesada, y el resultado estaba lleno de erratas.

Cuando los computadores aparecieron en escena, una de las primeras reacciones fue ¡Que bueno! Podemos usar los computadores para generar las tablas, así no habrá errores”. Resulto cierto (casi), pero no se vio mas allá. Poco después los computadores y las calculadoras científicas se hicieron tan ubicuas que las tablas resultaron obsoletas.

Bueno, casi. Resulta que para ciertas operaciones, los computadores usan tablas para obtener un valor aproximado, y luego ejecutan cálculos para mejorar la aproximación. En algunos casos, ha habido errores en las tablas subyacentes; el mas famoso estaba en la tabla que el Pentium de Intel usaba para llevar a cabo la división de coma flotante.

Aunque una tabla logarítmica ya no es tan útil como lo fuera antaño, todavía constituye un buen ejemplo de iteración. El siguiente programa muestra una secuencia de valores en la columna izquierda y sus logaritmos en la columna derecha:

x = 1.0

while x < 10.0:

    print x, 't', math.log(x)

    x = x + 1.0

    

El nt representa un caracter de tabulacion.

Tal como se van mostrando en la pantalla caracteres y cadenas, un señalador invisible llamado cursor lleva la cuenta de donde ira el proximo caracter. Tras una sentencia print, el cursor va normalmente al principio de la línea siguiente.

El caracter de tabulacion hace que el cursor se desplace a la derecha hasta que alcance uno de los marcadores de tabulacion. Los tabuladores son utiles para alinear columnas de texto, como en la salida del programa anterior:

1.0 0.0
2.0 0.69314718056
3.0 1.09861228867
4.0 1.38629436112
5.0 1.60943791243
6.0 1.79175946923
7.0 1.94591014906
8.0 2.07944154168
9.0 2.19722457734

Si estos valores le parecen raros, recuerde que la funcion log usa como base e.

Debido a que las potencias de dos son muy importantes en las ciencias de la computación, generalmente querremos hallar los logaritmos en relacion con la base dos. Para llevarlo a cabo, podemos usar la siguiente formula:
log2 x =(logex/loge2)
(6.1)

Cambiar la sentencia de salida a:

   1: print x, 't', math.log(x)/math.log(2.0)

devuelve

1.0 0.0
2.0 1.0
3.0 1.58496250072
4.0 2.0
5.0 2.32192809489
6.0 2.58496250072
7.0 2.80735492206
8.0 3.0
9.0 3.16992500144

Podemos ver que 1, 2, 4 y 8 son potencias de dos, porque sus logaritomos de base 2 son numeros enteros. Si quisieramos encontrar los logaritmos de otras potencias de dos, podr³amos modificar el programa de la siguiente manera:

   1: x = 1.0

   2: while x < 100.0:

   3:     print x, 't', math.log(x)/math.log(2.0)

   4:     x = x * 2.0

Ahora, en lugar de añadir algo a x cada vez que atravesamos el bucle, que
devuelve una secuencia aritmetica, multiplicamos x por algo, devolviendo una
secuencia geometrica. El resultado es:
1.0 0.0
2.0 1.0
4.0 2.0
8.0 3.0
16.0 4.0
32.0 5.0
64.0 6.0

Debido a que usamos caracteres de tabulacion entre las columnas, la posicion de la segunda columna no depende del numero de dígitos de la primera columna.

Las tablas logarítmicas quizás ya no sean utiles, pero conocer las potencias de dos no ha dejado de serlo para los científicos informaticos.

Como actividad, modifique el programa para que muestre las potencias de dos hasta 65536 (es decir, 216). Imprímala y memorícela.

El caracter de barra invertida en ‘t’ indica el principio de una secuencia de escape. Las secuencias de escape se usan para representar caracteres invisibles como tabulaciones y retornos de carro. La secuencia n representa un retorno de carro.

Una sentencia de escape puede aparecer en cualquier lugar de una cadena; en el ejemplo, la secuencia de escape del tabulador es la unica de la cadena.
¿Como cree que puede representar una barra invertida en una cadena?

Como ejercicio, escriba un unica cadena que presente esta salida.

Página 5 de 22

Creado con WordPress & Tema de Anders Norén