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
code for modular summarization work published in ACL2021 by Krishna et al

This repository contains the code for running modular summarization pipelines as described in the publication Krishna K, Khosla K, Bigham J, Lipton ZC

Approximately Correct Machine Intelligence (ACMI) Lab 21 Nov 24, 2022
An Open-Source Package for Neural Relation Extraction (NRE)

OpenNRE We have a DEMO website (http://opennre.thunlp.ai/). Try it out! OpenNRE is an open-source and extensible toolkit that provides a unified frame

THUNLP 3.9k Jan 03, 2023
Jupyter Notebook tutorials on solving real-world problems with Machine Learning & Deep Learning using PyTorch

Jupyter Notebook tutorials on solving real-world problems with Machine Learning & Deep Learning using PyTorch. Topics: Face detection with Detectron 2, Time Series anomaly detection with LSTM Autoenc

Venelin Valkov 1.8k Dec 31, 2022
Search for documents in a domain through Google. The objective is to extract metadata

MetaFinder - Metadata search through Google _____ __ ___________ .__ .___ / \

Josué Encinar 85 Dec 16, 2022
An example project using OpenPrompt under pytorch-lightning for prompt-based SST2 sentiment analysis model

pl_prompt_sst An example project using OpenPrompt under the framework of pytorch-lightning for a training prompt-based text classification model on SS

Zhiling Zhang 5 Oct 21, 2022
Google's Meena transformer chatbot implementation

Here's my attempt at recreating Meena, a state of the art chatbot developed by Google Research and described in the paper Towards a Human-like Open-Domain Chatbot.

Francesco Pham 94 Dec 25, 2022
Generate product descriptions, blogs, ads and more using GPT architecture with a single request to TextCortex API a.k.a Hemingwai

TextCortex - HemingwAI Generate product descriptions, blogs, ads and more using GPT architecture with a single request to TextCortex API a.k.a Hemingw

TextCortex AI 27 Nov 28, 2022
2021 AI CUP Competition on Traditional Chinese Scene Text Recognition - Intermediate Contest

繁體中文場景文字辨識 程式碼說明 組別:這就是我 成員:蔣明憲 唐碩謙 黃玥菱 林冠霆 蕭靖騰 目錄 環境套件 安裝方式 資料夾布局 前處理-製作偵測訓練註解檔 前處理-製作分類訓練樣本 part.py : 從 json 裁切出分類訓練樣本 Class.py : 將切出來的樣本按照文字分類到各資料夾

HuanyueTW 3 Jan 14, 2022
Open-Source Toolkit for End-to-End Speech Recognition leveraging PyTorch-Lightning and Hydra.

OpenSpeech provides reference implementations of various ASR modeling papers and three languages recipe to perform tasks on automatic speech recogniti

Soohwan Kim 26 Dec 14, 2022
Analyse japanese ebooks using MeCab to determine the difficulty level for japanese learners

japanese-ebook-analysis This aim of this project is to make analysing the contents of a japanese ebook easy and streamline the process for non-technic

Christoffer Aakre 14 Jul 23, 2022
A collection of models for image - text generation in ACM MM 2021.

Bi-directional Image and Text Generation UMT-BITG (image & text generator) Unifying Multimodal Transformer for Bi-directional Image and Text Generatio

Multimedia Research 63 Oct 30, 2022
Indonesia spellchecker with python

indonesia-spellchecker Ganti kata yang terdapat pada file teks.txt untuk diperiksa kebenaran kata. Run on local machine python3 main.py

Rahmat Agung Julians 1 Sep 14, 2022
Local cross-platform machine translation GUI, based on CTranslate2

DesktopTranslator Local cross-platform machine translation GUI, based on CTranslate2 Download Windows Installer You can either download a ready-made W

Yasmin Moslem 29 Jan 05, 2023
MicBot - MicBot uses Google Translate to speak everyone's chat messages

MicBot MicBot uses Google Translate to speak everyone's chat messages. It can al

2 Mar 09, 2022
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
ALIbaba's Collection of Encoder-decoders from MinD (Machine IntelligeNce of Damo) Lab

AliceMind AliceMind: ALIbaba's Collection of Encoder-decoders from MinD (Machine IntelligeNce of Damo) Lab This repository provides pre-trained encode

Alibaba 1.4k Jan 04, 2023
Anomaly Detection 이상치 탐지 전처리 모듈

Anomaly Detection 시계열 데이터에 대한 이상치 탐지 1. Kernel Density Estimation을 활용한 이상치 탐지 train_data_path와 test_data_path에 존재하는 시점 정보를 포함하고 있는 csv 형태의 train data와

CLUST-consortium 43 Nov 28, 2022
LSTM model - IMDB review sentiment analysis

NLP - Movie review sentiment analysis The colab notebook contains the code for building a LSTM Recurrent Neural Network that gives 87-88% accuracy on

Sundeep Bhimireddy 1 Jan 29, 2022
This is a GUI program that will generate a word search puzzle image

Word Search Puzzle Generator Table of Contents About The Project Built With Getting Started Prerequisites Installation Usage Roadmap Contributing Cont

11 Feb 22, 2022
Implementation of the Hybrid Perception Block and Dual-Pruned Self-Attention block from the ITTR paper for Image to Image Translation using Transformers

ITTR - Pytorch Implementation of the Hybrid Perception Block (HPB) and Dual-Pruned Self-Attention (DPSA) block from the ITTR paper for Image to Image

Phil Wang 17 Dec 23, 2022