Correspondance de séquences en Python avec match et case

Python 3.10 a introduit la correspondance de motifs via les instructions match et case, offrant une alternative élégante aux structures conditionnelles traditionnelles pour gérer les données structurées. Dans le contexte des séquences, cela permet de décomposer et analyser des listes, tuples ou autres itérables de manière déclarative.

Syntaxe de base de match/case pour les séquences

L'instruction match prend un objet sujet, suivi de blocs case qui définissent des motifs à comparer. La syntaxe générale est :

match sujet:
    case motif:
        # action correspondante
    case _:  # wildcard par défaut
        # action par défaut

Les motifs pour les séquences peuvent inclure des variables pour extraire des éléments, utiliser le symbole _ comme wildcard, et combiner des options avec |. Par exemple, le motif case [premier, *reste] décompose une séquence en son premier élément et le reste des éléments dans une liste.

Caractéristiques clés :

  • Wildcard : _ correspond à n'importe quel élément sans capturer de valeur.
  • Clause as : Permet d'attribuer un nom à un sous-motif, comme case [nom, _, _, (lat, lon) as coord].
  • Multiples options : Utilisation de | pour des alternatives, par exemple case ['A' | 'B', *suite].
  • Extraction étoilée : * capture plusieurs éléments dans une variable, comme dans case [a, *b, c].
  • Correspondance de types : Imposer des types via case [str(nom), ..., (float(lat), float(lon))].
  • Gardes conditionnelles : Ajouter des conditions avec if, par exemple case [...] if condition.

Noter que les motifs de séquence s'appliquent aux instances de collections.abc.Sequence, mais pas aux itérateurs ou aux types non séquentiels comme les chaînes sans conversion.

Application dans un interpréteur Lisp

Pour illustrer l'utilité de match/case, considérons un interpréteur pour un sous-ensemble de Lisp, inspiré de conecpts classiuqes. L'interpréteur comprend une fonction d'évaluation qui interprète des expressions. Comparons une implémentation avec if-else traditionnel à une avec match/case pour une meilleure lisibilité.

Implémentation avec if-else

def evaluer(expr: Expression, environ: Environnement) -> Any:
    "Évalue une expression dans un environnement."
    if isinstance(expr, Symbol):  # référence à une variable
        return environ[expr]
    elif not isinstance(expr, list):  # littéral constant
        return expr
    elif expr[0] == 'quote':  # (quote expr)
        (_, valeur) = expr
        return valeur
    elif expr[0] == 'if':  # (if test consequence alternative)
        (_, test, consequence, alternative) = expr
        if evaluer(test, environ):
            return evaluer(consequence, environ)
        else:
            return evaluer(alternative, environ)
    elif expr[0] == 'lambda':  # (lambda (params...) corps...)
        (_, parametres, *corps) = expr
        return Procedure(parametres, corps, environ)
    elif expr[0] == 'define':
        (_, nom, valeur_expr) = expr
        environ[nom] = evaluer(valeur_expr, environ)
    elif expr[0] == 'set!':
        (_, nom, valeur_expr) = expr
        environ.modifier(nom, evaluer(valeur_expr, environ))
    else:  # (proc arg...)
        (func_expr, *args) = expr
        proc = evaluer(func_expr, environ)
        valeurs = [evaluer(arg, environ) for arg in args]
        return proc(*valeurs)

Implémentation avec match/case

KEYWORDS = ['quote', 'if', 'lambda', 'define', 'set!']

def evaluer(expr: Expression, environ: Environnement) -> Any:
    "Évalue une expression dans un environnement."
    match expr:
        case int(x) | float(x):
            return x
        case Symbol() as nom:
            return environ[nom]
        case ['quote', valeur]:
            return valeur
        case ['if', test, consequence, alternative]:
            if evaluer(test, environ):
                return evaluer(consequence, environ)
            else:
                return evaluer(alternative, environ)
        case ['lambda', [*parametres], *corps] if corps:
            return Procedure(parametres, corps, environ)
        case ['define', Symbol() as nom, valeur_expr]:
            environ[nom] = evaluer(valeur_expr, environ)
        case ['define', [Symbol() as nom, *params], *corps] if corps:
            environ[nom] = Procedure(params, corps, environ)
        case ['set!', Symbol() as nom, valeur_expr]:
            environ.modifier(nom, evaluer(valeur_expr, environ))
        case [func_expr, *args] if func_expr not in KEYWORDS:
            proc = evaluer(func_expr, environ)
            valeurs = [evaluer(arg, environ) for arg in args]
            return proc(*valeurs)
        case _:
            raise ErreurSyntaxe(str_lisp(expr))

L'utilisation de match/case rend le code plus concis et lisible, surtout pour les structures imbriquées comme les lambda et les définitions de fonctions. Les motifs capturent dircetement les sous-éléments, réduisant le besoin de vérifications manuelles et de dépacking explicite.

Étiquettes: Python match-case pattern-matching séquence Lisp-interpreter

Publié le 9 juillet à 07h44