Condicionales

../../../_images/ali-nafezarefi-62H_swdrc4A-unsplash.jpg

En esta sección veremos las sentencias if y match-case junto a las distintas variantes que pueden asumir, pero antes de eso introduciremos algunas cuestiones generales de escritura de código. 1

Definición de bloques

A diferencia de otros lenguajes que utilizan llaves para definir los bloques de código, cuando Guido Van Rossum creó el lenguaje quiso evitar estos caracteres por considerarlos innecesarios. Es por ello que en Python los bloques de código se definen a través de espacios en blanco, preferiblemente 4. 2 En términos técnicos se habla del tamaño de indentación.

../../../_images/four-spaces.png

Python recomienda 4 espacios en blanco para indentar

Consejo

Esto puede resultar extraño e incómodo a personas que vienen de otros lenguajes de programación pero desaparece rápido y se siente natural a medida que se escribe código.

Comentarios

Los comentarios son anotaciones que podemos incluir en nuestro programa y que nos permiten aclarar ciertos aspectos del código. Estas indicaciones son ignoradas por el intérprete de Python.

Los comentarios se incluyen usando el símbolo almohadilla # y comprenden hasta el final de la línea.

Comentario en bloque
# Universe age expressed in days
universe_age = 13800 * (10 ** 6) * 365

Los comentarios también pueden aparecer en la misma línea de código, aunque la guía de estilo de Python no aconseja usarlos en demasía:

Comentario en línea
stock = 0   # Release additional articles

Reglas para escribir buenos comentarios: 6

  1. Los comentarios no deberían duplicar el código.

  2. Los buenos comentarios no arreglan un código poco claro.

  3. Si no puedes escribir un comentario claro, puede haber un problema en el código.

  4. Los comentarios deberían evitar la confusión, no crearla.

  5. Usa comentarios para explicar código no idiomático.

  6. Proporciona enlaces a la fuente original del código copiado.

  7. Incluye enlaces a referencias externas que sean de ayuda.

  8. Añade comentarios cuando arregles errores.

  9. Usa comentarios para destacar implementaciones incompletas.

Ancho del código

Los programas suelen ser más legibles cuando las líneas no son excesivamente largas. La longitud máxima de línea recomendada por la guía de estilo de Python es de 80 caracteres.

Sin embargo, esto genera una cierta polémica hoy en día, ya que los tamaños de pantalla han aumentado y las resoluciones son mucho mayores que hace años. Así las líneas de más de 80 caracteres se siguen visualizando correctamente. Hay personas que son más estrictas en este límite y otras más flexibles.

En caso de que queramos romper una línea de código demasiado larga, tenemos dos opciones:

  1. Usar la barra invertida \:

    >>> factorial = 4 * 3 * 2 * 1
    
    >>> factorial = 4 * \
    ...             3 * \
    ...             2 * \
    ...             1
    
  2. Usar los paréntesis (...):

    >>> factorial = 4 * 3 * 2 * 1
    
    >>> factorial = (4 *
    ...              3 *
    ...              2 *
    ...              1)
    

La sentencia if

La sentencia condicional en Python (al igual que en muchos otros lenguajes de programación) es if. En su escritura debemos añadir una expresión de comparación terminando con dos puntos al final de la línea. Veamos un ejemplo:

>>> temperature = 40

>>> if temperature > 35:
...     print('Aviso por alta temperatura')
...
Aviso por alta temperatura

Nota

Nótese que en Python no es necesario incluir paréntesis ( y ) al escribir condiciones. Hay veces que es recomendable por claridad o por establecer prioridades.

En el caso anterior se puede ver claramente que la condición se cumple y por tanto se ejecuta la instrucción que tenemos dentro del cuerpo de la condición. Pero podría no ser así. Para controlar ese caso existe la sentencia else. Veamos el mismo ejemplo anterior pero añadiendo esta variante:

>>> temperature = 20

