Aller au contenu

La structure match / case

Introduction

Disponible depuis la version 3.10 de Python, la structure match / case permet de sélectionner un traitement selon une valeur, un type ou la structure d’une donnée. Ce mécanisme est appelé filtrage structurel par motifs (structural pattern matching).

Pour un développeur C#, elle correspond à l’instruction switch et les possibilités de pattern matching.

La syntaxe générale

La structure s'implémente de la manière suivante :

match expression:
    case motif_1:
        # Instructions du premier cas
        ...
    case motif_2:
        # Instructions du deuxième cas
        ...
    case _:
        # Instructions du cas général
        ...

L’expression placée après match est évaluée une seule fois. Python examine ensuite les clauses case dans leur ordre d’implémentation. Chaque clause contient un motif décrivant les valeurs ou les structures qu’elle accepte. Lorsqu’un motif correspond, Python exécute le bloc de code associé, puis poursuit après la structure match.

Contrairement à l’instruction switch du langage C#, aucun break n’est nécessaire pour terminer une clause case.

Les deux-points : introduisent les blocs, et l’indentation délimite les instructions qui leur appartiennent.

Comparer des valeurs

Commençons par sélectionner un traitement selon une chaîne de caractères :

command = "save"

match command:
    case "open":
        print("Ouverture du fichier")
    case "save":
        print("Enregistrement du fichier")
    case "quit":
        print("Fermeture de l’application")
    case _:
        print("Commande inconnue")

Les motifs peuvent notamment contenir des chaînes, des nombres, des booléens ou None.

Résultat de l'exécution :

Enregistrement du fichier

La valeur de command correspondant au motif "save", seul le bloc de code associé est exécuté. Voici un exemple comparable en C# :

string command = "save";

switch (command)
{
    case "open":
        Console.WriteLine("Ouverture du fichier");
        break;

    case "save":
        Console.WriteLine("Enregistrement du fichier");
        break;

    case "quit":
        Console.WriteLine("Fermeture de l’application");
        break;

    default:
        Console.WriteLine("Commande inconnue");
        break;
}

Définir un cas général avec _

Le motif _, appelé motif joker (wildcard pattern), est exécuté si aucun cas précédemment évalué est exécuté. Il doit être placé en dernière position :

status_code = 418

match status_code:
    case 200:
        print("Succès")
    case 404:
        print("Ressource introuvable")
    case _:
        print("Autre code de statut")

Résultat de l'exécution :

Autre code de statut

Ce cas est facultatif. Si aucun cas ne correspond, Python poursuit après le bloc match, sans lever automatiquement d’exception :

status_code = 418

match status_code:
    case 200:
        print("Succès")
    case 404:
        print("Ressource introuvable")

print("Fin du traitement")

Résultat de l'exécution :

Fin du traitement

Regrouper plusieurs motifs avec l'opérateur |

L'opérateur | permet de regrouper plusieurs possibilités au sein d'un même cas :

command = "exit"

match command:
    case "quit" | "exit" | "close":
        print("Fermeture de l’application")
    case "save" | "export":
        print("Enregistrement des données")
    case _:
        print("Commande inconnue")

Résultat de l'exécution :

Fermeture de l’application

Dans une expression, l'opérateur | représente une alternative. Il ne faut pas le remplacer par l’opérateur binaire or. En C#, nous pouvons utiliser cet opérateur :

string command = "exit";

string message = command switch
{
    "quit" or "exit" or "close" => "Fermeture de l’application",
    "save" or "export" => "Enregistrement des données",
    _ => "Commande inconnue"
};

Console.WriteLine(message);

Capturer une valeur dans une variable

Un nom utilisé dans une expression capture la valeur examinée :

command = "print"

match command:
    case "save":
        print("Enregistrement du fichier")
    case other:
        print(f"Commande non prise en charge : {other}")

Résultat de l'exécution :

Commande non prise en charge : print

other est une variable qui contient la valeur examinée. elle se distingue de _ qui perme d'ignorer la valeur.

En C#, une capture de la données examinée est implémentée avec l'inférence de type (var) :

string command = "print";

string message = command switch
{
    "save" => "Enregistrement du fichier",
    var other => $"Commande non prise en charge : {other}"
};

Console.WriteLine(message);

Ajouter une condition avec une garde

Une garde est une expression conditionnelle positionnée après la valeur examinée à l’aide du mot-clé if :

age = 20

match age:
    case value if value < 0:
        print("Âge invalide")
    case value if value < 18:
        print("Personne mineure")
    case _:
        print("Personne majeure")

Résultat :

Personne majeure

Python vérifie d’abord la valeur examinée. Lorsqu’elle correspond, le bloc de code correspondant est exécuté.

