Python

Python

Tutorial Python: Pattern matching

Aprende pattern matching en Python 3.10 con match-case para manejar datos complejos y estructuras con guards y captura de variables.

Aprende Python y certifícate

Sintaxis match-case

El pattern matching es una característica moderna de Python introducida en la versión 3.10 que permite examinar una variable y ejecutar diferentes bloques de código según su estructura y valor. La sintaxis match-case proporciona una alternativa más potente y expresiva a las tradicionales estructuras if-elif-else para ciertos escenarios.

La estructura básica de una expresión match-case sigue este patrón:

match expresion:
    case patron1:
        # Código si expresion coincide con patron1
    case patron2:
        # Código si expresion coincide con patron2
    case _:
        # Código por defecto (similar a else)

Casos simples

La forma más básica de usar match-case es para comparar valores literales:

def analizar_estado(codigo):
    match codigo:
        case 200:
            return "OK"
        case 404:
            return "No encontrado"
        case 500:
            return "Error del servidor"
        case _:
            return f"Código desconocido: {codigo}"

print(analizar_estado(200))  # OK
print(analizar_estado(418))  # Código desconocido: 418

Patrones OR

Puedes combinar varios patrones usando el operador | (OR):

def clasificar_numero(n):
    match n:
        case 0:
            return "Cero"
        case 1 | 3 | 5 | 7 | 9:
            return "Impar de un dígito"
        case 2 | 4 | 6 | 8:
            return "Par de un dígito"
        case _:
            return "Número de múltiples dígitos"

print(clasificar_numero(5))  # Impar de un dígito
print(clasificar_numero(8))  # Par de un dígito
print(clasificar_numero(42)) # Número de múltiples dígitos

Captura de valores

Puedes capturar valores en variables dentro de los patrones:

def procesar_comando(comando):
    match comando.split():
        case ["salir"]:
            return "Saliendo del programa"
        case ["guardar", nombre_archivo]:
            return f"Guardando en {nombre_archivo}"
        case ["abrir", nombre_archivo]:
            return f"Abriendo {nombre_archivo}"
        case ["ayuda"]:
            return "Comandos disponibles: salir, guardar, abrir, ayuda"
        case _:
            return "Comando no reconocido"

print(procesar_comando("guardar documento.txt"))  # Guardando en documento.txt
print(procesar_comando("ayuda"))                  # Comandos disponibles: salir, guardar, abrir, ayuda

En este ejemplo, nombre_archivo captura cualquier valor en la segunda posición de la lista.

Patrones con constantes

Para usar constantes en patrones, debes prefijarlas con _:

MAX_USUARIOS = 100
MIN_USUARIOS = 10

def validar_usuarios(cantidad):
    match cantidad:
        case _MAX_USUARIOS:
            return "Límite máximo alcanzado"
        case _MIN_USUARIOS:
            return "Límite mínimo alcanzado"
        case _:
            return "Cantidad dentro de los límites"

print(validar_usuarios(100))  # Límite máximo alcanzado
print(validar_usuarios(50))   # Cantidad dentro de los límites

Patrones con clases

El match-case es particularmente útil con objetos y clases:

class Punto:
    def __init__(self, x, y):
        self.x = x
        self.y = y

def clasificar_punto(punto):
    match punto:
        case Punto(x=0, y=0):
            return "Origen"
        case Punto(x=0, y=y):
            return f"En el eje Y, coordenada {y}"
        case Punto(x=x, y=0):
            return f"En el eje X, coordenada {x}"
        case Punto(x=x, y=y) if x == y:
            return f"En la diagonal, coordenada {x}"
        case Punto():
            return "Punto en otra posición"
        case _:
            return "No es un punto"

print(clasificar_punto(Punto(0, 0)))    # Origen
print(clasificar_punto(Punto(5, 0)))    # En el eje X, coordenada 5
print(clasificar_punto(Punto(3, 3)))    # En la diagonal, coordenada 3

Patrones con secuencias

El match-case es muy potente para trabajar con secuencias como listas y tuplas:

def analizar_coordenadas(coords):
    match coords:
        case []:
            return "Lista vacía"
        case [x, y]:
            return f"Punto 2D: ({x}, {y})"
        case [x, y, z]:
            return f"Punto 3D: ({x}, {y}, {z})"
        case [x, y, *resto]:
            return f"Punto multidimensional: empieza con ({x}, {y}) y continúa con {resto}"
        case _:
            return "No es una lista de coordenadas"

print(analizar_coordenadas([]))             # Lista vacía
print(analizar_coordenadas([10, 20]))       # Punto 2D: (10, 20)
print(analizar_coordenadas([1, 2, 3, 4]))   # Punto multidimensional: empieza con (1, 2) y continúa con [3, 4]

El operador * captura cualquier número de elementos restantes en una lista.

Patrones anidados

Puedes crear patrones anidados para estructuras de datos complejas:

def procesar_datos(datos):
    match datos:
        case {"tipo": "usuario", "id": id_usuario, "detalles": {"nombre": nombre}}:
            return f"Usuario {nombre} con ID {id_usuario}"
        case {"tipo": "producto", "id": id_producto, "precio": precio}:
            return f"Producto con ID {id_producto}, precio: {precio}"
        case {"error": mensaje, "codigo": codigo}:
            return f"Error {codigo}: {mensaje}"
        case _:
            return "Formato de datos desconocido"

print(procesar_datos({"tipo": "usuario", "id": 123, "detalles": {"nombre": "Ana"}}))
# Usuario Ana con ID 123

print(procesar_datos({"error": "Acceso denegado", "codigo": 403}))
# Error 403: Acceso denegado

Ventajas sobre if-elif-else

La sintaxis match-case ofrece varias ventajas sobre las estructuras tradicionales:

  • Legibilidad: El código es más claro y expresivo, especialmente para estructuras de datos complejas.
  • Concisión: Reduce la cantidad de código necesario para manejar múltiples casos.
  • Captura de valores: Permite extraer y nombrar partes de los datos de forma elegante.
  • Patrones estructurales: Facilita la coincidencia basada en la estructura de los datos.
# Con if-elif-else
def analizar_punto_tradicional(punto):
    if not isinstance(punto, tuple) or len(punto) != 2:
        return "No es un punto 2D"
    x, y = punto
    if x == 0 and y == 0:
        return "Origen"
    elif x == 0:
        return f"En el eje Y, coordenada {y}"
    elif y == 0:
        return f"En el eje X, coordenada {x}"
    elif x == y:
        return f"En la diagonal, coordenada {x}"
    else:
        return "Punto en otra posición"

# Con match-case
def analizar_punto_moderno(punto):
    match punto:
        case (0, 0):
            return "Origen"
        case (0, y):
            return f"En el eje Y, coordenada {y}"
        case (x, 0):
            return f"En el eje X, coordenada {x}"
        case (x, y) if x == y:
            return f"En la diagonal, coordenada {x}"
        case (_, _):
            return "Punto en otra posición"
        case _:
            return "No es un punto 2D"

Consideraciones de rendimiento

El match-case está optimizado para ser eficiente en la mayoría de los casos. Python compila los patrones en código optimizado, similar a una serie de comprobaciones condicionales. Sin embargo, para casos muy simples, un if-elif-else tradicional puede ser ligeramente más rápido.

# Ejemplo de benchmark simple
import timeit

def test_if_else(valor):
    if valor == 1:
        return "uno"
    elif valor == 2:
        return "dos"
    else:
        return "otro"

def test_match_case(valor):
    match valor:
        case 1:
            return "uno"
        case 2:
            return "dos"
        case _:
            return "otro"

# Para casos simples, la diferencia de rendimiento es mínima
tiempo_if = timeit.timeit(lambda: test_if_else(3), number=1000000)
tiempo_match = timeit.timeit(lambda: test_match_case(3), number=1000000)

La sintaxis match-case brinda una forma más expresiva y potente de manejar múltiples condiciones en Python, especialmente cuando se trabaja con estructuras de datos complejas o cuando se necesita extraer componentes de los datos durante el proceso de coincidencia.

