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 exemplecase ['A' | 'B', *suite]. - Extraction étoilée :
*capture plusieurs éléments dans une variable, comme danscase [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 exemplecase [...] 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.