En C#, l'équivalent consiste à utiliser le mot-clé when :

int age = 20;

string message = age switch
{
    var value when value < 0 => "Âge invalide",
    var value when value < 18 => "Personne mineure",
    _ => "Personne majeure"
};

Console.WriteLine(message);

Reconnaître une séquence

Les motifs de séquence permettent de reconnaître une organisation d’éléments et d’en extraire les valeurs :

position = (10, 20)

match position:
    case (0, 0):
        print("Origine")
    case (x, 0):
        print(f"Axe horizontal : x = {x}")
    case (0, y):
        print(f"Axe vertical : y = {y}")
    case (x, y):
        print(f"Position : x = {x}, y = {y}")
    case _:
        print("Position non reconnue")

Résultat :

Position : x = 10, y = 20

Dans cet exemple :

  • (0, 0) exige deux éléments égaux à zéro.
  • (x, 0) capture le premier élément et exige que le second soit égal à zéro.
  • (0, y) exige que le premier élément soit égal à zéro et capture le second.
  • (x, y) capture les deux éléments.

L’ordre des cas est essentiel : le motif général (x, y) doit venir après les motifs plus précis.

Listes et tuples

Les écritures (x, y) et [x, y] reconnaissent toutes deux des séquences de deux éléments. Elles n’imposent pas respectivement un tuple ou une liste :

position = [10, 20]

match position:
    case (x, y):
        print(f"x = {x}, y = {y}")

Résultat :

x = 10, y = 20

Les chaînes de caractères, les objets bytes et les objets bytearray ne sont pas décomposés par ces motifs. Un itérateur ou un générateur n’est pas non plus reconnu comme une séquence par ce mécanisme.

En C#, les motifs de tuples offrent une possibilité comparable pour les tuples :

var position = (10, 20);

string message = position switch
{
    (0, 0) => "Origine",
    (var x, 0) => $"Axe horizontal : x = {x}",
    (0, var y) => $"Axe vertical : y = {y}",
    (var x, var y) => $"Position : x = {x}, y = {y}"
};

Console.WriteLine(message);

Capturer les éléments restants avec *

Dans un motif de séquence, * permet de capturer plusieurs éléments dans une liste :

numbers = [10, 20, 30, 40]

match numbers:
    case []:
        print("Séquence vide")
    case [first, *remaining]:
        print(f"Premier élément : {first}")
        print(f"Éléments restants : {remaining}")

Résultat :

Premier élément : 10
Éléments restants : [20, 30, 40]

Le motif [first, *remaining] accepte une séquence contenant au moins un élément. Si elle n’en contient qu’un, remaining reçoit une liste vide.

Nous pouvons aussi capturer les éléments intermédiaires :

numbers = [10, 20, 30, 40]

match numbers:
    case [first, *middle, last]:
        print(first)
        print(middle)
        print(last)

Résultat :

10
[20, 30]
40

Un motif de séquence ne peut contenir qu’une seule capture préfixée par *.

Reconnaître un dictionnaire

Un motif de dictionnaire permet de rechercher certaines clés et de capturer leurs valeurs :

message = {
    "action": "create",
    "name": "Clavier",
    "price": 69.90
}

match message:
    case {"action": "create", "name": name}:
        print(f"Création du produit : {name}")
    case {"action": "delete", "id": product_id}:
        print(f"Suppression du produit : {product_id}")
    case _:
        print("Message non reconnu")

Résultat :

Création du produit : Clavier

Le premier motif exige :

  • Une clé "action" dont la valeur est "create".
  • Une clé "name" dont la valeur est capturée dans name.

La clé supplémentaire "price" n’empêche pas la correspondance.

Capturer les autres entrées

La syntaxe **remaining récupère les entrées non mentionnées dans le motif :

match message:
    case {"action": action, **remaining}:
        print(action)
        print(remaining)

Résultat :

create
{'name': 'Clavier', 'price': 69.9}

Reconnaître un type

Un motif de classe permet de sélectionner un traitement selon le type d’une valeur :

value = 42

match value:
    case bool() as flag:
        print(f"Booléen : {flag}")
    case int() as number:
        print(f"Entier : {number}")
    case str() as text:
        print(f"Chaîne : {text}")
    case _:
        print("Autre type")

Résultat :

Entier : 42

Dans ce contexte, int() est un motif de classe : il ne réalise pas de conversion.

Le cas bool() est placé avant int() parce qu’en Python, bool est une sous-classe de int. Sans cette précaution, une valeur booléenne serait également reconnue par le motif int().

En C#, une sélection comparable peut s’écrire ainsi :

object value = 42;

string message = value switch
{
    bool flag => $"Booléen : {flag}",
    int number => $"Entier : {number}",
    string text => $"Chaîne : {text}",
    _ => "Autre type"
};

