Patron d'interpréteur Python

Le patron Interpréteur est un patron de conception comportemental qui définit la grammaire d'un langage et établit un interpréteur pour interpréter les phrases dans ce langage. En termes simples, il ressemble à un petit compilateur capable d'analyser et d'exécuter des expressions avec des règles syntaxiques spécifiques.

Idée centrale

Imaginez un scénario de calculatrice : lorsque vous saisissez"2 + 3 * 4"pour une expression comme celle-ci, la calculatrice doit comprendre le sens de la chaîne et effectuer les calculs selon les règles des opérations mathématiques. Le motif Interpréteur est une solution de conception qui met en œuvre ce processus de « compréhension » et d'« exécution ».

Scénarios applicables

Le motif Interpréteur est particulièrement adapté aux situations suivantes :

  • Lorsqu'il est nécessaire d'interpréter et d'exécuter un langage ou une expression
  • Lorsque la grammaire est relativement simple et pas trop complexe
  • Lorsque l'efficacité d'exécution n'est pas un critère essentiel
  • Lorsque les règles de grammaire doivent être fréquemment étendues

Structure du patron

Comprenons les composants essentiels du motif Interpréteur à l'aide d'un diagramme de classes :

Description des composants

AbstractExpression

  • Définit l'interface pour les opérations d'interprétation
  • Contient généralementinterpret()method

TerminalExpression

  • Implémente les opérations d'interprétation liées aux symboles terminaux de la grammaire
  • Ne peut pas être décomposé en expressions plus petites

NonterminalExpression

  • Implémente les opérations d'interprétation liées aux symboles non terminaux de la grammaire
  • Contient généralement des références à d'autres expressions

Contexte

  • Contient certaines informations globales nécessaires à l'interpréteur
  • Stocke les résultats d'entrée et de sortie

Syntaxe de base et implémentation

Classe d'expression abstraite

Exemple

from abc import ABC, abstractmethod

class Expression(ABC):
    """Classe d'expression abstraite, définit l'interface d'interprétation"""
   
    @abstractmethod
    def interpret(self, context):
        """Méthode d'interprétation, implémentée par les sous-classes concrètes"""
        pass

Expression terminale

Exemple

class NumberExpression(Expression):
    """Expression numérique - expression terminale"""
   
    def __init__(self, number):
        self.number = number
   
    def interpret(self, context):
        # Retourner directement la valeur numérique
        return self.number
   
    def __str__(self):
        return f"Number({self.number})"

Expression non terminale

Exemple

class AddExpression(Expression):
    """Expression d'addition - expression non terminale"""
   
    def __init__(self, left, right):
        self.left = left    # Expression gauche
        self.right = right  # Expression droite
   
    def interpret(self, context):
        # Interpréter les expressions gauche et droite respectivement, puis les additionner
        return self.left.interpret(context) + self.right.interpret(context)
   
    def __str__(self):
        return f"({self.left} + {self.right})"

class SubtractExpression(Expression):
    """Expression de soustraction - expression non terminale"""
   
    def __init__(self, left, right):
        self.left = left
        self.right = right
   
    def interpret(self, context):
        return self.left.interpret(context) - self.right.interpret(context)
   
    def __str__(self):
        return f"({self.left} - {self.right})"

class MultiplyExpression(Expression):
    """Expression de multiplication - expression non terminale"""
   
    def __init__(self, left, right):
        self.left = left
        self.right = right
   
    def interpret(self, context):
        return self.left.interpret(context) * self.right.interpret(context)
   
    def __str__(self):
        return f"({self.left} * {self.right})"

Exemple complet : Calculatrice simple

Implémentons une calculatrice simple complète pour interpréter des expressions mathématiques :

Exemple

class CalculatorContext:
    """Contexte de calculatrice, stocke les informations relatives au calcul"""
   
    def __init__(self):
        self.variables = {}  # Stocker les valeurs des variables
   
    def set_variable(self, name, value):
        """Définir la valeur d'une variable"""
        self.variables[name] = value
   
    def get_variable(self, name):
        """Obtenir la valeur d'une variable"""
        return self.variables.get(name, 0)