>>> if temperature > 35:
...     print('Aviso por alta temperatura')
... else:
...     print('Parámetros normales')
...
Parámetros normales

Podríamos tener incluso condiciones dentro de condiciones, lo que se viene a llamar técnicamente condiciones anidadas 3. Veamos un ejemplo ampliando el caso anterior:

>>> temperature = 28

>>> if temperature < 20:
...     if temperature < 10:
...         print('Nivel azul')
...     else:
...         print('Nivel verde')
... else:
...     if temperature < 30:
...         print('Nivel naranja')
...     else:
...         print('Nivel rojo')
...
Nivel naranja

Python nos ofrece una mejora en la escritura de condiciones anidadas cuando aparecen consecutivamente un else y un if. Podemos sustituirlos por la sentencia elif:

../../../_images/elif.png

Construcción de la sentencia elif

Apliquemos esta mejora al código del ejemplo anterior:

>>> temperature = 28

>>> if temperature < 20:
...     if temperature < 10:
...         print('Nivel azul')
...     else:
...         print('Nivel verde')
... elif temperature < 30:
...     print('Nivel naranja')
... else:
...     print('Nivel rojo')
...
Nivel naranja

Ejecución paso a paso a través de Python Tutor:

Asignaciones condicionales

Nivel intermedio

Supongamos que queremos asignar un nivel de riesgo de incendio en función de la temperatura. En su versión clásica escribiríamos:

>>> temperature = 35

>>> if temperature < 30:
...     fire_risk = 'LOW'
... else:
...     fire_risk = 'HIGH'
...

>>> fire_risk
'HIGH'

Sin embargo, esto lo podríamos abreviar con una asignación condicional de una única línea:

>>> fire_risk = 'LOW' if temperature < 30 else 'HIGH'

>>> fire_risk
'HIGH'

Operadores de comparación

Cuando escribimos condiciones debemos incluir alguna expresión de comparación. Para usar estas expresiones es fundamental conocer los operadores que nos ofrece Python:

Operador

Símbolo

Igualdad

==

Desigualdad

!=

Menor que

<

Menor o igual que

<=

Mayor que

>

Mayor o igual que

>=

A continuación vamos a ver una serie de ejemplos con expresiones de comparación. Téngase en cuenta que estas expresiones habría que incluirlas dentro de la sentencia condicional en el caso de que quisiéramos tomar una acción concreta:

# Asignación de valor inicial
>>> value = 8

>>> value == 8
True

>>> value != 8
False

>>> value < 12
True

>>> value <= 7
False

>>> value > 4
True

>>> value >= 9
False

Python ofrece la posibilidad de ver si un valor está entre dos límites de manera directa. Así, por ejemplo, para descubrir si x está entre 4 y 12 haríamos:

>>> 4 <= x <= 12
True

Operadores lógicos

Podemos escribir condiciones más complejas usando los operadores lógicos:
  • and

  • or

  • not

# Asignación de valor inicial
>>> x = 8

>>> x > 4 or x > 12  # True or False
True

>>> x < 4 or x > 12  # False or False
False

>>> x > 4 and x > 12  # True and False
False

>>> x > 4 and x < 12  # True and True
True

>>> not(x != 8)  # not False
True

Véanse las tablas de la verdad para cada operador lógico:

../../../_images/truth-tables.png

Resultados al aplicar operadores lógicos

Nota

  1. Una expresión de comparación siempre devuelve un valor booleano, es decir True o False.

  2. El uso de paréntesis, en función del caso, puede aclarar la expresión de comparación.

Ejercicio

pycheck: leap_year

Cortocircuito lógico

Es interesante comprender que las expresiones lógicas no se evalúan por completo si se dan una serie de circunstancias. Aquí es donde entra el concepto de cortocircuito que no es más que una forma de denominar a este escenario.

