Pattern Matching in Python

Overview

Pattern Matching finalmente chega no Python 3.10. E daí?

"Pattern matching", ou "correspondência de padrões" como é conhecido no Brasil. Algumas pessoas já conhecem; praticamente todas as que conhecem, amam. Compreensível: pattern matching oferece uma maneira concisa e elegante de verificar e validar valores e objetos em nossos programas.
Por ser uma funcionalidade que essencialmente nasceu com linguagens cujo paradigma principal é o funcional, o pattern matching é algo que já podia ser encontrado em linguagens como Haskell, Scala, Elixir, OCaml e F# (entre outras). Se tornou tão popular como uma funcionalidade que é consistentemente apreciada pela vasta maioria dos programadores que linguagens não-primariamente funcionais começaram a adotá-la, como Rust e, agora, Python.

Então vamos lá. Por que tanto barulho quando se fala de pattern matching? Qual é o motivo do hype? Afinal de contas, como algumas pessoas dizem...

É tipo um switch/case, né?

Bem, sim e não. "Sim" porque definitivamente pode ser usado como um switch/case; "não" porque vai muito, muito além de um simples switch/case. Para exemplificar, revisitemos alguns exemplos de switch/case em uma linguagem similarmente popular: JavaScript.
Pelo bem da simplicidade, tentaremos manter os exemplos simples o suficiente para que não haja uma necessidade adicional de aprender sobre a linguagem em si.

Todos os exemplos de JavaScript foram testados no console do navegador Google Chrome, versão 90.0.4430.212

Vamos supor que queiramos implementar um simples fatorial usando switch/case e recursividade em JavaScript. Precisamos:

  • receber um número inteiro como argumento,
  • verificar o seu valor:
    • se for 0 ou 1, retornamos 1.
    • caso contrário, sendo n, retornamos n * factorial(n-1).

Em um primeiro rascunho, temos:

function factorial(n) {
    switch (n) {
        case 0:
            return 1;

        case 1:
            return 1;

        default:
            return n * factorial(n-1);
    }
}

O equivalente usando pattern matching em Python seria:

def factorial(n):
    match n:
        case 0:
            return 1
        
        case 1:
            return 1
        
        case _:
            return n * factorial(n-1)

A primeira diferença que podemos observar é a palavra match: em vez de switch, como nas linguagens que usam switch/case, o pattern matching do Python usa match. Fora isso, em vez do default, temos case _ como o "pega tudo" do match. Pythonistas já conhecem e usam o underscore (_) quando querem ignorar o valor; em uma instrução match, ele serve exatamente o mesmo propósito.

Certo, em ambos os casos nós temos dois return 1. Em JavaScript é fácil resolver: só precisamos nos aproveitar do fall-through, ou efeito cascata do switch:

function factorial(n) {
    switch (n) {
        case 0:
        case 1:
            return 1;

        default:
            return n * factorial(n-1);
    }
}

Analogamente, o match do Python nos oferece o operador or. Com este operador, podemos capturar um ou outro padrão.

def factorial(n):
    match n:
        case 0 | 1:
            return 1

        case _:
            return n * factorial(n-1)

E da mesma forma como podemos utilizar múltiplos níveis de cascata em um switch, podemos capturar múltiplos padrões com o operador or em um match:

function factorial(n) {
    switch (n) {
        case -2:
        case -1:
        case 0:
        case 1:
            return 1;

        default:
            return n * factorial(n-1);
    }
}
def factorial(n):
    match n:
        case -2 | -1 | 0 | 1:
            return 1

        case _:
            return n * factorial(n-1)

Legal, né? Podemos capturar diversos padrões de uma vez só e executar um só bloco associado a eles. Mas este exemplo levanta uma questão: normalmente, quando calculamos fatorial em programação, precisamos checar se o número recebido como argumento é maior ou igual a zero. Em ambas as instruções podemos aninhar uma instrução if como a seguir:

function factorial(n) {
    switch (n) {
        case 0:
        case 1:
            return 1;

        default:
            return (n < 0) ? null : n * factorial(n - 1);
            // ou
            if (n < 0) {
                return null;
            }
            else {
                return n * factorial(n-1);
            }
    }
}
def factorial(n):
    match n:
        case 0 | 1:
            return 1

        case _:
            return None if n < 0 else n * factorial(n-1)
            # ou
            if n < 0:
                return None

            else:
                return n * factorial(n-1)