Patrones estructurales

Los patrones estructurales son una de las características más potentes del pattern matching en Python. Estos patrones permiten examinar y descomponer estructuras de datos complejas como diccionarios, listas, tuplas y objetos de clases personalizadas en una sola operación.

A diferencia de las comparaciones simples de valores, los patrones estructurales analizan la forma y composición interna de los datos. Esto nos permite escribir código más declarativo que se centra en "qué estamos buscando" en lugar de "cómo lo estamos buscando".

Patrones para diccionarios

Los diccionarios son estructuras de datos fundamentales en Python, y el pattern matching ofrece una sintaxis elegante para trabajar con ellos:

def procesar_configuracion(config):
    match config:
        case {"debug": True, "entorno": entorno}:
            return f"Modo depuración activado en entorno {entorno}"
        case {"usuarios": usuarios, "max_conexiones": max_con} if len(usuarios) > max_con:
            return f"Advertencia: {len(usuarios)} usuarios exceden el límite de {max_con} conexiones"
        case {"version": version, "api_key": _}:
            return f"Configuración válida para API v{version}"
        case {"error": mensaje, **resto}:
            return f"Error en configuración: {mensaje}. Detalles adicionales: {resto}"
        case {}:
            return "Configuración vacía"
        case _:
            return "Formato de configuración no reconocido"

# Ejemplos de uso
print(procesar_configuracion({"debug": True, "entorno": "desarrollo"}))
# Modo depuración activado en entorno desarrollo

print(procesar_configuracion({"usuarios": ["ana", "juan", "elena"], "max_conexiones": 2}))
# Advertencia: 3 usuarios exceden el límite de 2 conexiones

Características importantes:

  • Podemos verificar la presencia de claves específicas en el diccionario
  • Podemos capturar valores asociados a claves específicas
  • El operador **resto captura todas las claves restantes no especificadas
  • Podemos combinar patrones con guardas para condiciones adicionales

Patrones para secuencias (listas y tuplas)

El pattern matching brinda herramientas poderosas para trabajar con secuencias:

def analizar_serie_temporal(datos):
    match datos:
        case []:
            return "Serie vacía"
        case [único_valor]:
            return f"Serie con un solo valor: {único_valor}"
        case [primer, *_, último] if primer == último:
            return f"Serie circular: comienza y termina con {primer}"
        case [inicio, segundo, *resto]:
            diferencia = segundo - inicio
            return f"Serie que comienza con {inicio}, {segundo} (diferencia: {diferencia})"
        case (año, mes, día):
            return f"Fecha en formato tupla: {día}/{mes}/{año}"
        case _:
            return "Formato no reconocido"

# Ejemplos de uso
print(analizar_serie_temporal([10, 15, 20, 25, 10]))
# Serie circular: comienza y termina con 10

print(analizar_serie_temporal([5, 8, 11, 14, 17]))
# Serie que comienza con 5, 8 (diferencia: 3)

print(analizar_serie_temporal((2023, 11, 15)))
# Fecha en formato tupla: 15/11/2023

Características importantes:

  • El operador *_ captura cualquier número de elementos que no necesitamos nombrar
  • Podemos combinar captura de elementos al inicio y final de la secuencia
  • El pattern matching distingue automáticamente entre listas y tuplas
  • Podemos aplicar condiciones adicionales a los valores capturados

Patrones para clases y objetos personalizados

Una de las aplicaciones más potentes del pattern matching es con objetos de clases personalizadas:

class Evento:
    def __init__(self, tipo, datos=None):
        self.tipo = tipo
        self.datos = datos or {}

class Respuesta:
    def __init__(self, codigo, contenido=None):
        self.codigo = codigo
        self.contenido = contenido