class VariableExpression(Expression):
    """Expression de variable - expression terminale"""
   
    def __init__(self, name):
        self.name = name
   
    def interpret(self, context):
        # Récupérer la valeur de la variable depuis le contexte
        return context.get_variable(self.name)
   
    def __str__(self):
        return self.name

class Calculator:
    """Classe Calculator - construit et interprète des expressions"""
   
    @staticmethod
    def parse_expression(expression_str, context):
        """
Analyser des expressions de chaîne en un arbre d'expression
Simplifié ici ; les applications réelles peuvent nécessiter un analyseur plus complexe
        """

        # Analyse simple d'expression (pour de vrais projets, il est recommandé d'utiliser une bibliothèque d'analyse dédiée)
        tokens = expression_str.replace('(', ' ( ').replace(')', ' ) ').split()
        return Calculator._parse_tokens(tokens)
   
    @staticmethod
    def _parse_tokens(tokens):
        """Analyser récursivement la liste des jetons"""
        if not tokens:
            return None
           
        token = tokens.pop(0)
       
        if token == '(':
            # Commencer à analyser les expressions composées
            left = Calculator._parse_tokens(tokens)
            operator = tokens.pop(0)
            right = Calculator._parse_tokens(tokens)
            tokens.pop(0)  # Supprimer ')'
           
            if operator == '+':
                return AddExpression(left, right)
            elif operator == '-':
                return SubtractExpression(left, right)
            elif operator == '*':
                return MultiplyExpression(left, right)
        else:
            # Analyser les expressions de base (nombres ou variables)
            if token.isdigit() or (token[0] == '-' and token[1:].isdigit()):
                return NumberExpression(int(token))
            else:
                return VariableExpression(token)

# Exemple d'utilisation
def main():
    # Créer le contexte
    context = CalculatorContext()
    context.set_variable('x', 10)
    context.set_variable('y', 5)
   
    # Construire manuellement l'expression : (x + 3) * (y - 2)
    expression = MultiplyExpression(
        AddExpression(VariableExpression('x'), NumberExpression(3)),
        SubtractExpression(VariableExpression('y'), NumberExpression(2))
    )
   
    print(f"Expression : {expression}")
    result = expression.interpret(context)
    print(f"Résultat : {result}")  # Sortie : (10 + 3) * (5 - 2) = 39
   
    # Utiliser l'analyseur
    simple_expr = AddExpression(NumberExpression(5), NumberExpression(3))
    print(f"Expression simple : {simple_expr} = {simple_expr.interpret(context)}")

if __name__ == "__main__":
    main()

Application avancée : Interpréteur de condition SQL WHERE

Regardons un exemple plus pratique - implémenter un interpréteur simplifié de condition SQL WHERE :

Exemple

class SQLContext:
    """Contexte de requête SQL"""
   
    def __init__(self, record):
        self.record = record  # Enregistrements de données
   
    def get_value(self, field):
        """Obtenir la valeur du champ"""
        return self.record.get(field)

class FieldExpression(Expression):
    """Expression de champ"""
   
    def __init__(self, field_name):
        self.field_name = field_name
   
    def interpret(self, context):
        return context.get_value(self.field_name)

class ConstantExpression(Expression):
    """Expression constante"""
   
    def __init__(self, value):
        self.value = value
   
    def interpret(self, context):
        return self.value

class EqualsExpression(Expression):
    """Expression de comparaison d'égalité"""
   
    def __init__(self, left, right):
        self.left = left
        self.right = right
   
    def interpret(self, context):
        return self.left.interpret(context) == self.right.interpret(context)

class AndExpression(Expression):
    """Expression ET"""
   
    def __init__(self, left, right):
        self.left = left
        self.right = right
   
    def interpret(self, context):
        return self.left.interpret(context) and self.right.interpret(context)

class OrExpression(Expression):
    """Expression OU"""
   
    def __init__(self, left, right):
        self.left = left
        self.right = right
   
    def interpret(self, context):
        return self.left.interpret(context) or self.right.interpret(context)