E é aqui que o pattern matching começa a brilhar mais do que o simplório switch/case. Neste caso podemos fazer uso de guardas.

Guardas? Como assim guardas?

guard

Sim, guardas. E eles fazem exatamente o que o meme sugere: "permitem" a execução do bloco de código caso uma condição seja verdadeira. Ao adaptar o código acima para usar guardas, ficamos com

def factorial(n):
    match n:
        case 0 | 1:
            return 1

        case _ if n > 1:
            return n * factorial(n-1)

        case _:
            return None

Precisamos destacar dois pontos aqui:

  1. Diferentemente do meme, escrevemos case _ em vez de case n. Isso é porque já temos o n definido. Mas mais à frente veremos exemplos onde o guarda fará uso de variáveis definidas no respectivo case.
  2. É verdade que escrevemos mais do que se fizermos como no one-liner do exemplo anterior a este, mas leve em consideração que desta forma não só é mais legível como vale para o bloco inteiro, não limitado a uma única expressão.

Mudemos um pouco o exemplo. Em vez de continuarmos calculando fatoriais, vamos ver agora como podemos lidar com listas e tuplas* no contexto do pattern matching. Já sabemos que tanto em Python quanto em JavaScript podemos separar o(s) primeiro(s) elemento(s) de uma lista do resto dela.

Devido à natureza das tuplas do Python, todos os exemplos usando listas também se aplicam a tuplas.

Se escrevermos:

let list = [1, 2, 3, 4, 5];
let [head, ...tail] = list;
console.log(head);
console.log(tail);

veremos que o JavaScript atribuirá o valor 1 à variável head e a lista [2, 3, 4, 5] à variável tail, imprimindo-os no console logo após. Da mesma forma, podemos escrever em Python:

list_ = [1, 2, 3, 4, 5]
[head, *tail] = list_
print(head)
print(tail)

para obter exatamente a mesma saída. E, claro, podemos usar quantas variáveis quisermos entre head e tail desde que tenhamos elementos o suficiente na lista:

let list = [1, 2, 3, 4, 5];
let [head, neck, torso, ...tail] = list;
console.log({head, neck, torso, tail});
list_ = [1, 2, 3, 4, 5]
[head, neck, torso, *tail] = list_
print(dict(head=head, neck=neck, torso=torso, tail=tail))

Se tentarmos capturar mais valores do que é possível, o Python nos mostrará uma mensagem de erro. E com razão:

>>> [head, neck, *tail] = [1]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: not enough values to unpack (expected at least 2, got 1)

Ou seja, conseguimos colocar o valor 1 em head, mas não existe mais nada na lista para colocarmos na variável neck. A mensagem de erro é clara: esperava pelo menos 2 [valores], mas há só 1. Em situações normais, precisaríamos verificar o tamanho da lista antes de realizar esta atribuição.
(Em JavaScript, neck recebe undefined.)

Com isso chegamos ao próximo tópico, onde veremos como o match lida automaticamente com esse tipo de ocasião.

Desmontando objetos

O termo original em inglês é "destructuring". Eu decidi traduzir como "desmontando" porque, bem, é exatamente isso que acontece. Você passou por aquela fase, quando criança, em que queria desmontar os seus brinquedos? Quebrá-los em pequenos pedaços para que pudesse ver e entender como é por dentro ou como funciona? A instrução match nos ajuda a fazer algo parecido. Não só isso: ela faz checagens por nós, deixando que escrevamos menos e façamos mais.

Vamos voltar ao exemplo com listas. Podemos escrever:

def match_list(the_list):
    match the_list:
        case []:
            print('a lista está vazia')

        case [x]:
            print(f'a lista possui um único elemento: {x}')

        case [x, y]:
            print(f'a lista possui dois elementos: {x} e {y}')

        case _:
            print('a lista possui mais de dois elementos')

match_list([])
match_list([10])
match_list([3, 7])
match_list([1, 2, 3, 4, 5])

seguido de sua execução:

$ python py-06-match-list-destructuring.py 
a lista está vazia
a lista possui um único elemento: 10
a lista possui dois elementos: 3 e 7
a lista possui mais de dois elementos

Opa, pera aí! Esse match sabe o que fazer com listas de tamanhos diferentes? Sim! E é aí que começa a diversão. O match faz todas as verificações de forma implícita quando escrevemos os casos. Neste exemplo, ele verifica:

  • se a lista está vazia (len(the_list) == 0),
  • se a lista possui um elemento (len(the_list) == 1), e
  • se a lista possui dois elementos (len(the_list) == 2).

