Ce document explique comment le programme expression.py analyse et évalue une expression mathématique comme a = 10±2. Pas besoin d'être expert en programmation pour suivre.
Le programme fait trois étapes pour comprendre et calculer une expression :
"10±2 + 5" → Tokenize → Build RPN → Evaluate → 15.00 ± 2.00
- Tokenize : découper le texte en morceaux (tokens)
- Build RPN : réorganiser les tokens pour respecter les priorités (+, *, etc.) dans ce qui s'appelle une notation polonaise inversée (i.e. Reverse Polish Notation)
- Evaluate : calculer le résultat en parcourant la liste réorganisée
C'est comme lire une phrase : d'abord on identifie les mots (tokenize), puis on comprend la structure grammaticale (RPN), puis on extrait le sens (evaluate).
Un token, c'est le plus petit morceau significatif d'une expression. Par exemple :
"10±2 + 5" → [NUMBER(10), PLUSMINUS(±), NUMBER(2), ADD(+), NUMBER(5), END]
Chaque token a un type (ce que c'est) et un texte (ce qu'on a lu). Les types possibles sont :
| Type | Exemples | Description |
|---|---|---|
NUMBER |
10, 3.14, 1e-3 |
Un nombre |
NUMBERS |
[10, 11, 12] |
Une liste de nombres |
VAR |
a, pi, x |
Un nom de variable |
ADD, SUB, MUL, DIV |
+, -, *, / |
Les opérateurs de base |
PLUSMINUS |
± |
L'opérateur d'incertitude |
ASSIGN |
= |
Assignation de variable |
DEFINE |
:= |
Définition (lazy) |
FUNCTION |
sin, cos |
Appel de fonction |
LEFT_PAR, RIGHT_PAR |
(, ) |
Parenthèses |
La méthode next_token() essaie de reconnaître le début du texte avec des expressions régulières (regex). Une regex est un patron de texte. Par exemple, la regex pour un nombre est :
r"(-?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)"Ça veut dire : "un signe négatif optionnel, suivi de chiffres, avec éventuellement un point décimal et un exposant". Ça reconnaît 10, 3.14, -2, 1.5e-3, etc. Les expressions régulières sont au texte, comme l'algèbre est aux mathématiques: c'est la meilleure description fonctionnelle d'une expression textuelle.
Le programme essaie chaque regex dans l'ordre. La première qui marche détermine le type du token. Ensuite, il enlève le texte reconnu et recommence avec le reste, jusqu'à ce qu'il n'y ait plus de texte.
"10±2 + 5"
→ match NUMBER "10", reste: "±2 + 5"
→ match PLUSMINUS "±", reste: "2 + 5"
→ match NUMBER "2", reste: " + 5"
→ match ADD "+", reste: " 5"
→ match NUMBER "5", reste: ""
→ ajout ENDOn a maintenant une liste de tokens, mais dans l'ordre où on les a lus (de gauche à droite). Le problème : 1 + 2 * 3 doit donner 7 (pas 9), parce que * a priorité sur +.
La solution : réorganiser les tokens en notation polonaise inversée (RPN : Reverse Polish Notation). En RPN, les opérateurs viennent après leurs opérandes :
Normal : 1 + 2 * 3
RPN : 1, 2, 3, *, +
Pourquoi c'est utile? Parce qu'en RPN, on n'a plus besoin de priorités ni de parenthèses. On lit de gauche à droite et on applique chaque opérateur aux dernières valeurs.
Le code utilise une pile d'opérateurs (operator_stack) pour trier. Les règles sont simples :
- Un nombre ou variable va directement dans la liste RPN
- Un opérateur est comparé avec ce qui est au sommet de la pile :
- Si sa priorité est plus basse ou égale, l'opérateur du sommet va dans la liste RPN
- Sinon, il est empilé
- Une parenthèse ouvrante est empilée
- Une parenthèse fermante vide la pile jusqu'à la parenthèse ouvrante
Exemple avec 1 + 2 * 3 :
Token Action RPN Pile
1 → rpn [1] []
+ → pile (vide) [1] [+]
2 → rpn [1, 2] [+]
* → pile (* > +, empiler) [1, 2] [+, *]
3 → rpn [1, 2, 3] [+, *]
END → vider pile [1, 2, 3, *, +] []
Le résultat [1, 2, 3, *, +] se lit : "prends 2 et 3, multiplie (=6), puis prends 1 et 6, additionne (=7)".
precedence = {
NUMBER: 0, VAR: 0, # les valeurs n'ont pas de priorité
ADD: 5, SUB: 5, # + et - ont la même priorité
MUL: 6, DIV: 6, # * et / sont plus prioritaires
FUNCTION: 7, # les fonctions sont encore plus prioritaires
PLUSMINUS: 5, # ± a la même priorité que + et -
ASSIGN: 3, DEFINE: 3, # = et := sont les moins prioritaires
}Maintenant qu'on a la liste RPN, le calcul est simple. On utilise une pile de valeurs (eval_stack) :
- On lit chaque token de gauche à droite
- Si c'est un nombre : on le met sur la pile
- Si c'est une variable : on cherche sa valeur dans le dictionnaire et on la met sur la pile
- Si c'est un opérateur : on prend les valeurs du sommet de la pile, on fait l'opération, et on remet le résultat
Exemple avec [1, 2, 3, *, +] :
Token Action Pile
1 empiler [1]
2 empiler [1, 2]
3 empiler [1, 2, 3]
* pop 3 et 2, push 2*3 [1, 6]
+ pop 6 et 1, push 1+6 [7]
Résultat : 7. C'est le seul élément restant sur la pile.
Quand le programme rencontre ±, il ne génère pas un seul nombre. Il génère un tableau de 500 000 valeurs aléatoires suivant une distribution gaussienne :
# Au lieu de: result = 10
# On fait: result = [9.8, 10.3, 10.1, 9.7, 10.5, ...] (500 000 valeurs)
distribution = np.random.normal(loc=10, scale=2, size=500000)Ensuite, toutes les opérations se font sur ces 500 000 valeurs en parallèle (grâce à numpy). À la fin, on calcule la moyenne et l'écart-type du tableau résultant.
Le programme garde un dictionnaire global_variables qui associe des noms à des valeurs :
global_variables = {
"pi": 3.14159...,
"e": 2.71828...,
"format": 2,
}Quand on écrit a = 10±2, le programme :
- Évalue
10±2(génère un tableau de 500 000 valeurs) - Stocke ce tableau dans
global_variables["a"]
Quand on écrit ensuite a + 5, le programme :
- Cherche
adans le dictionnaire, trouve le tableau - Additionne 5 à chaque élément du tableau
- Calcule la moyenne et l'écart-type du résultat
Les fonctions sont stockées dans un dictionnaire séparé functions. Quand on écrit f(x) := x*2, l'objet Expression complet est stocké. Quand on appelle f(3), l'expression est réévaluée avec x=3.
Entrée utilisateur : "a + b"
1. Tokenize : [VAR(a), ADD(+), VAR(b), END]
2. Build RPN : [VAR(a), VAR(b), ADD]
3. Evaluate :
- VAR(a) → cherche dans global_variables → tableau [9.8, 10.3, ...]
- VAR(b) → cherche dans global_variables → tableau [19.5, 20.1, ...]
- ADD → additionne les deux tableaux élément par élément
- Résultat : tableau [29.3, 30.4, ...]
- Moyenne : 30.00, Écart-type : 2.24
4. Affichage : "30.00 ± 2.24"
- Regardez la méthode
tokenize()pour voir comment les regex sont utilisées - Regardez
build_rpn_list()pour l'algorithme de tri par priorité - Regardez
evaluate()pour le calcul avec la pile - Essayez d'ajouter une nouvelle fonction mathématique dans le dictionnaire
functions - Essayez de comprendre comment
±%est géré (c'est une combinaison de deux tokens fusionnés dans le RPN)
J'ai écrit ce code en Python d'abord, je l'ai testé, validé, modifié, ensuite j'ai traduit le code en Javascript avec Claude AI car je ne connais pas vraiment Javascript. Ensuite, j'ai eu des idées pour l'interface que j'ai fait en Javascript avec Claude, pour ensuite les ramener dans le code Python (aussi avec Claude). Vous remarquerez que le code a une collection de tests Python et Javascript, on plus de code pour valider que les deux versions du code dans les deux langages donnent le même comportement.
Daniel C. Côté, 3 avril 2026