O pattern matching, introduzido no Python 3.10 através da declaração match/case, é uma poderosa ferramenta para comparar valores contra padrões estruturais. Diferente de um simples switch-case de outras linguagens, o match/case permite desestruturar dados, combinar tipos e condições, tornando o código mais legível e expressivo.

Nesta aula, exploraremos a sintaxe, os tipos de padrões, o uso de guardas (condições adicionais) e casos de uso reais. Ao final, você será capaz de aplicar pattern matching em seus projetos Python modernos.

Sintaxe (Python 3.10+)

A estrutura básica do match/case é semelhante a um switch, mas com muito mais flexibilidade. A palavra-chave match inicia um bloco, seguida por um valor a ser comparado. Cada case define um padrão a ser verificado. Se o padrão corresponder, o bloco de código associado é executado. O primeiro padrão correspondente é utilizado.

def saudacao(comando):
    match comando:
        case "ola":
            print("Olá!")
        case "tchau":
            print("Tchau!")
        case _:
            print("Comando desconhecido")

O padrão curinga _ (underline) corresponde a qualquer valor, funcionando como um default. É importante notar que o match/case não exige break; apenas o primeiro caso correspondente é executado.

Padrões

Os padrões podem ser literais, variáveis, sequências, mapeamentos, classes, ou combinações destes. Abaixo alguns exemplos:

  • Padrão literal: case 42: ou case "texto":
  • Padrão de variável: case x: (atribui o valor a x)
  • Padrão de sequência: case [a, b, c]: (desestrutura listas/tuplas)
  • Padrão de mapeamento: case {"chave": valor}: (desestrutura dicionários)
  • Padrão de classe: case Ponto(x, y): (com classes data class ou NamedTuple)

Exemplo com sequência:

def processa_lista(lista):
    match lista:
        case []:
            print("Lista vazia")
        case [x]:
            print(f"Um elemento: {x}")
        case [x, y]:
            print(f"Dois elementos: {x} e {y}")
        case _:
            print("Mais de dois elementos")

Exemplo com classe (usando dataclass):

from dataclasses import dataclass

@dataclass
class Ponto:
    x: int
    y: int

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

Guardas

Guardas permitem adicionar uma condição extra a um padrão, usando a palavra-chave if após o padrão. A condição é avaliada apenas se o padrão corresponder. Se a condição for falsa, o próximo caso é testado.

def classifica_numero(n):
    match n:
        case 0:
            print("Zero")
        case n if n > 0:
            print("Positivo")
        case n if n < 0:
            print("Negativo")

Note que a variável n no caso com guarda é uma nova variável que captura o valor. Cuidado: se você usar o mesmo nome de uma variável externa, ela será sobrescrita no escopo do case. Use nomes diferentes ou o padrão curinga quando necessário.

Casos de uso

Pattern matching é útil em várias situações:

  • Interpretação de comandos: processar comandos de uma CLI, como um parser simples.
  • Manipulação de AST: em compiladores ou interpretadores, para tratar diferentes nós da árvore sintática.
  • Processamento de dados: lidar com diferentes formatos de entrada (JSON, tuplas, etc.).
  • Máquinas de estado: transições baseadas em eventos.

Exemplo prático: processar uma resposta de API que pode ser sucesso ou erro.

def trata_resposta(resposta):
    match resposta:
        case {"status": 200, "dados": dados}:
            print(f"Sucesso: {dados}")
        case {"status": 404}:
            print("Não encontrado")
        case {"status": status, "erro": msg}:
            print(f"Erro {status}: {msg}")
        case _:
            print("Resposta desconhecida")

Outro exemplo: combinar padrões com classes para simular uma álgebra de tipos.

Boas práticas e observações finais

  • Use padrões curinga (_) para capturar casos não previstos, garantindo exaustividade.
  • Evite repetir lógica; use guardas para condições complexas.
  • Lembre-se que o match/case é uma expressão? Não, é uma declaração. Mas você pode usar match dentro de funções.
  • Em Python 3.10+, o match/case está disponível; versões anteriores não suportam.

Com o pattern matching, seu código fica mais declarativo e próximo da intenção, reduzindo a necessidade de múltiplos if-elif.

Referências

Exercícios

  1. Escreva uma função que use match/case para classificar um número como 'positivo', 'negativo' ou 'zero' (sem usar guardas).

    ✓ Resposta:
    def classifica_numero(n):
        match n:
            case 0:
                return "zero"
            case n if n > 0:
                return "positivo"
            case n if n < 0:
                return "negativo"
  2. Crie uma função que receba uma lista e use match/case para retornar 'vazia', 'um elemento', 'dois elementos' ou 'muitos elementos'.

    ✓ Resposta:
    def tamanho_lista(lista):
        match lista:
            case []:
                return "vazia"
            case [x]:
                return "um elemento"
            case [x, y]:
                return "dois elementos"
            case _:
                return "muitos elementos"
  3. Escreva uma função que use match/case para processar um dicionário representando uma forma geométrica: {"tipo": "circulo", "raio": r} ou {"tipo": "retangulo", "largura": w, "altura": h}. Retorne a área.

    ✓ Resposta:
    import math
    
    def area(forma):
        match forma:
            case {"tipo": "circulo", "raio": r}:
                return math.pi * r * r
            case {"tipo": "retangulo", "largura": w, "altura": h}:
                return w * h
            case _:
                return None
  4. Crie uma função que receba uma tupla (x, y) e use match/case para determinar em qual quadrante o ponto se encontra (assumindo x e y diferentes de zero). Use guardas.

    ✓ Resposta:
    def quadrante(ponto):
        match ponto:
            case (x, y) if x > 0 and y > 0:
                return "primeiro"
            case (x, y) if x < 0 and y > 0:
                return "segundo"
            case (x, y) if x < 0 and y < 0:
                return "terceiro"
            case (x, y) if x > 0 and y < 0:
                return "quarto"
            case _:
                return "sobre eixo"
  5. Implemente uma função que receba um valor que pode ser int, str ou None e use match/case para retornar uma descrição: 'int: {valor}', 'str: {valor}', ou 'None'.

    ✓ Resposta:
    def descreve(valor):
        match valor:
            case int(n):
                return f"int: {n}"
            case str(s):
                return f"str: {s}"
            case None:
                return "None"
            case _:
                return "tipo desconhecido"