Este octubre de 2021 está previsto que se libere la próxima versión del intérprete de Python. Se trata de la versión 3.10, que traerá varias novedades interesantes. Podemos ver el roadmap y los principales cambios previstos en el PEP 619.

Sin embargo, de todas las novedades hay una que destaca por ser una de las más esperadas y que lleva mucho tiempo en discusión dentro del equipo de desarrolladores del core de Python. Se trata de la característica de Structural Pattern Matching, que ha sido impulsada personalmente por Guido Van Rossum, el creador original del lenguaje y que, sin duda, va a dar mucho que hablar.

En este artículo vamos a analizar en qué consiste esta nueva posibilidad que nos ofrece Python e ilustrarlo con algunos ejemplos.

¿De qué se trata?

Pero, ¿qué es realmente el Structural Pattern Matching? Para hacernos una idea podemos pensar en una estructura de tipo switch al estilo de las que tenemos en C, Java u otros lenguajes, pero mucho más potente. Realmente la aproximación más cercana sería la estructura match, presente en el lenguaje Rust.

Para construir nuestra estructura en Python haremos uso de dos nuevas palabras en el lenguaje: match y case. Vemos un ejemplo básico para matching de códigos de error HTTP:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the Internet"

Vemos como con “match” definimos sobre qué variable queremos hacer la comparación y con “case” vamos definiendo las posibles alternativas de forma similar a un “switch” clásico. Anteriormente esto podría implementarse en Python con una secuencia de “if..elif..else” pero de este modo queda más elegante. Destacar que “_” actúa como “wildcard” o comodín para encajar con cualquier expresión que no encaje con el resto de opciones.

Más madera

Hasta aquí hemos visto un ejemplo básico, que no parece especialmente novedoso. Sin embargo, las posibilidades son mucho mayores y más potentes para poder hacer matching con diferentes estructuras y tipos de datos, lo que realmente convierte al “Structural Pattern Matching” en una herramienta muy conveniente en muchas circunstancias. Vemos algunos ejemplos más avanzados:

match my_list:
    case []:
        print("Lista vacía")
    case [x]:
        print(f"Lista de un elemento: {x}")
    case [1, 2] | [2, 1] | [1, 3] | [3, 1]:
        print(f"Estas combinaciones me interesan mucho")
    case [x, y]:
        print(f"Lista con dos elementos: {x} y {y}")
    case [x, y, z]:
        print(f"Lista con tres elementos: {x}, {y} y {z}")
    case [0, 1, 1, 2, *tail]:
        print(f"Parece que es la serie de Fibonacci...")
    case ["end", "of", "game"]:
        print(f"Se acabó el juego...")
    case [x, y, *tail]:
        print(f"Lista con más de tres elementos. Los dos primeros son: {x} y {y}")

En este ejemplo vemos como junto al case, podemos no solo poner literales o enteros, sino listas con diferente estructura. De esta forma podemos hacer un manejo muy cómodo y sencillo en función de la estructura y contenido de la lista recibida y ejecutar diferente lógica, sin la necesidad de usar la función len() ni acceder explícitamente a los elementos de la lista.

Como vemos en el tercer “case”, podemos usar el operador “|”, (OR) para declarar varias opciones que harían match en ese caso.

Con diccionarios también

Podemos operar de forma similar con diccionarios, jugando con su estructura y contenido:

match customer_data:
    case {"password": password, **personal_data}:
        customer.set_password(password=password)
        customer.update_personal_data(personal_data)
    case {"gdpr_check": True, "customer_id": cid}:
        apply_gdpr_policies(cid)
    case dict(x) if not x:
        raise Exception("no data to process")

En este ejemplo vemos diferentes casuísticas: en el primer case estamos verificando la presencia de una clave en concreto en el diccionario; en el segundo case, de una clave y un valor específicos; y en el tercero, si se trata de un diccionario vacío.

Este escenario de procesamiento condicionado al contenido de los diccionarios es muy habitual en muchas ocasiones, por ejemplo cuando procesamos documentos JSON, formularios o el contenido de peticiones HTTP. De esta forma podemos escribir estos matchings de una forma más sencilla, elegante y legible.

Incluso con nuestros propios objetos

No sólo podemos hacer uso de tipos de datos básicos de Python, sino que podemos “matchear” con objetos de clases definidas por nosotros mismos y ahorrarnos un montón de llamadas a la función isinstance().

match event.get():
    case Click(position=(x, y)):
        handle_click_at(x, y)
    case KeyPress(key_name="Q") | Quit():
        game.quit()
    case KeyPress(key_name="up arrow"):
        game.go_north()
    ...
    case KeyPress():
        pass # Ignore other keystrokes
    case other_event:
        raise ValueError(f"Unrecognized event: {other_event}")

Para ello, en el case declararemos la construcción del objeto con los argumentos correspondientes nombrados y de esta forma haremos matching.

También es posible no nombrar los argumentos del constructor y pasarlos de forma posicional, pero para ello tendremos que apoyarnos en el decorador dataclass de la biblioteca estándar de Python o definiendo el atributo match_args de cualquiera de nuestras clases:

from dataclasses import dataclass

@dataclass
class Pair:
    first: int
    second: int

pair = Pair(10, 20)
match pair:
    case Pair(0, x):
        print("Case #1")
    case Pair(x, y) if x == y:
        print("Case #2")
    case Pair(first=x, second=20):
        print("Case #3")
    case Pair as p:
        print("Case #4")

En este último ejemplo observamos también la posibilidad de añadir “guardas” o condicionales adicionales al matching. En el caso 2 vemos cómo se ha añadido una condición adicional y es que x sea igual a y a través de una construcción “if”. Esto nos permite llegar a un nivel de casuísticas y de control muy fino.

Conclusión

En resumen, la funcionalidad de Structural Pattern Matching enriquece y mejora un lenguaje como Python que está vivo y en constante evolución. Al tratarse de un cambio tan significativo, su desarrollo no ha estado exento de cierta polémica y ha contado con defensores y detractores, como podemos ver en algunos hilos y conversaciones de foros y listas de correo.

En cualquier caso, será la comunidad de usuarios de Python la que con el tiempo y la experiencia, juzgarán los beneficios de esta funcionalidad e iremos viendo si extiende su uso de forma masiva.

¿Estás deseando tener disponible Python 3.10 para probarlo? No te preocupes, Guido nos ha dejado un pequeño regalo en este entorno donde ya puedes probar las nuevas funcionalidades de Python 3.10 en un notebook de Jupyter. Os invito a que juguéis con Structural Pattern Matching y, por supuesto, que a partir de otoño de 2021 uséis Python 3.10 en vuestros proyectos.

Si quieres profundizar más y entender en profundidad todas las posibilidades del Structural Pattern Matching, tenemos disponibles tres PEPs (Python Enhancement Proposal) muy completos con todos los detalles:

Y tú, ¿qué opinión tienes del Structural Pattern Matching?, ¿estás deseando usarlo en producción o no piensas usarlo nunca? Deja un comentario con tu opinión.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.