Console.WriteLine(message);

Examiner les attributs d’un objet

Définissons une classe représentant un produit :

from dataclasses import dataclass


@dataclass
class Product:
    name: str
    price: float

Nous pouvons reconnaître une instance de cette classe et examiner ses attributs :

product = Product("Clavier", 69.90)

match product:
    case Product(name=name, price=price) if price >= 100:
        print(f"Produit coûteux : {name}")
    case Product(name=name):
        print(f"Produit : {name}")
    case _:
        print("Objet non pris en charge")

Résultat :

Produit : Clavier

Dans Product(name=name, price=price) :

  • Product désigne la classe recherchée.
  • Les noms à gauche des signes = désignent les attributs.
  • Les noms à droite capturent les valeurs de ces attributs.

Cette écriture ne construit pas d’objet et n’appelle pas son constructeur.

Une classe ordinaire peut également être utilisée avec ces motifs nommés : le décorateur @dataclass n’est pas obligatoire.

En C#, nous pouvons utiliser un motif de propriétés :

public record Product(string Name, double Price);
object product = new Product("Clavier", 69.90);

string message = product switch
{
    Product { Name: var name, Price: >= 100 }
        => $"Produit coûteux : {name}",

    Product { Name: var name }
        => $"Produit : {name}",

    _ => "Objet non pris en charge"
};

Console.WriteLine(message);

Utiliser un motif positionnel

Une classe décorée avec @dataclass fournit, par défaut, les informations nécessaires aux motifs positionnels :

match product:
    case Product(name, price):
        print(f"{name} : {price}")

L’ordre correspond ici à celui des champs de la classe.

Pour une classe ordinaire, les motifs positionnels reposent sur l’attribut de classe __match_args__. Les motifs nommés évitent de dépendre de cet ordre.

Conserver la valeur reconnue avec as

Le mot-clé as permet de capturer une valeur qui correspond à un motif :

command = "export"

match command:
    case ("save" | "export") as action:
        print(f"Action demandée : {action}")
    case _:
        print("Action inconnue")

Résultat :

Action demandée : export

Le motif vérifie que la valeur est "save" ou "export", puis la capture dans action.

Utiliser une énumération

Les valeurs d’une énumération doivent être référencées avec leur nom qualifié :

from enum import Enum


class OrderStatus(Enum):
    PENDING = 1
    SHIPPED = 2
    CANCELLED = 3


status = OrderStatus.SHIPPED

match status:
    case OrderStatus.PENDING:
        print("Commande en attente")
    case OrderStatus.SHIPPED:
        print("Commande expédiée")
    case OrderStatus.CANCELLED:
        print("Commande annulée")

Résultat :

Commande expédiée

L’écriture OrderStatus.SHIPPED désigne une valeur à comparer. Un nom isolé tel que SHIPPED serait interprété comme une capture.

Produire une valeur

En Python, match est une instruction et ne peut pas être directement utilisé à droite d’une affectation comme une expression switch en C#.

Nous pouvons placer le traitement dans une fonction :

def describe_status(status_code: int) -> str:
    match status_code:
        case 200:
            return "Succès"
        case 404:
            return "Ressource introuvable"
        case 500:
            return "Erreur du serveur"
        case _:
            return "Autre statut"


description = describe_status(404)

print(description)

Résultat :

Ressource introuvable

En C#, une expression switch permet une affectation directe :

int statusCode = 404;

string description = statusCode switch
{
    200 => "Succès",
    404 => "Ressource introuvable",
    500 => "Erreur du serveur",
    _ => "Autre statut"
};

Console.WriteLine(description);

Exemple : analyser des commandes

La combinaison des motifs permet de traiter des commandes de différentes formes :

def execute_command(command: str) -> None:
    match command.split():
        case ["help"]:
            print("Commandes : help, add <nom>, delete <id>, quit")

        case ["add", name]:
            print(f"Ajout du produit : {name}")

        case ["delete", identifier] if identifier.isdecimal():
            print(f"Suppression du produit : {int(identifier)}")

        case ["delete", _]:
            print("L’identifiant doit être un entier positif ou nul.")

        case ["quit"] | ["exit"]:
            print("Fermeture de l’application")

        case _:
            print("Commande inconnue")


execute_command("add Clavier")
execute_command("delete 42")
execute_command("delete abc")

Résultat :

Ajout du produit : Clavier
Suppression du produit : 42
L’identifiant doit être un entier positif ou nul.

La méthode split() produit une liste de mots. Les motifs vérifient leur nombre et leur contenu, puis capturent les arguments utiles.

Dans cet exemple, le nom du produit doit tenir en un seul mot.