O pattern matching (correspondência de padrões) é um recurso poderoso introduzido no Python 3.10 por meio da declaração match/case. Inspirado em linguagens como Haskell, Rust e Scala, ele permite comparar um valor contra uma série de padrões de forma clara e concisa, substituindo longas cadeias de if/elif/else ou consultas complexas a dicionários. Nesta aula, você aprenderá a sintaxe básica, os diferentes tipos de padrões, como usar guardas para condições adicionais e exemplos práticos de aplicação.

O pattern matching não é apenas um simples switch case: ele suporta desestruturação, captura de variáveis, padrões aninhados e muito mais. Com ele, você pode escrever código mais legível e menos propenso a erros, especialmente ao lidar com estruturas de dados complexas.

Sintaxe (Python 3.10+)

A declaração match avalia uma expressão e tenta correspondê-la a um dos padrões listados nos blocos case. A sintaxe básica é:

match valor:
    case padrão1:
        # ação para padrão1
    case padrão2:
        # ação para padrão2
    case _:
        # padrão curinga (opcional)

O padrão _ (underscore) funciona como curinga, correspondendo a qualquer valor. É importante que os casos sejam exaustivos, embora o Python não exija isso obrigatoriamente; se nenhum padrão corresponder, a execução simplesmente continua após o bloco match.

Exemplo simples:

def saudacao(idioma):
    match idioma:
        case "pt":
            print("Olá!")
        case "en":
            print("Hello!")
        case "es":
            print("¡Hola!")
        case _:
            print("Idioma desconhecido")

saudacao("pt")  # Olá!

Observe que não é necessário usar break como em outras linguagens; apenas o primeiro caso correspondente é executado.

Padrões

O Python suporta diversos tipos de padrões, que podem ser combinados para criar correspondências complexas.

Padrões literais: correspondem a valores específicos (números, strings, booleanos, None).

match x:
    case 0:
        print("zero")
    case 1:
        print("um")
    case None:
        print("nulo")

Padrões de captura: atribuem o valor a uma variável, permitindo seu uso no bloco do caso.

match ponto:
    case (0, 0):
        print("Origem")
    case (x, 0):
        print(f"Eixo X em {x}")
    case (0, y):
        print(f"Eixo Y em {y}")
    case (x, y):
        print(f"Ponto ({x}, {y})")

Padrões de sequência: correspondem a sequências (listas, tuplas) e podem desestruturar elementos.

match dados:
    case [a, b]:
        print(f"Lista de dois elementos: {a}, {b}")
    case [a, b, c]:
        print(f"Lista de três elementos: {a}, {b}, {c}")
    case _:
        print("Outro formato")

Padrões de mapeamento: correspondem a dicionários, permitindo verificar chaves e capturar valores.

match usuario:
    case {"nome": nome, "idade": idade}:
        print(f"{nome} tem {idade} anos")
    case {"nome": nome}:
        print(f"{nome} idade desconhecida")
    case _:
        print("Dados inválidos")

Padrões de classe: correspondem a instâncias de classes, desestruturando atributos.

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

match p:
    case Ponto(x=0, y=0):
        print("Origem")
    case Ponto(x=x, y=y):
        print(f"Ponto ({x}, {y})")

Padrões aninhados: qualquer combinação dos anteriores.

match dados:
    case {"tipo": "retangulo", "dimensoes": [largura, altura]}:
        print(f"Retângulo {largura}x{altura}")
    case {"tipo": "circulo", "dimensoes": {"raio": r}}:
        print(f"Círculo raio {r}")

Guardas

Guardas são condições adicionais que devem ser verdadeiras para que um caso seja selecionado. Elas são escritas após a palavra-chave if no final do padrão.

match valor:
    case x if x > 0:
        print(f"Positivo: {x}")
    case x if x < 0:
        print(f"Negativo: {x}")
    case 0:
        print("Zero")

Guardas são avaliadas somente após o padrão corresponder. Se a guarda for falsa, o padrão é descartado e a execução continua para o próximo caso.

Exemplo com desestruturação:

match ponto:
    case (x, y) if x == y:
        print(f"Diagonal: {x}")
    case (x, y):
        print(f"Ponto ({x}, {y})")

É importante notar que as variáveis capturadas no padrão podem ser usadas na guarda.

Casos de uso

O pattern matching é útil em diversas situações, substituindo construções mais complexas.

1. Processamento de comandos: interpretar comandos de uma CLI.

def processar_comando(comando):
    match comando.split():
        case ["sair"]:
            print("Encerrando...")
        case ["ola", nome]:
            print(f"Olá, {nome}!")
        case ["soma", *numeros]:
            print(f"Soma: {sum(map(int, numeros))}")
        case _:
            print("Comando inválido")

2. Validação de dados: verificar estruturas recebidas de APIs.

