4. Ramificación e iteración

Un computador está diseñado para ejecutar instrucciones en secuencia. En condiciones normales las sentencias de Python se ejecutan una después de otra (ejecución secuencial). En Python el flujo normal se representa con una secuencia de sentencias, cada una empieza en una línea diferente y con el mismo margen a la izquierda. Una de estas secuencias se denomina bloque.

Ramificación e iteración son los mecanismos básicos que aporta Python para controlar el flujo del programa más allá de la secuencia normal de ejecución. Se consigue con sentencias compuestas, que contienen bloques de sentencias en su interior, pero que por otro lado se comportan como una única sentencia. Veamos las más importantes.

4.1. Ramificación

Es necesario emplear sentencias de bifurcación o ramificación siempre que se necesite ejecutar un conjunto de sentencias solo en determinados casos y no en otros.

La sentencia básica de ramificación es la sentencia if. La estructura más simple es la siguiente:

if condición:
    bloque_si_cierto

La condición debe tener un valor booleano o debe ser posible convertirlo a un valor booleano. Es decir, la condición debe poder evaluarse como uno de los valores True (cierto) o False (falso). Si la condición es True entonces se ejecutará el bloque_si_cierto. Después sigue con la ejecución secuencial.

Truco

Los bloques como bloque_si_cierto no tienen por qué consistir en una única instrucción. Pueden ser varias siempre que tengan el mismo nivel de indentación, es decir, el ´mismo número de espacios antes de cada sentencia.

Todas las sentencias compuestas tienen la misma estructura. Empiezan con una palabra clave que la identifica, luego pueden aparecer una serie de elementos sintácticos propios de la sentencia y termina la línea con un signo : (dos puntos). A continuación viene al menos un bloque de sentencias que debe cambiar su margen respecto a la palabra clave. ¿Cuánto margen? Da igual, mientras sea diferente del margen de la palabra clave a Python le da lo mismo.

En el caso de la sentencia if existen versiones algo más complejas, que, no obstante, mantienen la misma estructura. Por ejemplo, es posible indicar también un bloque de sentencias que se ejecuta solo si no se cumple la condición:

if condición:
    bloque_si_cierto
else:
    bloque_si_falso

Funciona igual que el caso anterior pero en caso de que la condición se evalúe como False se ejecutará el bloque de sentencias bloque_si_falso, que sigue a la palabra clave else. El nuevo añadido tiene la misma estructura de una sentencia compuesta cualquiera, pero es parte de la sentencia *if*. Se denomina claúsula else.

Todavía hay otra claúsula más en la sentencia if. Se trata de la claúsula elif que puede repetirse tantas veces como sea necesario justo antes de la claúsula else:

if condición1:
    bloque_si_cierto_1
elif condición2:
    bloque_si_cierto_2
elif condición3:
    bloque_si_cierto_3
else:
    bloque_si_falso

La palabra clave elif significa else if, es decir, en caso contrario, si ... Es equivalente a una claúsula else seguida de una nueva sentencia if pero es mucho más compacta. Por ejemplo, el mismo caso que hemos puesto arriba sin claúsulas elif sería algo así:

if condición1:
    bloque_si_cierto_1
else:
    if condición2:
        bloque_si_cierto_2
    else:
        if condición3:
            bloque_si_cierto_3
        else:
            bloque_si_falso

Como puedes ver este código está lleno de márgenes diferentes. Eso es extremadamente feo. Los programas de ordenador, como toda labor artesanal, tienen también cierto sentido estético. Un programa de ordenador bonito debe ser agradable de leer, fácil de entender y modificar, sin redundancias, sin código innecesario. Este fragmento es muy difícil de entender y modificar, así que no lo hagas nunca.

Truco

Las claúsulas elif hacen más legible un código como el de arriba. Pero debes pararte a pensar antes de usarlas. Una sentencia if con claúsulas elif es mucho más compleja que un if sencillo. Procura evitarlas, procura también evitar las claúsulas else.