Se a lista for composta de um elemento, ele atribui esse único elemento à variável x. Se forem dois, atribui ambos os elementos às variáveis x e y.

Mas não termina aqui; é possível fazer a mesma coisa com dicionários, capturando apenas as chaves que nos interessam:

def match_dict(the_dict):
    match the_dict:
        case _ if len(the_dict) == 0:
            print('o dicionário está vazio')

        case {'name': nome}:
            print(f'a chave nome possui o valor {nome}')

        case {'date': data, 'article': {'title': titulo}}:
            print(f'o artigo {titulo} foi publicado em {data}')

        case _:
            print('nenhuma chave nos interessa')

match_dict({})
match_dict({'name': 'Fabricio', 'age': 29})
match_dict({'date': '01/06/2021', 'time': '00:10', 'article': {'title': 'Pattern Matching do Python'}})
match_dict({'what is love': "baby don't hurt me"})

E a sua execução:

$ python py-07-match-dict-destructuring.py 
o dicionário está vazio
a chave nome possui o valor Fabricio
o artigo Pattern Matching do Python foi publicado em 01/06/2021
nenhuma chave nos interessa

Perceba que não podemos tentar capturar o dicionário vazio ({}) pois o dicionário com o menor número de chaves válidas é o que passa. E o dicionário vazio é o dicionário com o menor número de chaves válidas: zero.

>>> match {'key': 'value'}:
...     case {}:
...         print('vazio')
...     case {'key': v}:
...         print(v)
... 
vazio

Diferentemente do switch/case, não precisamos escrever breaks ao fim de cada case; o match funciona como uma cadeia de ifs, interrompendo-se ao encontrar a primeira correspondência.

Bora aumentar o nível. Mas com cuidado para não nos machucarmos, pois cair de árvores pode doer.

Uma possível maneira de implementar uma árvore binária com uma função que calcula a sua altura em JavaScript é a seguinte:

class Tree {}

class Branch extends Tree {
    constructor(value, left, right) {
        super();
        [this.value, this.left, this.right] = [value, left, right];
    }
}

class Leaf extends Tree {}

function treeHeight(tree) {
    if (tree instanceof Branch) {
        let [left, right] = [tree.left, tree.right];
        return 1 + Math.max(treeHeight(left), treeHeight(right));
    }
    else if (tree instanceof Leaf) {
        return 0;
    }
}

let tree = new Branch(5,
                      new Branch(3,
                                 new Leaf(), new Leaf()),
                      new Leaf());

console.log(treeHeight(tree));
2

Claro que esse não é o tipo de código que veríamos no dia a dia - se fosse para usar classes desta forma, o "normal" seria que tanto Branch quanto Leaf sobreescrevessem um método height da superclasse Tree. Mas é importante que vejamos como treeHeight é implementado. Atenção especial aos if instanceof e [left, right] = [tree.left, tree.right] pois é nestes pontos que o match do Python se destaca.

class Tree:
    pass

class Branch(Tree):
    __match_args__ = ('value', 'left', 'right')

    def __init__(self, value, left, right):
        (self.value, self.left, self.right) = (value, left, right)

class Leaf(Tree):
    pass


def tree_height(tree):
    match tree:
        case Branch(_, left, right):
            return 1 + max(tree_height(left), tree_height(right))

        case Leaf():
            return 0


tree = Branch(5,
              Branch(3,
                     Leaf(), Leaf()),
              Leaf())


print(tree_height(tree))
$ python py-08-match-object-destructuring.py 
2

Isso mesmo - o match não só valida o tipo correto como também já é capaz de associar os valores dos atributos a variáveis para que possamos usá-las. E podemos aninhar padrões do mesmo jeito que podemos aninhar dicionários. Isso significa que é possível capturar o valor de um Branch final assim:

case Branch(v, Leaf(), Leaf()):
    # isinstance(left, Leaf) and isinstance(right, Leaf) == True
    # fazer algo com v, p.ex.:
    return v

O exemplo completo:

class Tree:
    pass

class Branch(Tree):
    __match_args__ = ('value', 'left', 'right')

    def __init__(self, value, left, right):
        (self.value, self.left, self.right) = (value, left, right)

class Leaf(Tree):
    pass