def procesar_mensaje(mensaje):
    match mensaje:
        case Evento(tipo="click", datos={"x": x, "y": y}):
            return f"Click en coordenadas ({x}, {y})"
        case Evento(tipo="login", datos={"usuario": usuario}):
            return f"Inicio de sesión de {usuario}"
        case Evento(tipo="error", datos=datos) if "crítico" in datos:
            return f"¡ERROR CRÍTICO! {datos['mensaje']}"
        case Respuesta(codigo=200, contenido=contenido):
            return f"Respuesta exitosa: {contenido}"
        case Respuesta(codigo=codigo) if 400 <= codigo < 500:
            return f"Error del cliente: código {codigo}"
        case Respuesta(codigo=codigo) if 500 <= codigo < 600:
            return f"Error del servidor: código {codigo}"
        case _:
            return "Mensaje no reconocido"

# Ejemplos de uso
print(procesar_mensaje(Evento("click", {"x": 100, "y": 200})))
# Click en coordenadas (100, 200)

print(procesar_mensaje(Respuesta(404, "Página no encontrada")))
# Error del cliente: código 404

print(procesar_mensaje(Evento("error", {"crítico": True, "mensaje": "Base de datos no disponible"})))
# ¡ERROR CRÍTICO! Base de datos no disponible

Características importantes:

  • Podemos hacer coincidencia por atributos de objetos
  • Podemos combinar patrones para atributos que son estructuras de datos complejas
  • Funciona con cualquier clase que tenga los atributos especificados
  • Podemos usar guardas para condiciones adicionales sobre los atributos

Patrones anidados y complejos

El verdadero poder de los patrones estructurales se revela cuando combinamos diferentes tipos de patrones:

def analizar_datos_iot(mensaje):
    match mensaje:
        case {"dispositivo": "sensor", "lecturas": [{"temp": t, "humedad": h} as lectura, *_]} if t > 30:
            return f"Alerta: temperatura alta ({t}°C) en lectura: {lectura}"
        case {"dispositivo": "gateway", "conexiones": conexiones} if len(conexiones) == 0:
            return "Gateway sin dispositivos conectados"
        case {"dispositivo": "actuador", "estado": {"encendido": True, "modo": modo}}:
            return f"Actuador funcionando en modo {modo}"
        case {"error": {"codigo": codigo, "detalles": detalles}}:
            return f"Error {codigo}: {detalles}"
        case _:
            return "Formato de mensaje IoT no reconocido"

# Ejemplo de uso
datos = {
    "dispositivo": "sensor",
    "id": "temp-01",
    "lecturas": [
        {"temp": 32.5, "humedad": 60, "timestamp": "2023-11-15T14:30:00"},
        {"temp": 31.8, "humedad": 62, "timestamp": "2023-11-15T14:35:00"}
    ]
}

print(analizar_datos_iot(datos))
# Alerta: temperatura alta (32.5°C) en lectura: {'temp': 32.5, 'humedad': 60, 'timestamp': '2023-11-15T14:30:00'}

En este ejemplo, utilizamos:

  • Patrones anidados para examinar estructuras dentro de estructuras
  • La palabra clave as para capturar un subpatrón completo
  • Guardas condicionales basadas en valores capturados
  • Combinación de patrones de diccionario y lista

Patrones con clases de datos (dataclasses)

Las dataclasses de Python funcionan especialmente bien con pattern matching:

from dataclasses import dataclass
from typing import List, Optional

@dataclass
class Usuario:
    nombre: str
    rol: str
    permisos: List[str]

@dataclass
class Recurso:
    tipo: str
    propietario: Optional[Usuario] = None
    publico: bool = False

def verificar_acceso(usuario, recurso, accion):
    match (usuario, recurso, accion):
        case (Usuario(rol="admin", permisos=_), _, _):
            return True
        case (Usuario(permisos=permisos), Recurso(propietario=propietario), _) if usuario == propietario:
            return True
        case (Usuario(permisos=permisos), Recurso(publico=True), "leer"):
            return True
        case (Usuario(permisos=permisos), Recurso(tipo=tipo), accion) if f"{accion}_{tipo}" in permisos:
            return True
        case _:
            return False

# Ejemplos de uso
admin = Usuario("Ana", "admin", ["eliminar_usuarios"])
usuario = Usuario("Carlos", "usuario", ["leer_documento", "editar_imagen"])
documento = Recurso("documento", usuario, False)
imagen_publica = Recurso("imagen", None, True)