def validar_resposta(resposta):
    match resposta:
        case {"status": 200, "dados": dados}:
            return dados
        case {"status": 404}:
            raise ValueError("Não encontrado")
        case {"status": status} if 500 <= status < 600:
            raise ConnectionError("Erro no servidor")
        case _:
            raise ValueError("Resposta inesperada")

3. Tratamento de eventos: em interfaces gráficas ou jogos.

def tratar_evento(evento):
    match evento:
        case ("clique", x, y):
            print(f"Clique em ({x}, {y})")
        case ("tecla", tecla) if tecla == "q":
            print("Saindo...")
        case ("tecla", tecla):
            print(f"Tecla pressionada: {tecla}")
        case _:
            pass

4. Expressões algébricas: implementar evaluadores.

def avaliar(expr):
    match expr:
        case ("soma", a, b):
            return avaliar(a) + avaliar(b)
        case ("mult", a, b):
            return avaliar(a) * avaliar(b)
        case int(n):
            return n
        case _:
            raise ValueError("Expressão inválida")

O pattern matching torna o código mais declarativo e fácil de entender, reduzindo a complexidade condicional.

Referências

Exercícios

  1. Usando pattern matching, escreva uma função que recebe uma tupla (operacao, a, b) e retorna o resultado da operação. As operações podem ser 'soma', 'subtracao', 'multiplicacao', 'divisao'. Se a operação for desconhecida, retorne None.

    ✓ Resposta:
    def calculadora(expr):
        match expr:
            case ('soma', a, b):
                return a + b
            case ('subtracao', a, b):
                return a - b
            case ('multiplicacao', a, b):
                return a * b
            case ('divisao', a, b):
                return a / b if b != 0 else None
            case _:
                return None
  2. Escreva uma função que recebe uma lista e imprime seu conteúdo formatado: se a lista tiver um elemento, imprime "[elemento]"; se tiver dois, imprime "[a, b]"; se tiver três ou mais, imprime "[a, b, ...]". Use pattern matching.

    ✓ Resposta:
    def formatar_lista(lista):
        match lista:
            case [a]:
                print(f"[{a}]")
            case [a, b]:
                print(f"[{a}, {b}]")
            case [a, b, *_]:
                print(f"[{a}, {b}, ...]")
            case _:
                print("Lista vazia")
  3. Crie uma função que recebe um dicionário representando um pedido com chaves 'id', 'itens' (lista de strings) e 'total' (float). Use pattern matching para validar a estrutura: se faltar alguma chave, imprima "Pedido inválido"; caso contrário, imprima o id e o número de itens.

    ✓ Resposta:
    def processar_pedido(pedido):
        match pedido:
            case {"id": id, "itens": itens, "total": total}:
                print(f"Pedido {id} com {len(itens)} itens")
            case _:
                print("Pedido inválido")
  4. Escreva uma função que recebe um objeto (instância de classe) e, usando pattern matching, verifica se é uma instância de 'int', 'str', 'list' ou 'dict'. Se for int e maior que 100, imprima "Inteiro grande"; se for str e vazia, imprima "String vazia"; para list, imprima o primeiro elemento; para dict, imprima as chaves; caso contrário, imprima "Tipo desconhecido".

    ✓ Resposta:
    def analisar_objeto(obj):
        match obj:
            case int(n) if n > 100:
                print("Inteiro grande")
            case int():
                print("Inteiro")
            case str(s) if s == "":
                print("String vazia")
            case str():
                print("String")
            case list(primeiro, *_):
                print(f"Primeiro elemento: {primeiro}")
            case dict(**kwargs):
                print(f"Chaves: {list(kwargs.keys())}")
            case _:
                print("Tipo desconhecido")
  5. Implemente uma função que recebe uma expressão na forma de tupla aninhada representando uma árvore binária de números, onde 'soma' e 'mult' são operadores binários e números são folhas. Use pattern matching para avaliar a expressão.

    ✓ Resposta:
    def avaliar_expr(expr):
        match expr:
            case ('soma', a, b):
                return avaliar_expr(a) + avaliar_expr(b)
            case ('mult', a, b):
                return avaliar_expr(a) * avaliar_expr(b)
            case int(n):
                return n
            case _:
                raise ValueError("Expressão inválida")

Boas práticas e observações finais

Ao usar pattern matching, lembre-se de que a ordem dos casos importa: o primeiro padrão correspondente é executado. Coloque padrões mais específicos antes dos mais genéricos. Use guardas para condições adicionais, mas evite guardas muito complexas que possam prejudicar a legibilidade. O pattern matching é especialmente útil para desestruturar dados e tornar o código mais expressivo, mas não é adequado para substituir todos os if/else; use-o quando houver correspondência de padrões clara e benefício na legibilidade.