def get_first_double_leaf_branch_value(tree):
    match tree:
        # a
        case Branch(v, Leaf(), Leaf()):
            return v

        # b
        case Branch(_, Branch() as left, _):
            return get_first_double_leaf_branch_value(left)

        # c
        case Branch(_, _, Branch() as right):
            return get_first_double_leaf_branch_value(right)

        # d
        case Leaf():
            return None


tree = Branch(5,
              Branch(3,
                     Leaf(),
                     Branch(4,
                            Leaf(), Leaf())),
              Leaf())


print(get_first_double_leaf_branch_value(tree))
$ python py-08b-match-object-destructuring.py 
4
  • No caso a temos o que queremos: um Branch com dois Leafs. Só retornamos o valor.
  • No caso b temos um Branch com outro Branch à esquerda; seguimos por este lado recursivamente.
  • No caso c, mesma coisa que b. Porém, à direita.
  • Finalmente, o caso d indica que a Tree que foi passada como argumento é um mero Leaf; retornamos None.

Espera um pouco. Isso não significa que conseguimos validar valores diretamente nos nossos padrões? E se...

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def is_age_major(self):
        match self:
            case Person(name=n, age=18):  # 18 é a maioridade na maioria dos países
                print(f'{n} acabou de atingir a maioridade!')
                return True

            case Person(age=a) if a > 18:
                return True

            case _:
                return False


print(Person('Felipe', 18).is_age_major())
print(Person('Fabrício', 29).is_age_major())
print(Person('Letícia', 15).is_age_major())
$ python py-08c-match-object-destructuring.py 
Felipe acabou de atingir a maioridade!
True
True
False

E quando nenhum dos padrões atende ao valor?

Algumas liguagens adotaram comportamentos diferentes quanto à checagem de casos. Rust é a mais rígida neste sentido: o programador recebe um erro de compilação caso a checagem não seja exaustiva (ou seja, caso ele não tenha checado todos os padrões possíveis). F# preferiu apenas mostrar um warning ao programador, avisando que alguns padrões não foram cobertos. Se o valor não se encaixar em nenhum padrão, ocorre um erro de tempo de execução (runtime error).

Python é um pouco pouco mais relaxado quanto a isso; se nenhum caso for adequado, o programa simplesmente segue em frente. Do mesmo jeito que uma cadeia de ifs e elifs não precisa de um else, um match não necessariamente precisa de um case _.

def exhaustive_matching(number):
    match number:
        case -1:
            return '<'

        case 0:
            return '='

        case 1:
            return '>'

        case _:
            return 'dunno'


def non_exhaustive_matching(number):
    match number:
        case -1:
            return '<'

        case 0:
            return '='

        case 1:
            return '>'


print(exhaustive_matching(10))
print(non_exhaustive_matching(-10))
$ python py-09-match-exhaustive.py
dunno
None

Uma função que não retorna nada explicitamente retorna None implicitamente.

Conclusão

Vimos neste extenso artigo algumas maneiras de se usar match, como ele se compara com o que já conhecemos de outras linguagens - switch/case, if instanceof do JavaScript - e como ele é dimensões superior a essas estruturas. O pattern matching é uma funcionalidade já presente em muitas linguagens; agora os Pythonistas também poderão usufruir de todo o seu poder e criar aplicações ainda mais incríveis. Para finalizar, você consegue dizer o que a próxima função faz?

def m(f, xs):
    match xs:
        case x, *xs_:
            yield f(x)
            yield from m(f, xs_)

        case []:
            pass

  • Escrito com base na PEP 636;
  • Testado com a imagem docker python:3.10-rc-alpine
Owner
Fabricio Werneck
Ti Ne Si Fe
Fabricio Werneck
Library for Russian imprecise rhymes generation

TOM RHYMER Library for Russian imprecise rhymes generation. Quick Start Generate rhymes by any given rhyme scheme (aabb, abab, aaccbb, etc ...): from

Alexey Karnachev 6 Oct 18, 2022
An easy to use Natural Language Processing library and framework for predicting, training, fine-tuning, and serving up state-of-the-art NLP models.

Welcome to AdaptNLP A high level framework and library for running, training, and deploying state-of-the-art Natural Language Processing (NLP) models

Novetta 407 Jan 03, 2023
Non-Autoregressive Translation with Layer-Wise Prediction and Deep Supervision

Deeply Supervised, Layer-wise Prediction-aware (DSLP) Transformer for Non-autoregressive Neural Machine Translation

Chenyang Huang 37 Jan 04, 2023
Easy-to-use CPM for Chinese text generation

