Saltar a contenido

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_patrones.py
# 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:

Terminal (Entrada/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ón ordenar_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í:

ordenar_herramientas_por_patrones.py
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:

Terminal (Entrada/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:

ordenar_herramientas_por_patrones.py
 
        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?

ordenar_herramientas_por_patrones.py
 
        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:

ordenar_herramientas_por_patrones.py
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 siguiente case, imprimiendo "Coloca tu pinza en el compartimento de llaves", y luego continuaría con el siguiente case, 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:

ordenar_herramientas_por_patrones.py
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:

Terminal (Entrada/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ón reglas_de_divisibilidad(numero, divisor=2, divisible=False) para comparar el valor de divisor 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 variable divisible 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.