Veamos unos ejemplos:

  • Imprimir por pantalla si un número x es par o impar.

    El mensaje a imprimir es diferente según x sea par o impar. Es decir, lo que hay que hacer es diferente según el valor de una condición. Eso claramente nos indica que tenemos que usar una sentencia de bifurcación.

    En realidad pronto veremos que podemos evitar hacer cosas diferentes en la mayoría de los casos, pero todavía no sabemos suficiente Python.

  • Encontrar el mínimo de los números x, y, z.

    Este ejemplo ilustra dos aspectos interesantes. Uno es la posibilidad de hacer expresiones complejas usando operadores x < y and x < z. Otro es la posibilidad de asignar varias variables de golpe en una línea, separando los valores y las variables por comas. Los detalles de esta construcción los veremos más adelante, pero empieza a usarla desde ya, ahorra mucho espacio.

  • Imprimir por pantalla los números x, y, z en orden creciente.

    ¿No te parece excesivamente largo para un problema tan pequeño? Puedes apostar a que no es la mejor solución, pero es un buen ejemplo de if.

No te vamos a dejar con ese mal sabor de boca, vamos a escribirlo bien.

No solo es mucho más corto, es además más general porque permite aplicarlo a cualquier número de argumentos.

Esos corchetes seguro que ahora mismo no te resultan familiares, pero pronto serán de la familia. De todas formas los podemos quitar de la salida y dejarla igual que antes.

Por esta vez vamos a explicártelo en detalle, pero intenta usar la documentación oficial de http://docs.python.org para entender lo que hacen los programas que veremos más adelante.

Seguramente habrás adivinado que sorted es una función que devuelve una versión ordenada de lo que se le pasa como argumento. Lo que pasamos como argumento a sorted es una lista, un tipo de objeto de Python que agrupa una secuencia de objetos.

Por tanto [x, y, z] no es más que una lista que contiene la secuencia de elementos x, y y z. Y sorted([x,y,z]) es una versión ordenada de esa lista.

Si queremos que la lista se muestre con otro formato no tenemos más remedio que imprimir nosotros cada elemento en lugar de usar print. Por suerte sabemos que print imprime todos sus argumentos separados por un espacio. Por tanto basta con pasar todos los elementos de la lista como argumentos independientes de print. Eso es lo que conseguimos con el asterisco antes de sorted.

Así es, el asterisco no solo vale para multiplicar. Como otros muchos operadores en Python tiene diferentes significados dependiendo del contexto. Cuando el operador * solo tiene un argumento (operador unario) y el argumento es una secuencia de elementos el asterisco desempaqueta los elementos y permite usarlos en contextos en los que se necesitan varios argumentos. Por ejemplo, en llamadas a función.

Hemos visto suficiente de ejecución condicional como para hacer con facilidad todos los ejercicios del curso, pero no queremos cerrar la sección sin mostrar otras posibilidades que ofrece Python de ejecución condicional. El operador ternario if/else permite evaluar expresiones de forma condicional. En el siguiente ejemplo el valor de y depende del valor de x. Si x es par y toma el valor x/2, en caso contrario y toma el valor x.

El operador ternario if/else es distinto a la sentencia if con claúsula else aunque usan las mismas palabras reservadas. En este caso no usamos los dos puntos para marcar inicios del bloque contenido. Ese pequeño detalle hace que no se interprete como una sentencia, sino como una expresión.

4.2. Iteración

Los bucles son construcciones que permiten volver atrás en la secuencia de sentencias. Cada ejecución del bloque de sentencias que compone el cuerpo del bucle se le llama iteración.

El más general es el bucle while que repite un bloque de sentencias mientras se verifique una condición booleana y que ya conocemos de ejemplos anteriores. La estructura general es:

while condición:
    bloque_si_cierto

El bloque del bucle while se repite continuamente mientras se cumpla la condición. Se comprueba la condición siempre al empezar cada repetición.

Veamos un ejemplo muy similar a un conocido.

  • Encontrar la raiz cúbica de un número natural n.

    Es un ejemplo de enumeración exhaustiva. Pasamos por todas las posibilidades comprobando si alguna de ellas es la respuesta correcta. ¿Y qué pasa si el número n no tiene una raiz cúbica perfecta? Nuestro algoritmo no sabe de números reales.

    No hay una respuesta universal para esta pregunta, pero aquí te proponemos una posibilidad, no devolver nada. Otra posibilidad es devolver False y otra es utilizar un mecanismo de control de errores denominado excepción. Esta última opción es seguramente la más recomendable pero aún no sabemos suficiente Python.

    Con nuestra propuesta de no devolver nada la función puede utilizarse con seguridad así:

A lo mejor este ejemplo es muy sencillo y tú mismo ves que el algoritmo es fácil de entender, pero las cosas en la vida real no suelen ser tan fáciles. En general nos vamos a enfrentar al problema de saber si lo que hemos escrito está bien o no hemos entendido todos los casos posibles. ¿Cómo sabemos que el bucle que hemos escrito termina alguna vez? ¿Cómo sabemos que no se queda indefinidamente en él?

La respuesta es que debemos buscar una función de decremento. No hay que escribirla en el programa y en muchos casos ni siquiera hay que escribirla en papel, pero tiene que existir. Una función de decremento tiene que cumplir cuatro características básicas:

  1. Se trata de una función que hace corresponder números enteros a valores de las variables del programa \(f:V \rightarrow \mathbb{Z}\).
  2. Cuando el programa entra en el bucle tiene que tomar un valor no negativo.
  3. Cuando la función de decremento toma un valor <= 0 el programa debe salir del bucle.
  4. En cada iteración del bucle la función toma un valor menor que en la anterior iteración (de ahí el nombre de función de decremento).

Por ejemplo, en el caso anterior la función de decremento es \(f(i,n) = n - i^3\). Al entrar en el bucle con n = 27 toma el valor 26. En cada iteración se incrementa i, por lo que se decrementa la función (toma los valores 26, 19, 0). La condición de permanencia en el bucle es precisamente la que impone la función de decremento, \(f(i,n) > 0\), es decir, \(n < i^3\).

Otra forma de iteración soportada por Python es el bucle for. En este caso una variable toma una secuencia de valores que se indica. La forma general es:

for variable in iterable:
    bloque_del_bucle

Un iterable no es más que una secuencia de valores. La variable de control del bucle toma en cada iteración uno de los valores contenidos en el iterable. El bloque_del_bucle se ejecuta tantas veces como valores tenga el iterable. En cada iteración la variable de control toma uno de estos valores, lo que puede utilizarse para variar el comportamiento del bloque. Por ejemplo:

Prueba a cambiar la lista de valores, poniendo elementos desordenados e incluso cambiando el tipo de los elementos.

Para crear secuencias de valores es muy conveniente el uso de la función range. Esta función devuelve un iterable que contiene un conjunto de números enteros consecutivos.

Este rango contiene todos los valores desde 0 hasta el límite marcado sin contarlo. No es una lista, no podemos verlo imprimiendo sin más. Pero podemos verlo usando nuevamente el operador * para expandir el rango:

También podemos especificar los límites inferior y superior. El límite inferior está incluído en el rango.

Por último se puede especificar el incremento, de manera que solo se incluya uno de cada n números del rango. Por ejemplo:

Con range es muy sencillo construir bucles for.

Ambos tipos de bucle pueden utilizarse en la mayoría de las situaciones. Es quizás más sencillo buscar la función de decremento en el caso del while pero también suele ser algo más largo. Cuál usar es cuestión de gustos o conveniencia. Para recorrer elementos en una secuencia el for seguramente será más apropiado, mientras que para hacer un número de iteraciones que depende de los valores calculados el while es más natural.

Veamos el ejemplo de la raiz cúbica con for.

La sentencia break interrumpe el bucle. Es decir, sale del bucle que está ejecutando y continúa con la siguiente sentencia.

Las cadenas de texto también pueden ser recorridas carácter a carácter con un bucle for.

El bucle for tiene otra forma interesante, con ayuda de la función enumerate, en la que además de recorrer los elementos de la secuencia también recorre las posiciones de esos elementos. Fíjate bien, ahora tenemos dos variables de control.

Aún hay otra forma de for que resulta muy útil. Se utiliza con la función zip cuando queremos recorrer dos iterables de manera sincronizada. Es decir, cuando tenemos que recorrer los dos primeros elementos de cada iterable, después los dos segundos, etc.

Esto es suficiente para completar con facilidad todos los ejercicios del curso. Evidentemente debes entrenar todas las formas de iteración vistas, así que procura hacer los ejercicios que se piden a continuación.

Recuerda que los ejercicios tienen una función similar a los ejercicios deportivos. No se trata de hacerlos, sino de perfeccionar su ejecución y plantearse retos. Por ejemplo, después de hacer un ejercicio con un for prueba a hacerlo con un while.

4.3. Ejercicios

        Q-4: Reordena las líneas para que el programa imprima la suma de los 10
números que se introduzcan por pantalla.suma = 0
for a in range(10):
    suma = suma + int(input('Introduce un numero '))
print(suma)

Nota

