A correspondência de padrões finalmente foi trazida para o aniversário menor da terceira python. O conceito em si dificilmente pode ser chamado de novo, ele já foi implementado em várias linguagens, tanto da nova geração (Rust, Golang) e daquelas que já estão acima de 0x18 (Java).
A correspondência de padrões foi anunciada por Guido van Rossum , o autor da linguagem de programação Python e um “ditador generoso ao longo da vida.”
Meu nome é Denis Kaishev, e sou revisor de código do curso de desenvolvedor Middle Python . Neste post, quero dizer por que o Python tem correspondência de padrões e como trabalhar com isso.
Sintaticamente, a correspondência de padrões é essencialmente a mesma que em várias outras linguagens:
match_expr:
| star_named_expression ',' star_named_expressions?
| named_expression
match_stmt: "match" match_expr ':' NEWLINE INDENT case_block+ DEDENT
case_block: "case" patterns [guard] ':' block
guard: 'if' named_expression
patterns: value_pattern ',' [values_pattern] | pattern
pattern: walrus_pattern | or_pattern
walrus_pattern: NAME ':=' or_pattern
or_pattern: '|'.closed_pattern+
closed_pattern:
| capture_pattern
| literal_pattern
| constant_pattern
| group_pattern
| sequence_pattern
| mapping_pattern
| class_pattern
capture_pattern: NAME !('.' | '(' | '=')
literal_pattern:
| signed_number !('+' | '-')
| signed_number '+' NUMBER
| signed_number '-' NUMBER
| strings
| 'None'
| 'True'
| 'False'
constant_pattern: attr !('.' | '(' | '=')
group_pattern: '(' patterns ')'
sequence_pattern: '[' [values_pattern] ']' | '(' ')'
mapping_pattern: '{' items_pattern? '}'
class_pattern:
| name_or_attr '(' ')'
| name_or_attr '(' ','.pattern+ ','? ')'
| name_or_attr '(' ','.keyword_pattern+ ','? ')'
| name_or_attr '(' ','.pattern+ ',' ','.keyword_pattern+ ','? ')'
signed_number: NUMBER | '-' NUMBER
attr: name_or_attr '.' NAME
name_or_attr: attr | NAME
values_pattern: ','.value_pattern+ ','?
items_pattern: ','.key_value_pattern+ ','?
keyword_pattern: NAME '=' or_pattern
value_pattern: '*' capture_pattern | pattern
key_value_pattern:
| (literal_pattern | constant_pattern) ':' or_pattern
| '**' capture_pattern
Pode parecer complicado e confuso, mas na realidade tudo se resume a algo assim:
match some_expression: case pattern_1: ... case pattern_2: ...
Parece muito mais claro e agradável aos olhos.
Os próprios modelos são divididos em vários grupos:
- Padrões literais;
- Padrões de captura;
- Padrão curinga;
- Padrões de valor constante;
- Padrões de sequência;
- Padrões de mapeamento;
- Padrões de classe.
Vou contar um pouco sobre cada um deles.
Padrões literais
O padrão Literal, como o nome sugere, envolve a correspondência de uma série de valores, a saber, strings, números, booleanos e
Parece que o
string == 'string'
método está sendo usado
__eq__
.
match number:
case 42:
print('answer')
case 43:
print('not answer')
Padrões de captura
Um modelo de captura permite vincular uma variável a um nome fornecido no modelo e usar esse nome no escopo local.
match greeting:
case "":
print('Hello my friend')
case name:
print(f'Hello {name}')
Padrão curinga
Se houver muitas opções de correspondência, você pode usar
_
, que é um determinado valor padrão e irá corresponder a todos os elementos na estrutura
match
match number:
case 42:
print("Its’s forty two")
case _:
print("I don’t know, what it is")
Padrões de valor constante
Ao usar constantes, você precisa usar nomes pontilhados, por exemplo enumerações, caso contrário, o padrão de captura funcionará.
OK = 200
CONFLICT = 409
response = {'status': 409, 'msg': 'database error'}
match response['status'], response['msg']:
case OK, ok_msg:
print('handler 200')
case CONFLICT, err_msg:
print('handler 409')
case _:
print('idk this status')
E o resultado esperado não será o mais óbvio.
Padrões de Sequência
Ele permite que você comparar listas, tuplas e quaisquer outros objetos de
collections.abc.Sequence
, exceto
str
,
bytes
,
bytearray
.
answer = [42]
match answer:
case []:
print('i do not find answer')
case [x]:
print('asnwer is 42')
case [x, *_]:
print('i find more than one answers')
Agora não há necessidade de chamar a cada vez
len()
para verificar a quantidade de itens da lista, pois o método será chamado
__len__
.
Padrões de mapeamento
Este grupo é um pouco parecido com o anterior, só que aqui estamos combinando dicionários, ou, para ser mais preciso, objetos do tipo
collections.abc.Mapping
. Eles podem ser combinados muito bem entre si.
args = (1, 2)
kwargs = {'kwarg': 'kwarg', 'one_more_kwarg': 'one_more_kwarg'}
def match_something(*args, **kwargs):
match (args, kwargs):
case (arg1, arg2), {'kwarg': kwarg}:
print('i find positional args and one keyword args')
case (arg1, arg2), {'kwarg': kwarg, 'one_more_kwarg': one_more_kwarg}:
print('i find a few keyword args')
case _:
print('i cannot match anything')
match_something(*args, **kwargs)
E tudo ficaria bem, mas há um recurso. Este padrão garante a entrada desta (s) chave (s) no dicionário, mas o comprimento do dicionário não importa. Então eu acho que argumentos posicionais e argumentos de uma palavra-chave aparecerão na tela .
Padrões de classe
Para tipos de dados definidos pelo usuário, a sintaxe é semelhante à inicialização do objeto.
É assim que ficará com o exemplo de classes de dados:
from dataclasses import dataclass
@dataclass
class Coordinate:
x: int
y: int
z: int
coordinate = Coordinate(1, 2, 3)
match coordinate:
case Coordinate(0, 0, 0):
print('Zero point')
case _:
print('Another point')
Você também pode usar
if
, ou os chamados
guard
. Se a condição for falsa, a correspondência de padrões continua. É importante notar que o padrão é correspondido primeiro, e somente depois que a condição é verificada:
case Coordinate(x, y, z) if z == 0:
print('Point in the plane XY')
Se você usa classes diretamente, então você precisa de um atributo
__match_args__
no qual os argumentos posicionais são necessários (para namedtuple e classes de dados, ele é
__match_args__
gerado automaticamente).
class Coordinate:
__match_args__ = ['x', 'y', 'z']
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
oordinate = oordinate(1, 2, 3)
match oordinate:
case oordinate(0, 0, 0):
print('Zero oordinate')
case oordinate(x, y, z) if z == 0:
print('oordinate in the plane Z')
case _:
print('Another oordinate')
Caso contrário, uma exceção TypeError será lançada: Coordinate () aceita 0 subpadrões posicionais (3 dados)
Qual é o resultado final?
Na verdade, ele se parece com outro açúcar sintático junto com o recente
walrus operator
. A implementação , tal como está, converte blocos de instrução
match
em construções equivalentes
if/else
, nomeadamente bytecode, que tem o mesmo efeito.
Armin Ronacher, o criador do framework Web Flask para Python, descreveu de forma muito sucinta o estado atual da correspondência de padrões
Sim, é difícil argumentar: o código ficará um pouco mais limpo do que seria
if/else
um terço da torre de tela. Mas você também não pode chamar de algo que produza um efeito surpreendente. Não é ruim que seja introduzido: será conveniente usá-lo em alguns lugares, mas não em todos os lugares. De uma forma ou de outra, o principal com essa novidade é não exagerar, não correr mais rápido para atualizar todos os projetos para 3.10 e reescrever tudo, porque:
Agora é melhor do que nunca. Embora nunca seja melhor do que agora.
Você vai usá-lo? Se sim, onde?