CPM 项目描述 CPM(Chinese Pretrained Models)模型是北京智源人工智能研究院和清华大学发布的中文大规模预训练模型。官方发布了三种规模的模型,参数量分别为109M、334M、2.6B,用户需申请与通过审核,方可下载。 由于原项目需要考虑大模型的训练和使用,需要安装较为复杂

382 Jan 07, 2023
Pipeline for training LSA models using Scikit-Learn.

Latent Semantic Analysis Pipeline for training LSA models using Scikit-Learn. Usage Instead of writing custom code for latent semantic analysis, you j

Dani El-Ayyass 23 Sep 05, 2022
PyTorch implementation of Microsoft's text-to-speech system FastSpeech 2: Fast and High-Quality End-to-End Text to Speech.

An implementation of Microsoft's "FastSpeech 2: Fast and High-Quality End-to-End Text to Speech"

Chung-Ming Chien 1k Dec 30, 2022
NL. The natural language programming language.

NL A Natural-Language programming language. Built using Codex. A few examples are inside the nl_projects directory. How it works Write any code in pur

2 Jan 17, 2022
Autoregressive Entity Retrieval

The GENRE (Generative ENtity REtrieval) system as presented in Autoregressive Entity Retrieval implemented in pytorch. @inproceedings{decao2020autoreg

Meta Research 611 Dec 16, 2022
DeepSpeech - Easy-to-use Speech Toolkit including SOTA ASR pipeline, influential TTS with text frontend and End-to-End Speech Simultaneous Translation.

(简体中文|English) Quick Start | Documents | Models List PaddleSpeech is an open-source toolkit on PaddlePaddle platform for a variety of critical tasks i

5.6k Jan 03, 2023
STT for TorchScript is a port of Coqui STT based on DeepSpeech to PyTorch.

st3 STT for TorchScript is a port of Coqui STT based on DeepSpeech to PyTorch. Currently it supports converting pbmm models to pt scripts with integra

Vlad Ki 8 Oct 18, 2021
Trankit is a Light-Weight Transformer-based Python Toolkit for Multilingual Natural Language Processing

Trankit: A Light-Weight Transformer-based Python Toolkit for Multilingual Natural Language Processing Trankit is a light-weight Transformer-based Pyth

652 Jan 06, 2023
[ICLR 2021 Spotlight] Pytorch implementation for "Long-tailed Recognition by Routing Diverse Distribution-Aware Experts."

RIDE: Long-tailed Recognition by Routing Diverse Distribution-Aware Experts. by Xudong Wang, Long Lian, Zhongqi Miao, Ziwei Liu and Stella X. Yu at UC

Xudong (Frank) Wang 205 Dec 16, 2022
Deploying a Text Summarization NLP use case on Docker Container Utilizing Nvidia GPU

GPU Docker NLP Application Deployment Deploying a Text Summarization NLP use case on Docker Container Utilizing Nvidia GPU, to setup the enviroment on

Ritesh Yadav 9 Oct 14, 2022
An open source library for deep learning end-to-end dialog systems and chatbots.

DeepPavlov is an open-source conversational AI library built on TensorFlow, Keras and PyTorch. DeepPavlov is designed for development of production re

Neural Networks and Deep Learning lab, MIPT 6k Dec 30, 2022
jiant is an NLP toolkit

🚨 Update 🚨 : As of 2021/10/17, the jiant project is no longer being actively maintained. This means there will be no plans to add new models, tasks,

ML² AT CILVR 1.5k Dec 28, 2022
Material for GW4SHM workshop, 16/03/2022.

GW4SHM Workshop Wednesday, 16th March 2022 (13:00 – 15:15 GMT): Presented by: Dr. Rhodri Nelson, Imperial College London Project website: https://www.

Devito Codes 1 Mar 16, 2022
The aim of this task is to predict someone's English proficiency based on a text input.

English_proficiency_prediction_NLP The aim of this task is to predict someone's English proficiency based on a text input. Using the The NICT JLE Corp

1 Dec 13, 2021
This repo stores the codes for topic modeling on palliative care journals.

This repo stores the codes for topic modeling on palliative care journals. Data Preparation You first need to download the journal papers. bash 1_down

3 Dec 20, 2022
Scikit-learn style model finetuning for NLP

Scikit-learn style model finetuning for NLP Finetune is a library that allows users to leverage state-of-the-art pretrained NLP models for a wide vari

indico 665 Dec 17, 2022