Supongamos un ejemplo en el que utilizamos un teléfono móvil que mide la batería por la variable power de 0 a 100% y la cobertura 4G por la variable signal_4g de 0 a 100%.

Para poder enviar un mensaje por Telegram necesitamos tener al menos un 25% de batería y al menos un 10% de cobertura:

>>> power = 10
>>> signal_4g = 60

>>> power > 25 and signal_4g > 10
False
../../../_images/shortcircuit-and.jpg

Cortocircuito para expresión lógica «and»

Dado que estamos en un and y la primera condición power > 25 no se cumple, se produce un cortocircuito y no se sigue evaluando el resto de la expresión porque ya se sabe que va a dar False.

Otro ejemplo. Para poder hacer una llamada VoIP necesitamos tener al menos un 40% de batería o al menos un 30% de cobertura:

>>> power = 50
>>> signal_4g = 20

>>> power > 40 or signal_4g > 30
True
../../../_images/shortcircuit-or.jpg

Cortocircuito para expresión lógica «or»

Dado que estamos en un or y la primera condición power > 40 se cumple, se produce un cortocircuito y no se sigue evaluando el resto de la expresión porque ya se sabe que va a dar True.

Nota

Si no se produjera un cortocircuito en la evaluación de la expresión, se seguiría comprobando todas las condiciones posteriores hasta llegar al final de la misma.

«Booleanos» en condiciones

Cuando queremos preguntar por la veracidad de una determinada variable «booleana» en una condición, la primera aproximación que parece razonable es la siguiente:

>>> is_cold = True

>>> if is_cold == True:
...     print('Coge chaqueta')
... else:
...     print('Usa camiseta')
...
Coge chaqueta

Pero podemos simplificar esta condición tal que así:

>>> if is_cold:
...     print('Coge chaqueta')
... else:
...     print('Usa camiseta')
...
Coge chaqueta

Hemos visto una comparación para un valor «booleano» verdadero (True). En el caso de que la comparación fuera para un valor falso lo haríamos así:

>>> is_cold = False

>>> if not is_cold:  # Equivalente a if is_cold == False
...     print('Usa camiseta')
... else:
...     print('Coge chaqueta')
...
Usa camiseta

De hecho, si lo pensamos, estamos reproduciendo bastante bien el lenguaje natural:

  • Si hace frío, coge chaqueta.

  • Si no hace frío, usa camiseta.

Ejercicio

pycheck: marvel_akinator

Valor nulo

Nivel intermedio

None es un valor especial de Python que almacena el valor nulo 4. Veamos cómo se comporta al incorporarlo en condiciones de veracidad:

>>> value = None

>>> if value:
...     print('Value has some useful value')
... else:
...     # value podría contener None, False (u otro)
...     print('Value seems to be void')
...
Value seems to be void

Para distinguir None de los valores propiamente booleanos, se recomienda el uso del operador is. Veamos un ejemplo en el que tratamos de averiguar si un valor es nulo:

>>> value = None

>>> if value is None:
...     print('Value is clearly None')
... else:
...     # value podría contener True, False (u otro)
...     print('Value has some useful value')
...
Value is clearly None

De igual forma, podemos usar esta construcción para el caso contrario. La forma «pitónica» de preguntar si algo no es nulo es la siguiente:

>>> value = 99

>>> if value is not None:
...     print(f'{value=}')
...
value=99

Nivel avanzado

Cabe preguntarse por qué utilizamos is en vez del operador == al comprobar si un valor es nulo, ya que ambas aproximaciones nos dan el mismo resultado 7:

>>> value = None

>>> value is None
True

>>> value == None
True

La respuesta es que el operador is comprueba únicamente si los identificadores (posiciones en memoria) de dos objetos son iguales, mientras que la comparación == puede englobar muchas otras acciones. De este hecho se deriva que su ejecución sea mucho más rápida y que se eviten «falsos positivos».

Cuando ejecutamos un programa Python existe una serie de objetos precargados en memoria. Uno de ellos es None:

>>> id(None)
4314501456

Cualquier variable que igualemos al valor nulo, únicamente será una referencia al mismo objeto None en memoria:

>>> value = None

>>> id(value)
4314501456

Por lo tanto, ver si un objeto es None es simplemente comprobar que su identificador coincida con el de None, que es exactamente el cometido de la función is():

>>> id(value) == id(None)
True

>>> value is None
True

Truco

Python carga inicialmente en memoria objetos como True o False, pero también los números enteros que van desde el -5 hasta el 256. Se entiende que tiene que ver con optimizaciones a nivel de rendimiento.

Veracidad

Nivel intermedio

Cuando trabajamos con expresiones que incorporan valores booleanos, se produce una conversión implícita que transforma los tipos de datos involucrados a valores True o False.

Lo primero que debemos entender de cara comprobar la veracidad son los valores que evalúan a falso o evalúan a verdadero.

Veamos las únicas «cosas» que son evaluadas a False en Python:

>>> bool(False)
False

>>> bool(None)
False

>>> bool(0)
False

>>> bool(0.0)
False

>>> bool('')  # cadena vacía
False

>>> bool([])  # lista vacía
False

>>> bool(())  # tupla vacía
False

>>> bool({})  # diccionario vacío
False

>>> bool(set())  # conjunto vacío
False

Importante

El resto de objetos son evaluados a True en Python.

Veamos algunos ejemplos que son evaluados a True en Python:

>>> bool('False')
True

>>> bool(' ')
True

>>> bool(1e-10)
True

>>> bool([0])
True

>>> bool('🦆')
True

Asignación lógica

Es posible utilizar operadores lógicos en sentencias de asignación sacando partido de las tablas de la verdad que funcionan para estos casos.

Veamos un ejemplo de asignación lógica utilizando el operador or:

>>> b = 0
>>> c = 5

>>> a = b or c

>>> a
5

En la línea resaltada podemos ver que se está aplicando una expresión lógica, por lo tanto se aplica una conversión implícita de los valores enteros a valores «booleanos». En este sentido el valor 0 se evalúa a falso y el valor 5 se evalúa a verdadero. Como estamos en un or el resultado será verdadero, que en este caso es el valor 5 asignado finalmente a la variable a.

Veamos el mismo ejemplo de antes pero utilizando el operador and:

>>> b = 0
>>> c = 5

>>> a = b and c

>>> a
0

En este caso, como estamos en un and el resultado será falso, por lo que el valor 0 es asignado finalmente a la variable a.

Sentencia match-case

Una de las novedades más esperadas (y quizás controvertidas) de Python 3.10 fue el llamado Structural Pattern Matching que introdujo en el lenguaje una nueva sentencia condicional. Ésta se podría asemejar a la sentencia «switch» que ya existe en otros lenguajes de programación.

Comparando valores

En su versión más simple, el «pattern matching» permite comparar un valor de entrada con una serie de literales. Algo así como un conjunto de sentencias «if» encadenadas. Veamos esta aproximación mediante un ejemplo:

>>> color = '#FF0000'

>>> match color:
...     case '#FF0000':
...         print('🔴')
...     case '#00FF00':
...         print('🟢')
...     case '#0000FF':
...         print('🔵')
...
🔴

¿Qué ocurre si el valor que comparamos no existe entre las opciones disponibles? Pues en principio, nada, ya que este caso no está cubierto. Si lo queremos controlar, hay que añadir una nueva regla utilizando el subguión _ como patrón:

>>> color = '#AF549B'

>>> match color:
...     case '#FF0000':
...         print('🔴')
...     case '#00FF00':
...         print('🟢')
...     case '#0000FF':
...         print('🔵')
...     case _:
...         print('Unknown color!')
...
Unknown color!

Ejercicio

pycheck: simple_op

Patrones avanzados

Nivel avanzado