print(verificar_acceso(admin, documento, "eliminar"))  # True (admin)
print(verificar_acceso(usuario, documento, "leer"))    # True (propietario)
print(verificar_acceso(usuario, imagen_publica, "leer"))  # True (recurso público)

Las dataclasses proporcionan una estructura clara que se integra perfectamente con los patrones estructurales, haciendo que el código sea más legible y mantenible.

Consideraciones de diseño

Al trabajar con patrones estructurales, es importante tener en cuenta:

  • Orden de los casos: Python evalúa los casos en orden secuencial, por lo que debes colocar los patrones más específicos antes que los más generales.
  • Exhaustividad: Considera siempre incluir un caso por defecto (case _:) para manejar entradas inesperadas.
  • Complejidad: Si un patrón se vuelve demasiado complejo, considera dividirlo en múltiples funciones o usar guardas para claridad.
  • Rendimiento: Para estructuras de datos muy grandes, el pattern matching puede ser menos eficiente que accesos directos, pero la claridad del código suele compensar esta diferencia.

Los patrones estructurales transforman la forma en que trabajamos con datos complejos en Python, permitiéndonos escribir código más declarativo, expresivo y fácil de entender.

Guards y captura de variables

El pattern matching en Python se vuelve aún más potente cuando combinamos la captura de variables con condiciones adicionales llamadas guards. Estos mecanismos nos permiten no solo identificar patrones estructurales, sino también establecer restricciones sobre los valores capturados y reutilizarlos de manera eficiente.

Captura de variables

La captura de variables es el proceso mediante el cual asignamos nombres a partes de la estructura que estamos analizando. Esto nos permite acceder a esos valores dentro del bloque de código correspondiente:

def analizar_punto(punto):
    match punto:
        case (x, y):
            return f"Coordenadas capturadas: x={x}, y={y}"

print(analizar_punto((10, 20)))  # Coordenadas capturadas: x=10, y=20

En este ejemplo, x y y son variables de captura que reciben los valores de la tupla. A diferencia de un desempaquetado normal, la captura en pattern matching ocurre solo si el patrón coincide completamente.

Captura con nombres específicos

Podemos capturar valores con nombres específicos en estructuras complejas:

def procesar_transaccion(transaccion):
    match transaccion:
        case {"monto": monto, "divisa": divisa, "fecha": fecha}:
            return f"Transacción de {monto} {divisa} realizada el {fecha}"

print(procesar_transaccion({"monto": 100, "divisa": "EUR", "fecha": "2023-11-15", "id": "T123"}))
# Transacción de 100 EUR realizada el 2023-11-15

Aquí capturamos solo los campos que nos interesan, ignorando otros campos como id.

Captura con el operador AS

El operador as nos permite capturar subpatrones completos mientras seguimos analizando su estructura interna:

def analizar_medicion(datos):
    match datos:
        case {"sensor": id_sensor, "lecturas": [primera, *resto] as todas_lecturas}:
            return f"Sensor {id_sensor}: {len(todas_lecturas)} lecturas, primera: {primera}"

datos = {"sensor": "temp-01", "lecturas": [32.5, 31.8, 30.2]}
print(analizar_medicion(datos))
# Sensor temp-01: 3 lecturas, primera: 32.5

En este ejemplo, capturamos tanto la primera lectura individualmente como la lista completa de lecturas usando as todas_lecturas.

Guards (condiciones de guarda)

Los guards son condiciones adicionales que se evalúan después de que un patrón coincide. Se introducen con la palabra clave if y permiten un control más preciso sobre cuándo debe ejecutarse un caso:

def clasificar_temperatura(temp):
    match temp:
        case float() | int() as t if t < 0:
            return f"Temperatura bajo cero: {t}°C"
        case float() | int() as t if 0 <= t <= 20:
            return f"Temperatura fresca: {t}°C"
        case float() | int() as t if 20 < t <= 30:
            return f"Temperatura agradable: {t}°C"
        case float() | int() as t:
            return f"Temperatura calurosa: {t}°C"
        case _:
            return "Valor no es una temperatura válida"

