Correspondência de padrões. Agora em Python

Ei!



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 NULL Nenhum.



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?



All Articles