La sentencia match-case va mucho más allá de una simple comparación de valores. Con ella podremos deconstruir estructuras de datos, capturar elementos o mapear valores.

Para ejemplificar varias de sus funcionalidades, vamos a partir de una tupla que representará un punto en el plano (2 coordenadas) o en el espacio (3 coordenadas). Lo primero que vamos a hacer es detectar en qué dimensión se encuentra el punto:

>>> point = (2, 5)

>>> match point:
...     case (x, y):
...         print(f'({x},{y}) is in plane')
...     case (x, y, z):
...         print(f'({x},{y},{z}) is in space')
...
(2,5) is in plane

>>> point = (3, 1, 7)

>>> match point:
...     case (x, y):
...         print(f'({x},{y}) is in plane')
...     case (x, y, z):
...         print(f'({x},{y},{z}) is in space')
...
(3,1,7) is in space

En cualquier caso, esta aproximación permitiría un punto formado por «strings»:

>>> point = ('2', '5')

>>> match point:
...     case (x, y):
...         print(f'({x},{y}) is in plane')
...     case (x, y, z):
...         print(f'({x},{y},{z}) is in space')
...
(2,5) is in plane

Por lo tanto, en un siguiente paso, podemos restringir nuestros patrones a valores enteros:

>>> point = ('2', '5')

>>> match point:
...     case (int(), int()):
...         print(f'{point} is in plane')
...     case (int(), int(), int()):
...         print(f'{point} is in space')
...     case _:
...         print('Unknown!')
...
Unknown!

>>> point = (3, 9, 1)

>>> match point:
...     case (int(), int()):
...         print(f'{point} is in plane')
...     case (int(), int(), int()):
...         print(f'{point} is in space')
...     case _:
...         print('Unknown!')
...
(3, 9, 1) is in space

Imaginemos ahora que nos piden calcular la distancia del punto al origen. Debemos tener en cuenta que, a priori, desconocemos si el punto está en el plano o en el espacio:

>>> point = (8, 3, 5)

>>> match point:
...     case (int(x), int(y)):
...         dist_to_origin = (x ** 2 + y ** 2) ** (1 / 2)
...     case (int(x), int(y), int(z)):
...         dist_to_origin = (x ** 2 + y ** 2 + z ** 2) ** (1 / 2)
...     case _:
...         print('Unknown!')
...

>>> dist_to_origin
9.899494936611665

Con este enfoque, nos aseguramos que los puntos de entrada deben tener todas sus coordenadas como valores enteros:

>>> point = ('8', 3, 5)  # Nótese el 8 como "string"

>>> match point:
...     case (int(x), int(y)):
...         dist_to_origin = (x ** 2 + y ** 2) ** (1 / 2)
...     case (int(x), int(y), int(z)):
...         dist_to_origin = (x ** 2 + y ** 2 + z ** 2) ** (1 / 2)
...     case _:
...         print('Unknown!')
...
Unknown!

Cambiando de ejemplo, veamos un fragmento de código en el que tenemos que comprobar la estructura de un bloque de autenticación definido mediante un diccionario. Los métodos válidos de autenticación son únicamente dos: bien usando nombre de usuario y contraseña, o bien usando correo electrónico y «token» de acceso. Además, los valores deben venir en formato cadena de texto:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
>>> # Lista de diccionarios
>>> auths = [
...     {'username': 'sdelquin', 'password': '1234'},
...     {'email': 'sdelquin@gmail.com', 'token': '4321'},
...     {'email': 'test@test.com', 'password': 'ABCD'},
...     {'username': 'sdelquin', 'password': 1234}
... ]