print(clasificar_temperatura(-5))    # Temperatura bajo cero: -5°C
print(clasificar_temperatura(25))    # Temperatura agradable: 25°C
print(clasificar_temperatura(35))    # Temperatura calurosa: 35°C
print(clasificar_temperatura("35"))  # Valor no es una temperatura válida

En este ejemplo:

  1. Primero verificamos si el valor coincide con el tipo (float o int)
  2. Luego capturamos ese valor en la variable t
  3. Finalmente aplicamos una condición adicional con if

Guards con múltiples variables

Los guards pueden utilizar múltiples variables capturadas en expresiones complejas:

def analizar_rango(rango):
    match rango:
        case (inicio, fin) if inicio < fin:
            return f"Rango válido de {inicio} a {fin}"
        case (inicio, fin) if inicio == fin:
            return f"Rango degenerado: {inicio}"
        case (inicio, fin):
            return f"Rango invertido: {fin} a {inicio}"

print(analizar_rango((10, 20)))  # Rango válido de 10 a 20
print(analizar_rango((5, 5)))    # Rango degenerado: 5
print(analizar_rango((30, 20)))  # Rango invertido: 20 a 30

Combinando guards con patrones complejos

La verdadera potencia de los guards se revela cuando los combinamos con patrones estructurales complejos:

def validar_usuario(datos):
    match datos:
        case {"nombre": nombre, "edad": edad, "roles": roles} if edad < 18 and "admin" in roles:
            return f"Error: {nombre} es menor de edad y no puede ser administrador"
        case {"nombre": nombre, "edad": edad} if edad < 18:
            return f"{nombre} es menor de edad, acceso limitado"
        case {"nombre": nombre, "roles": roles} if "admin" in roles:
            return f"{nombre} tiene privilegios de administrador"
        case {"nombre": nombre, "email": email} if "@" in email:
            return f"Usuario {nombre} con email válido"
        case {"nombre": nombre}:
            return f"Usuario {nombre} con datos incompletos"
        case _:
            return "Datos de usuario inválidos"

print(validar_usuario({"nombre": "Ana", "edad": 16, "roles": ["usuario", "admin"]}))
# Error: Ana es menor de edad y no puede ser administrador

print(validar_usuario({"nombre": "Carlos", "edad": 25, "roles": ["admin"]}))
# Carlos tiene privilegios de administrador

Captura de variables en patrones anidados

Podemos capturar variables en estructuras profundamente anidadas:

def analizar_pedido(pedido):
    match pedido:
        case {"cliente": {"nombre": nombre, "premium": True}, 
              "productos": [{"id": id_producto, "precio": precio} as primer_producto, *_]} if precio > 100:
            return f"Cliente premium {nombre} con producto caro ({precio}€): {primer_producto}"
        case {"cliente": {"nombre": nombre}, "productos": productos} if sum(p.get("precio", 0) for p in productos) > 200:
            return f"Pedido grande de {nombre}: total superior a 200€"
        case {"cliente": cliente, "productos": []}:
            return f"Carrito vacío para {cliente.get('nombre', 'cliente desconocido')}"
        case _:
            return "Formato de pedido no reconocido"

pedido = {
    "cliente": {"nombre": "Laura", "premium": True, "desde": 2020},
    "productos": [
        {"id": "P123", "nombre": "Auriculares", "precio": 129.99},
        {"id": "P456", "nombre": "Funda", "precio": 19.99}
    ]
}

print(analizar_pedido(pedido))
# Cliente premium Laura con producto caro (129.99€): {'id': 'P123', 'nombre': 'Auriculares', 'precio': 129.99}

Captura con comodines

Podemos usar comodines (_) para partes del patrón que no necesitamos capturar:

def extraer_información(datos):
    match datos:
        case [_, segundo, *_, último]:
            return f"Segundo elemento: {segundo}, último: {último}"
        case {"principal": {"secundario": {"valor": valor}}, **_}:
            return f"Valor anidado encontrado: {valor}"
        case _:
            return "Patrón no reconocido"

print(extraer_información([10, 20, 30, 40, 50]))
# Segundo elemento: 20, último: 50

