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
A flask application to predict the speech emotion of any .wav file.

This is a speech emotion recognition app. It will allow you to train a modular MLP model with the RAVDESS dataset, and then use that model with a flask application to predict the speech emotion of an

Aryan Vijaywargia 2 Dec 15, 2021
TPlinker for NER 中文/英文命名实体识别

本项目是参考 TPLinker 中HandshakingTagging思想,将TPLinker由原来的关系抽取(RE)模型修改为命名实体识别(NER)模型。

GodK 113 Dec 28, 2022
GraphNLI: A Graph-based Natural Language Inference Model for Polarity Prediction in Online Debates

GraphNLI: A Graph-based Natural Language Inference Model for Polarity Prediction in Online Debates Vibhor Agarwal, Sagar Joglekar, Anthony P. Young an

Vibhor Agarwal 2 Jun 30, 2022
Plugin repository for Macast

Macast-plugins Plugin repository for Macast. How to use third-party player plugin Download Macast from GitHub Release. Download the plugin you want fr

109 Jan 04, 2023
Code for text augmentation method leveraging large-scale language models

HyperMix Code for our paper GPT3Mix and conducting classification experiments using GPT-3 prompt-based data augmentation. Getting Started Installing P

NAVER AI 47 Dec 20, 2022
Guide: Finetune GPT2-XL (1.5 Billion Parameters) and GPT-NEO (2.7 B) on a single 16 GB VRAM V100 Google Cloud instance with Huggingface Transformers using DeepSpeed

Guide: Finetune GPT2-XL (1.5 Billion Parameters) and GPT-NEO (2.7 Billion Parameters) on a single 16 GB VRAM V100 Google Cloud instance with Huggingfa

289 Jan 06, 2023
PyTorch implementation of NATSpeech: A Non-Autoregressive Text-to-Speech Framework

A Non-Autoregressive Text-to-Speech (NAR-TTS) framework, including official PyTorch implementation of PortaSpeech (NeurIPS 2021) and DiffSpeech (AAAI 2022)

760 Jan 03, 2023
A library that integrates huggingface transformers with the world of fastai, giving fastai devs everything they need to train, evaluate, and deploy transformer specific models.

blurr A library that integrates huggingface transformers with version 2 of the fastai framework Install You can now pip install blurr via pip install

ohmeow 253 Dec 31, 2022
this repository has datasets containing information of Uber pickups in NYC from April 2014 to September 2014 and January to June 2015. data Analysis , virtualization and some insights are gathered here

uber-pickups-analysis Data Source: https://www.kaggle.com/fivethirtyeight/uber-pickups-in-new-york-city Information about data set The dataset contain

1 Nov 02, 2021
State of the Art Natural Language Processing

Spark NLP: State of the Art Natural Language Processing Spark NLP is a Natural Language Processing library built on top of Apache Spark ML. It provide

John Snow Labs 3k Jan 05, 2023
LightSpeech: Lightweight and Fast Text to Speech with Neural Architecture Search

LightSpeech UnOfficial PyTorch implementation of LightSpeech: Lightweight and Fast Text to Speech with Neural Architecture Search.

Rishikesh (ऋषिकेश) 54 Dec 03, 2022
Train 🤗-transformers model with Poutyne.

poutyne-transformers Train 🤗 -transformers models with Poutyne. Installation pip install poutyne-transformers Example import torch from transformers

Lennart Keller 2 Dec 18, 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
Implementation of paper Does syntax matter? A strong baseline for Aspect-based Sentiment Analysis with RoBERTa.

RoBERTaABSA This repo contains the code for NAACL 2021 paper titled Does syntax matter? A strong baseline for Aspect-based Sentiment Analysis with RoB

106 Nov 28, 2022
Mapping a variable-length sentence to a fixed-length vector using BERT model

Are you looking for X-as-service? Try the Cloud-Native Neural Search Framework for Any Kind of Data bert-as-service Using BERT model as a sentence enc

Han Xiao 11.1k Jan 01, 2023
(ACL-IJCNLP 2021) Convolutions and Self-Attention: Re-interpreting Relative Positions in Pre-trained Language Models.

BERT Convolutions Code for the paper Convolutions and Self-Attention: Re-interpreting Relative Positions in Pre-trained Language Models. Contains expe

mlpc-ucsd 21 Jul 18, 2022
Speech Recognition for Uyghur using Speech transformer

Speech Recognition for Uyghur using Speech transformer Training: this model using CTC loss and Cross Entropy loss for training. Download pretrained mo

Uyghur 11 Nov 17, 2022
【原神】自动演奏风物之诗琴的程序

疯物之诗琴 读取midi并自动演奏原神风物之诗琴。 可以自定义配置文件自动调整音符来适配风物之诗琴。 (原神1.4直播那天就开始做了!到现在才能放出来。。) 如何使用 在Release页面中下载打包好的程序和midi压缩包并解压。 双击运行“疯物之诗琴.exe”。 在原神中打开风物之诗琴,软件内输入

435 Jan 04, 2023
Ελληνικά νέα (Python script) / Greek News Feed (Python script)

Ελληνικά νέα (Python script) / Greek News Feed (Python script) Ελληνικά English Το 2017 είχα υλοποιήσει ένα Python script για να εμφανίζει τα τωρινά ν

Loren Kociko 1 Jun 14, 2022
Chatbot with Pytorch, Python & Nextjs

Installation Instructions Make sure that you have Python 3, gcc, venv, and pip installed. Clone the repository $ git clone https://github.com/sahr

Rohit Sah 0 Dec 11, 2022