>>> for auth in auths:
...     print(auth)
...     match auth:
...         case {'username': str(username), 'password': str(password)}:
...             print('Authenticating with username and password')
...             print(f'{username}: {password}')
...         case {'email': str(email), 'token': str(token)}:
...             print('Authenticating with email and token')
...             print(f'{email}: {token}')
...         case _:
...             print('Authenticating method not valid!')
...     print('---')
...
{'username': 'sdelquin', 'password': '1234'}
Authenticating with username and password
sdelquin: 1234
---
{'email': 'sdelquin@gmail.com', 'token': '4321'}
Authenticating with email and token
sdelquin@gmail.com: 4321
---
{'email': 'test@test.com', 'password': 'ABCD'}
Authenticating method not valid!
---
{'username': 'sdelquin', 'password': 1234}
Authenticating method not valid!
---

Cambiando de ejemplo, a continuación veremos un código que nos indica si, dada la edad de una persona, puede beber alcohol:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
>>> age = 21

>>> match age:
...     case 0 | None:
...         print('Not a person')
...     case n if n < 17:
...         print('Nope')
...     case n if n < 22:
...         print('Not in the US')
...     case _:
...         print('Yes')
...
Not in the US
  • En la línea 4 podemos observar el uso del operador OR.

  • En las líneas 6 y 8 podemos observar el uso de condiciones dando lugar a cláusulas guarda.

Operador morsa

Nivel avanzado

A partir de Python 3.8 se incorpora el operador morsa 5 que permite unificar sentencias de asignación dentro de expresiones. Su nombre proviene de la forma que adquiere :=

Supongamos un ejemplo en el que computamos el perímetro de una circunferencia, indicando al usuario que debe incrementarlo siempre y cuando no llegue a un mínimo establecido.

Versión tradicional

>>> radius = 4.25
... perimeter = 2 * 3.14 * radius
... if perimeter < 100:
...     print('Increase radius to reach minimum perimeter')
...     print('Actual perimeter: ', perimeter)
...
Increase radius to reach minimum perimeter
Actual perimeter:  26.69

Versión con operador morsa

>>> radius = 4.25
... if (perimeter := 2 * 3.14 * radius) < 100:
...     print('Increase radius to reach minimum perimeter')
...     print('Actual perimeter: ', perimeter)
...
Increase radius to reach minimum perimeter
Actual perimeter:  26.69

Consejo

Como hemos comprobado, el operador morsa permite realizar asignaciones dentro de expresiones, lo que, en muchas ocasiones, permite obtener un código más compacto. Sería conveniente encontrar un equilibrio entre la expresividad y la legibilidad.


EJERCICIOS DE REPASO

  1. pycheck: rps

  2. pycheck: min3values

  3. pycheck: blood_donation

  4. pycheck: facemoji

  5. pycheck: shortcuts

EJERCICIOS EXTERNOS

  1. Return the day

  2. Return negative

  3. What’s the real floor?

  4. Area or Perimeter

  5. Check same case

  6. Simple multiplication

  7. Quarter of the year

  8. Grade book

  9. Transportation on vacation

  10. Safen User Input Part I - htmlspecialchars

  11. Remove an exclamation mark from the end of string

  12. Pythagorean triple

  13. How much water do I need?

  14. Set Alarm

  15. Compare within margin

  16. Will you make it?

  17. Plural

  18. Student’s final grade

  19. Drink about

  20. Switch it up!

  21. Floating point comparison

  22. No zeros for heros

  23. Tip calculator

  24. Grader

  25. Evil or Odious

  26. Validate code with simple regex

  27. Fuel calculator

AMPLIAR CONOCIMIENTOS

1

Foto original de portada por ali nafezarefi en Unsplash.

2

Reglas de indentación definidas en PEP 8

3

El anidamiento (o «nesting») hace referencia a incorporar sentencias unas dentro de otras mediante la inclusión de diversos niveles de profunidad (indentación).

4

Lo que en otros lenguajes se conoce como nil, null, nothing.

5

Se denomina así porque el operador := tiene similitud con los colmillos de una morsa.

6

Referencia: Best practices for writing code comments

7

Uso de is en comparación de valores nulos explicada aquí por Jared Grubb.