Para que el usuario pueda introducir texto por pantalla se usa la función input que devuelve una cadena de texto. Fíjate bien en cómo convertimos el resultado de input a un entero usando la función int. ¿Qué pasaría si lo que metemos no es un entero?

Vamos a enfrentarnos por primera vez a un programa de más de 5 líneas. Corrije el programa para que imprima la tabla de multiplicar completa (del 1 al 9). Solo tienes que poner la llamada a función adecuada en cada una de las líneas de comentario. Cada línea debe tener la forma n x i = resultado. Por ejemplo, 3 x 4 = 12.

Observa cómo escribimos el programa, desde lo más complejo hasta lo más simple. Empezamos con el problema que se pide (escribir las nueve tablas de multiplicar). Si en ese proceso nos surge otro problema (imprimir una tabla de multiplicar) lo asumimos resuelto con una función, la que nosotros decidimos (imprime_tabla) con todos los argumentos que necesitemos. Después aplicamos el mismo proceso con todas las funciones que hayamos necesitado usar y no estén aún definidas. Este procedimiento se denomina habitualmente metodología top-down. Fuerza el pensamiento a ir de lo más abstracto a lo más concreto.

No lo hagas al revés (también se conoce como metodología bottom-up). No anticipes las funciones que vas a necesitar antes de realmente necesitarlas. No es que sea imposible hacerlo así pero requiere mucha más experiencia que aún no tienes. La metodología bottom-up solo se utiliza cuando las funciones de más bajo nivel de abstracción vienen impuestas a priori, normalmente porque ya han sido realizadas antes.

Truco

Aunque debes tender a utilizar diseño top-down en la mayor parte de tus programas es prácticamente imposible que todo el programa sea realizado de esta forma. Vas a utilizar continuamente funciones de la biblioteca estándar, eso es bottom-up. No desesperes, los nombres no importan. Importa que sepas por qué te interesa una u otra metodología. Empezar en lo más abstracto e ir descomponiendo el programa hacia lo más concreto es lo que mejor se adapta a la forma de pensar de los seres humanos. Pero si tú conoces una biblioteca que resuelve parcial o totalmente tu problema no dudes en aprovecharla.

No hay una única solución para un problema. Por ejemplo, volvamos a la tabla de multiplicar. Considera esta otra forma de resolverla.

No te quedes mirando, lee, experimenta y cambia lo que necesites hasta entenderlo completamente. Como ves hemos seguido la misma metodología top-down de antes.

Vamos a seguir explorando la construcción de bucles. Un ejercicio frecuente (también en los exámenes) consiste en dibujar en la pantalla empleando caracteres normales. Por ejemplo, considera este cuadrado.

+----------------+
|                |
|                |
|                |
|                |
|                |
|                |
|                |
|                |
+----------------+
Modifica el siguiente programa para que imprima el cuadrado que se muestra sobre estas líneas.

Permíteme insistir en la metodología top-down. Es muy importante en los programas reales. Para ilustrarlo veamos un ejemplo más. Completa las funciones para que valide una contraseña según estos criterios:

  • La contraseña debe contener un mínimo de 8 caracteres.
  • Una contraseña debe contener letras minúsculas, mayúsculas, números y al menos 1 carácter no alfanumérico.
  • La contraseña no puede contener espacios en blanco.
  • Contraseña válida, retorna True, contraseña no válida, retorna False.

¿No es muy repetitivo? Las validaciones de tipos de caracteres son prácticamente iguales. Solo se diferencian en la función que determina el tipo de cada caracter. Por tanto para no repetir código se puede pasar como parámetro.

Si, efectivamente, las funciones también se pueden pasar como parámetro o devolver como resultado. Tenlo siempre presente porque abre un amplio abanico de nuevas posibilidades.

    Q-5: ¿Cuál de las siguientes expresiones es equivalente a enumerate(L)?
  • (A) zip(len(L), L)
  • El primer argumento de zip no es iterable, es simplemente una longitud.
  • (B) zip(L,range(len(L)))
  • No, es al revés. La función enumerate(L) genera pares en los que el primer elemento es la posición.
  • (C) zip(range(len(L)), L)
  • Exacto. Es correcto, pero todavía sería más eficiente usando itertools.count y la expresión zip(count(),L). Busca la documentación de itertools.count para entender cómo funciona.
  • (D) zip(L,L)
  • No es así. Compruébalo en una ventana de intérprete.
Next Section - 5. Abstracción con funciones