# Exemple d'utilisation : simuler le filtrage des conditions WHERE SQL
def sql_demo():
    # Simuler des enregistrements de données
    records = [
        {'name': 'Alice', 'age': 25, 'department': 'Ingénierie'},
        {'name': 'Bob', 'age': 30, 'department': 'Sales'},
        {'name': 'Charlie', 'age': 28, 'department': 'Ingénierie'},
        {'name': 'Diana', 'age': 35, 'department': 'Marketing'}
    ]
   
    # Construire la condition WHERE : (department = 'Ingénierie') AND (age > 26)
    class GreaterThanExpression(Expression):
        def __init__(self, left, right):
            self.left = left
            self.right = right
       
        def interpret(self, context):
            return self.left.interpret(context) > self.right.interpret(context)
   
    # Construire l'arbre d'expression
    condition = AndExpression(
        EqualsExpression(FieldExpression('department'), ConstantExpression('Ingénierie')),
        GreaterThanExpression(FieldExpression('age'), ConstantExpression(26))
    )
   
    print("Enregistrements qualifiés :")
    for record in records:
        context = SQLContext(record)
        if condition.interpret(context):
            print(f"- {record['name']}, {record['age']} ans, {record['department']}")

if __name__ == "__main__":
    sql_demo()

Avantages et inconvénients du patron

Avantages

  1. Facile d'étendre la grammaire: L'ajout de nouvelles classes d'expression peut étendre les règles de grammaire
  2. Facile à implémenter: Chaque classe d'expression est relativement simple et facile à implémenter et à maintenir
  3. Grande flexibilité: Peut modifier dynamiquement l'approche d'interprétation
  4. Respecte le principe Open/Closed: Ouvert à l'extension, fermé à la modification

Inconvénients

  1. Problèmes de performance: Le patron Interpreter est généralement inefficace, surtout pour les grammaires complexes
  2. Complexité accrue: Pour les grammaires complexes, un grand nombre de classes sont générées, ce qui augmente la complexité du système
  3. Débogage difficile: Le débogage d'arbres d'expressions complexes peut être difficile

Bonnes pratiques et considérations

Quand l'utiliser

  • Lorsque la grammaire est relativement simple et stable
  • Lorsque l'efficacité d'exécution n'est pas la considération principale
  • Lorsque de nouvelles règles de grammaire doivent être ajoutées fréquemment

Alternatives

Pour les grammaires complexes, envisagez les alternatives suivantes :

  • Utilisez des générateurs d'analyseurs syntaxiques existants(tels que ANTLR, PLY)
  • Utilisez des frameworks de compilateur/interpréteur
  • Envisagez d'autres patrons de conception, tels que le patron Visiteur pour gérer les arbres de syntaxe abstraite

Conseils d'optimisation des performances

Exemple

class OptimizedExpression(Expression):
    """Classe de base d'expression optimisée"""
   
    def __init__(self):
        self._cache = {}  # Ajouter un mécanisme de mise en cache
   
    def interpret(self, context):
        # Utiliser le cache pour éviter les calculs répétés
        cache_key = id(context)
        if cache_key not in self._cache:
            self._cache[cache_key] = self._do_interpret(context)
        return self._cache[cache_key]
   
    def _do_interpret(self, context):
        """Logique d'interprétation réelle, implémentée par les sous-classes"""
        pass

Exercices pratiques

Exercice 1 : Étendre la calculatrice

Ajoutez les fonctionnalités suivantes à la calculatrice :

  • Opération de division
  • Opération modulo
  • Prise en charge de la priorité des parenthèses

Exercice 2 : Implémenter un interpréteur d'expressions booléennes

Créez un interpréteur d'expressions booléennes qui prend en charge :

  • Opérations AND, OR, NOT
  • Opérations de comparaison (>, <, >=, <=, ==, !=)
  • Substitution de variables

Exercice 3 : Concevoir un moteur de règles

Utilisez le patron Interpréteur pour concevoir un moteur de règles simple qui peut :

  • Analyser les règles métier
  • Évaluer les règles en fonction des données d'entrée
  • Produire les résultats de décision

Résumé

Le patron Interpréteur fournit une solution élégante pour gérer les langages dédiés (DSL). Bien que, dans les projets réels, pour les grammaires complexes, nous ayons tendance à utiliser des outils d'analyse syntaxique professionnels, comprendre les principes du patron Interpréteur est très important pour maîtriser les principes de compilation et le traitement du langage.

Rappelez-vous les idées essentielles des patrons de conception :Chaque patron ne convient pas à tous les scénarios ; choisir la solution qui convient le mieux aux besoins actuels est la clé。

Autres extensions