Correspondance de motif. Maintenant en Python

Hé!



La correspondance de motifs a finalement été apportée à l'anniversaire mineur du troisième python. Le concept lui-même peut difficilement être qualifié de nouveau, il a déjà été implémenté dans de nombreux langages, à la fois de la nouvelle génération (Rust, Golang) et ceux qui dépassent déjà 0x18 (Java).





Le pattern matching a été annoncé par Guido van Rossum , l'auteur du langage de programmation Python et «généreux dictateur de longue date»



Je m'appelle Denis Kaishev et je suis réviseur de code pour le cours de développement Middle Python . Dans cet article, je veux vous dire pourquoi Python a une correspondance de modèle et comment l'utiliser.



D'un point de vue syntaxique, la correspondance de modèles est essentiellement la même que dans un certain nombre d'autres langues:



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

      
      





Cela peut sembler compliqué et déroutant, mais en réalité, tout se résume à quelque chose comme ceci:



match some_expression:
    case pattern_1:
        ...
    case pattern_2:
        ...

      
      





Il semble beaucoup plus clair et plus agréable à l'œil.



Les modèles eux-mêmes sont divisés en plusieurs groupes:



  • Modèles littéraux;
  • Modèles de capture;
  • Motif de caractères génériques;
  • Modèles de valeurs constantes;
  • Modèles de séquence;
  • Modèles de mappage;
  • Modèles de classe.


Je vais vous parler un peu de chacun d'eux.



Modèles littéraux



Le modèle Literal, comme son nom l'indique, implique la mise en correspondance d'une série de valeurs, à savoir des chaînes, des nombres, des booléens et NULL None.



Il semble que la string == 'string'



méthode soit utilisée __eq__



.



match number:
    case 42:
        print('answer')
    case 43:
        print('not answer')

      
      





Modèles de capture



Un modèle de capture vous permet de lier une variable avec un nom donné dans le modèle et d'utiliser ce nom dans la portée locale.



match greeting:
    case "":
        print('Hello my friend')
    case name:
        print(f'Hello  {name}')
      
      







Motif générique



S'il y a trop d'options correspondantes, vous pouvez utiliser _



, qui est une certaine valeur par défaut et correspondra à tous les éléments de la structure match






match number:
    case 42:
        print("Its’s forty two")
    case _:
        print("I don’t know, what it is")
      
      





Modèles de valeurs constantes



Lorsque vous utilisez des constantes, vous devez utiliser des noms en pointillés, par exemple des énumérations, sinon le modèle de capture fonctionnera.



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')
      
      





Et le résultat attendu ne sera pas le plus évident.



Modèles de séquence



Il vous permet de comparer les listes, les tuples et tout autre objet de collections.abc.Sequence



, sauf 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')
      
      





Désormais, il n'est plus nécessaire d'appeler à chaque fois len()



pour vérifier le nombre d'éléments dans la liste, puisque la méthode sera appelée __len__



.



Modèles de mappage



Ce groupe est un peu comme le précédent, seulement ici on fait correspondre des dictionnaires, ou, pour être précis, des objets de type collections.abc.Mapping



. Ils peuvent être assez bien combinés les uns avec les autres.



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)
      
      





Et tout irait bien, mais il y a une fonctionnalité. Ce modèle garantit l'entrée de cette (ces) clé (s) dans le dictionnaire, mais la longueur du dictionnaire n'a pas d'importance. Donc, je trouve des arguments positionnels et un mot-clé args apparaîtra à l'écran .



Modèles de classe



Pour les types de données définis par l'utilisateur, la syntaxe est similaire à l'initialisation d'objet.



Voici à quoi cela ressemblera avec l'exemple des classes de données:



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')

      
      





Vous pouvez également utiliser if



, ou ainsi appelé guard



. Si la condition est fausse, la correspondance de modèle se poursuit. Il est à noter que le modèle correspond en premier, et seulement après cela, la condition est vérifiée:



case Coordinate(x, y, z) if z == 0:
    print('Point in the plane XY')
      
      





Si vous utilisez directement des classes, alors vous avez besoin d'un attribut __match_args__



dans lequel des arguments de position sont nécessaires (pour namedtuple et dataclasses, il est __match_args__



généré automatiquement).



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')

      
      





Sinon, une exception TypeError sera levée: Coordinate () accepte 0 sous-motifs positionnels (3 donnés)



Quelle est la ligne de fond?



En fait, il ressemble à un autre sucre syntaxique avec le sucre récent walrus operator



. L'implémentation , telle qu'elle est, convertit les blocs d'instructions match



en constructions équivalentes if/else



, à savoir le bytecode, qui a le même effet.





Armin Ronacher, le créateur du framework web Flask pour Python, très succinctement décrit le . État actuel de la mise en correspondance de motif



Oui, il est difficile d'argumenter: le code deviendra un peu plus propre que ce serait if/else



un tiers de l'écran tour . Mais vous ne pouvez pas non plus appeler cela quelque chose qui produit un effet wow. Ce n'est pas mal qu'il soit introduit: il sera pratique de l'utiliser dans certains endroits, mais pas partout. D'une manière ou d'une autre, l'essentiel avec cette nouveauté est de ne pas en faire trop, de ne pas courir plus vite pour mettre à jour tous les projets vers la 3.10 et tout réécrire, car:

C'est mieux que jamais. Bien que jamais ne soit souvent mieux que maintenant.


L'utiliserez-vous? Si oui, où?



All Articles