print(extraer_información({"principal": {"secundario": {"valor": 42}}, "extra": "ignorado"}))
# Valor anidado encontrado: 42

Captura con tipos específicos

Podemos combinar la captura de variables con verificación de tipos:

def procesar_valor(valor):
    match valor:
        case str() as texto if texto.isdigit():
            return f"Texto numérico: {texto}"
        case str() as texto:
            return f"Texto no numérico: {texto}"
        case int() as numero if numero % 2 == 0:
            return f"Número par: {numero}"
        case int() as numero:
            return f"Número impar: {numero}"
        case list() as lista if all(isinstance(x, int) for x in lista):
            return f"Lista de enteros: {lista}"
        case _:
            return "Tipo no manejado"

print(procesar_valor("123"))      # Texto numérico: 123
print(procesar_valor("abc"))      # Texto no numérico: abc
print(procesar_valor(42))         # Número par: 42
print(procesar_valor([1, 2, 3]))  # Lista de enteros: [1, 2, 3]

Captura en clases con atributos dinámicos

Podemos capturar valores de atributos en clases que los definen dinámicamente:

class Configuración:
    def __init__(self, **kwargs):
        for key, value in kwargs.items():
            setattr(self, key, value)

def validar_configuración(config):
    match config:
        case Configuración(debug=True, entorno=entorno) if entorno != "producción":
            return f"Modo debug activado en entorno {entorno}"
        case Configuración(timeout=timeout) if timeout < 100:
            return f"Advertencia: timeout demasiado bajo ({timeout}ms)"
        case Configuración(max_usuarios=max_usuarios, conexiones=conexiones) if conexiones > max_usuarios:
            return f"Error: {conexiones} conexiones exceden el límite de {max_usuarios} usuarios"
        case _:
            return "Configuración válida"

print(validar_configuración(Configuración(debug=True, entorno="desarrollo")))
# Modo debug activado en entorno desarrollo

print(validar_configuración(Configuración(timeout=50, host="localhost")))
# Advertencia: timeout demasiado bajo (50ms)

Buenas prácticas con guards y captura de variables

  1. Orden de los casos: Coloca los patrones más específicos antes que los más generales, ya que Python evalúa los casos en orden secuencial.
def analizar_número(n):
    match n:
        case int() as x if x == 0:  # Caso específico primero
            return "Cero"
        case int() as x if x > 0:   # Caso más general después
            return "Positivo"
        case int():                 # Caso aún más general
            return "Negativo"
        case _:
            return "No es un entero"
  1. Evita efectos secundarios en guards: Los guards deben ser expresiones puras sin efectos secundarios.
# Incorrecto (efectos secundarios en guard)
case datos if print("Evaluando...") or len(datos) > 10:
    return "Muchos datos"

# Correcto
case datos:
    print("Evaluando...")
    if len(datos) > 10:
        return "Muchos datos"
  1. Usa nombres descriptivos para las variables capturadas:
# Poco descriptivo
case {"usuario": u, "permisos": p} if "admin" in p:
    return f"{u} es administrador"

# Más descriptivo
case {"usuario": nombre_usuario, "permisos": permisos} if "admin" in permisos:
    return f"{nombre_usuario} es administrador"
  1. Combina captura con desempaquetado cuando sea apropiado:
def procesar_coordenadas(datos):
    match datos:
        case {"puntos": [*puntos]} if len(puntos) >= 3:
            x1, y1 = puntos[0]
            x2, y2 = puntos[1]
            x3, y3 = puntos[2]
            # Cálculos con las coordenadas...
            return "Triángulo procesado"

Los guards y la captura de variables transforman el pattern matching de una simple herramienta de coincidencia a un poderoso mecanismo para expresar lógica compleja de manera concisa y legible. Estas características permiten escribir código que comunica claramente la intención y reduce la posibilidad de errores lógicos.

Aprende Python online

Otras lecciones de Python

Accede a todas las lecciones de Python y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Python y certifícate

Ejercicios de programación de Python

Evalúa tus conocimientos de esta lección Pattern matching con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.