Coincidencia de patrones
La coincidencia de patrones es una estructura de control introducida en Python 3.10 que permite comparar un valor contra una serie de patrones y ejecutar el bloque de código correspondiente al primer patrón que coincida.
¿Qué es un patrón?
Un patrón es una estructura que se utiliza para comparar y verificar si un valor o una estructura de datos coincide con una forma específica.
Los patrones pueden ser valores literales, estructuras de datos como listas o diccionarios, o incluso más complejos como clases y objetos.
No vamos a estudiar en detalle los patrones en esta unidad, pero es importante que entiendas que un patrón es una forma específica que se utiliza para comparar y verificar si un valor coincide con esa forma.
La palabra clave match
La palabra clave match
fue introducida en Python 3.10 como parte de la nueva estructura de control llamada "pattern matching" en inglés (coincidencia de patrones).
Esta estructura permite comparar un valor contra una serie de patrones y ejecutar el bloque de código correspondiente al primer patrón que coincida.
match
funciona de manera similar a switch
en otros lenguajes de programación, pero con una sintaxis más clara y concisa. Si no conoces el funcionamiento de switch
, no te preocupes, ya que te explicaré cómo funciona match
en Python y podrás comprenderlo.
Veamos un ejemplo simple de cómo se utiliza match
en Python. Supongamos que queremos ordenar nuestro juego de herramientas y para ello disponemos de cuatro compartimentos en nuestra caja de herramientas: uno para destornilladores, otro para pinzas, otro para llaves y otro para el resto de las herramientas. Por el momento contamos con 3 herramientas: destornillador, llave y martillo.
Y queremos saber donde va guardada cada una de ellas.
# Ordenar herramientas por compartimentos
def ordenar_herramientas(herramienta):
match herramienta:
case "destornillador":
print(f"Coloca tu {herramienta} en el compartimento de destornilladores")
case "pinza":
print(f"Coloca tu {herramienta} en el compartimento de pinzas")
case "llave":
print(f"Coloca tu {herramienta} en el compartimento de llaves")
case _:
print(f"Coloca tu {herramienta} en el compartimento de herramientas generales")
# Ordenar las herramientas
ordenar_herramientas("destornillador")
ordenar_herramientas("llave")
ordenar_herramientas("martillo")
Si ejecutamos este código, obtendremos la siguiente salida:
Coloca tu destornillador en el compartimento de destornilladores
Coloca tu llave en el compartimento de llaves
Coloca tu martillo en el compartimento de herramientas generales
En este ejemplo, utilizamos la palabra clave
match
dentro de la funciónordenar_herramientas(herramienta)
para comparar el valor de cada argumento recibido con una serie de patrones. De acuerdo con el reultado de cada comparación, se imprime en pantalla la respuesta correspondiente.
¡Mágico!
¿Pero cómo es que se logra este comportamiento del programa?
La palabra clave case
Dentro de un bloque match
, utilizamos la palabra clave case
para definir los patrones que queremos comparar.
Cada case
contiene un patrón y un bloque de código que se ejecutará si el valor coincide con ese patrón.
En nuestro programa tenemos un case
que compara el valor destornillador
con el patrón "destornillador"
, otro case
que compara el valor pinza
con el patrón "pinza"
y otro case
que compara el valor llave
con el patrón "llave"
.
Cada vez que se encuentra una coincidencia, se ejecuta el bloque de código correspondiente y se sale del bloque match
.
¿Pero qué ocurre si no existe ninguna coincidencia?
case _
por defecto
Si ninguno de los patrones definidos coincide con el valor a comparar, podemos utilizar el case
por defecto para manejar este caso.
La estructura alternativa match
posee un case
especial que se representa con un guion bajo _
y se utiliza para manejar cualquier valor que no haya coincidido con los patrones anteriores. Podemos decir que el símbolo _
es un "comodín".
De esta manera, el case _
actúa como un else
en una estructura condicional tradicional, manejando cualquier otro caso que no haya sido considerado en los case
anteriores.
Así, en nuestro programa, el case _
se encarga de manejar cualquier valor que no sea destornillador
, pinza
o llave
, colocando la herramienta en el compartimento de herramientas generales.
Hasta aquí nuestro programa es un éxito. Funciona. Pero, no es escalable ni modular.
¿Qué pasa si compramos más herramientas diferentes a las que ya tenemos?
Por ejemplo, nuestra llave es una llave inglesa. Pero luego adquirimos una llave francesa. Ahora poseemos dos tipos de llaves que deberemos guardar luego de usarlas.
Entonces ¿Deberíamos modificar la alternativa case
que evalúa si la herramienta es una llave para que ahora evalúe si es la llave inglesa y, además, agregar una nueva alternativa case
que evalúe si es la llave francesa , no?
Algo así:
def ordenar_herramientas(herramienta):
match herramienta:
case "destornillador":
print(f"Coloca tu {herramienta} en el compartimento de destornilladores")
case "pinza":
print(f"Coloca tu {herramienta} en el compartimento de pinzas")
case "llave inglesa":
print(f"Coloca tu {herramienta} en el compartimento de llaves")
case "llave francesa":
print(f"Coloca tu {herramienta} en el compartimento de llaves")
case _:
print(f"Coloca tu {herramienta} en el compartimento de herramientas generales")
# Ordenar las herramientas
ordenar_herramientas("destornillador")
ordenar_herramientas("llave francesa")
ordenar_herramientas("llave inglesa")
ordenar_herramientas("martillo")
Si ejecutamos nuestro programa con estas modificaciones, obtendremos la siguiente salida:
Coloca tu destornillador en el compartimento de destornilladores
Coloca tu llave francesa en el compartimento de llaves
Coloca tu llave inglesa en el compartimento de llaves
Coloca tu martillo en el compartimento de herramientas generales
Ahora tenemos dos alternativas
case
para la llave.
Pero, ¿no es redundante? Sin importar que llave sea, la herramienta va a guardarse en el mismo compartimento. Y si adquirimos más tipos de llaves, deberemos seguir agregando más alternativas case
para cada una de ellas.
Con este enfoque, nuestro código se tornaría más largo y difícil de comprender y mantener.
Entonces, ¿No hay una manera más eficiente de resolver este problema?
case
de coincidencia múltiple
¡Claro que sí! Y es aquí donde entra en juego la alternativa case
de coincidencia múltiple.
Veamos como sería la modificación al fragmento de código:
⋮
case "llave inglesa":
print(f"Coloca tu {herramienta} en el compartimento de llaves")
case "llave francesa":
print(f"Coloca tu {herramienta} en el compartimento de llaves")
⋮
Aquí vemos que la salida en pantalla es la misma para ambos casos. Entonces, ¿por qué no simplificarlo?
⋮
case "llave inglesa" | "llave francesa":
print(f"Coloca tu {herramienta} en el compartimento de llaves")
⋮
Aquí vemos que la salida en pantalla es compartida para ambos casos.
Al utilizar el operador de comparación |
entre los patrones, estamos indicando que si el valor de herramienta
es igual a llave inglesa
o llave francesa
, se ejecute el bloque de código correspondiente.
El carácter |
se llama "barra vertical" (pipe en inglés).
Diferencias y similitudes con otros lenguajes
Debemos señalar que si has programado en algún otro lenguaje, seguramente te preguntarás que ocurrió con la palabra clave break
o la declaración de interrupción de la estructura match
luego de finalizar el case
correspondiente para evitar que continúe con el case
siguiente.
Si no sabes de que estamos hablando, no te preocupes. Solo queremos decir que la sintaxis en Python es correcta. No necesita de dicha interrupción.
En otros lenguajes donde esta estructura posee varios case
, se necesita una declaración de interrupción para evitar que continúe con el siguiente case
. Si lo pensamos con nuestro código:
def ordenar_herramientas(herramienta):
match herramienta:
case "destornillador":
print(f"Coloca tu {herramienta} en el compartimento de destornilladores")
case "pinza":
print(f"Coloca tu {herramienta} en el compartimento de pinzas")
case "llave inglesa" | "llave francesa":
print(f"Coloca tu {herramienta} en el compartimento de llaves")
case _:
print(f"Coloca tu {herramienta} en el compartimento de herramientas generales")
Si
herramienta
tuviera valor "pinza", se imprimiría en pantalla "Coloca tu pinza en el compartimento de pinzas", y luego continuaría con el siguientecase
, imprimiendo "Coloca tu pinza en el compartimento de llaves", y luego continuaría con el siguientecase
, imprimiendo "Coloca tu pinza en el compartimento de herramientas generales".
En cambio, en Python, ocurriría que si herramienta
tuviera valor "pinza", se imprimiría en pantalla "Coloca tu pinza en el compartimento de pinzas" y finalizaría la ejecución del bloque match
.
¡Es así de simple!
Por otra parte, no necesita tampoco una palabra clave diferente como default
para indicar el caso predeterminado o por descarte. En este caso, simplemente utiliza case _
como complemento al final de la estructura, tal como hemos visto.
Ejecución de múltiples case
en Python
¡Pero espera! ¿Qué pasa si queremos ejecutar múltiples case
en Python?
En Python, la coincidencia de patrones con match
no permite la ejecución de múltiples case
secuenciales como en algunos otros lenguajes de programación.
Sin embargo, podemos lograr un comportamiento similar llamando explícitamente a funciones o bloques de código adicionales dentro de un case
.
Veamos un ejemplo de como conseguir el mismo comportamiento que nos brinda la ejecución de múltiples case
en otros lenguajes de programación:
def reglas_de_divisibilidad(numero, divisor=2, divisible=False):
if divisor > 9:
if not divisible:
print(f"{numero} no es divisible por 2, 3, 4, 5, 6, 7, 8 o 9.")
print(f"\nFin de las reglas de divisibilidad para el número {numero}.", end="\n\n")
return
match divisor:
case 2 if numero % 2 == 0:
print(f"{numero} es divisible por 2: Es par.")
divisible = True
case 3 if numero % 3 == 0:
print(f"{numero} es divisible por 3: La suma de sus dígitos es divisible por 3.")
divisible = True
case 4 if numero % 4 == 0:
print(f"{numero} es divisible por 4: Sus dos últimos dígitos son divisibles por 4.")
divisible = True
case 5 if numero % 5 == 0:
print(f"{numero} es divisible por 5: Termina en 0 o 5.")
divisible = True
case 6 if numero % 6 == 0:
print(f"{numero} es divisible por 6: Es divisible por 2 y por 3.")
divisible = True
case 7 if numero % 7 == 0:
print(f"{numero} es divisible por 7: Duplica el último dígito, réstalo al número sin su último dígito y verifica si es múltiplo de 7.")
divisible = True
case 8 if numero % 8 == 0:
print(f"{numero} es divisible por 8: Sus tres últimos dígitos son divisibles por 8.")
divisible = True
case 9 if numero % 9 == 0:
print(f"{numero} es divisible por 9: La suma de sus dígitos es divisible por 9.")
divisible = True
# Llamada recursiva a la función para verificar si es divisible por el siguiente divisor.
reglas_de_divisibilidad(numero, divisor + 1, divisible)
# Ejemplo de uso
for i in range(2, 10):
print(f"Reglas de divisibilidad para el número {i}:")
print("-" * len(f"Reglas de divisibilidad para el número {i}:"))
reglas_de_divisibilidad(i)
Si ejecutamos este código, obtendremos la siguiente salida:
Reglas de divisibilidad para el número 2:
-----------------------------------------
2 es divisible por 2: Es par.
Fin de las reglas de divisibilidad para el número 2.
Reglas de divisibilidad para el número 3:
-----------------------------------------
3 es divisible por 3: La suma de sus dígitos es divisible por 3.
Fin de las reglas de divisibilidad para el número 3.
Reglas de divisibilidad para el número 4:
-----------------------------------------
4 es divisible por 2: Es par.
4 es divisible por 4: Sus dos últimos dígitos son divisibles por 4.
Fin de las reglas de divisibilidad para el número 4.
Reglas de divisibilidad para el número 5:
-----------------------------------------
5 es divisible por 5: Termina en 0 o 5.
Fin de las reglas de divisibilidad para el número 5.
Reglas de divisibilidad para el número 6:
-----------------------------------------
6 es divisible por 2: Es par.
6 es divisible por 3: La suma de sus dígitos es divisible por 3.
6 es divisible por 6: Es divisible por 2 y por 3.
Fin de las reglas de divisibilidad para el número 6.
Reglas de divisibilidad para el número 7:
-----------------------------------------
7 es divisible por 7: Duplica el último dígito, réstalo al número sin su último dígito y verifica si es múltiplo de 7.
Fin de las reglas de divisibilidad para el número 7.
Reglas de divisibilidad para el número 8:
-----------------------------------------
8 es divisible por 2: Es par.
8 es divisible por 4: Sus dos últimos dígitos son divisibles por 4.
8 es divisible por 8: Sus tres últimos dígitos son divisibles por 8.
Fin de las reglas de divisibilidad para el número 8.
Reglas de divisibilidad para el número 9:
-----------------------------------------
9 es divisible por 3: La suma de sus dígitos es divisible por 3.
9 es divisible por 9: La suma de sus dígitos es divisible por 9.
Fin de las reglas de divisibilidad para el número 9.
En este ejemplo, utilizamos la palabra clave
match
dentro de la funciónreglas_de_divisibilidad(numero, divisor=2, divisible=False)
para comparar el valor dedivisor
con una serie de patrones. De acuerdo con el resultado de cada comparación, se imprime en pantalla la respuesta correspondiente; y se modifica la variabledivisible
si el número es divisible por el divisor correspondiente.Luego, llamamos a la función
reglas_de_divisibilidad(numero, divisor + 1, divisible)
de manera recursiva para verificar si el número es divisible por el siguiente divisor, además de informarle a la función si el número ya es o no es divisible por otro divisor menor.Así, sucesivamente, hasta que el divisor sea mayor a 9, momento en el que finalizamos la ejecución de la función.
De esta manera, logramos ejecutar múltiples case
en Python, aunque no de manera secuencial como en otros lenguajes de programación, utilizando la recursividad para lograr el mismo comportamiento.
¡Y funciona!
Conclusión
Tanto en Python como en otros lenguajes de programación, podemos alcanzar la solución a un problema de diferentes maneras.
En este caso, hemos utilizado la coincidencia de patrones a través de match… case… case _
para implementar la misma idea que construye if… elif… else
.
Esto logra una sintaxis más clara y concisa, reduciendo la duplicación de signos iguales y elif
, y elif
, y elif
por todas partes.
Pero resulta que con una declaración de coincidencia también puedes crear formas de coincidencia aún más poderosas. Así que no dudes en explorar y experimentar con esta nueva estructura de control en Python.