INTRODUCTION

Comme j'ai beaucoup travaillé avec des calculatrices HP utilisant cette technique de calcul, je me suis amusé à en faire une en Python qui fonctionne en console.

Le principe de la "notation polonaise inverse" est de mettre les opérateurs après les données qu'ils doivent traiter. Ainsi, un calcul '2+3' s'écrira en fait: '2 3 +'. Autre exemple plus complexe: un calcul '3+(1+2)*4' s'écrira comme '3 1 2 + 4 * +'. Plus d'information ici:
https://fr.wikipedia.org/wiki/Notati...onaise_inverse

L'avantage principal est qu'on se passe complètement des parenthèses. En contrepartie, il faut un peu de pratique pour faire ça couramment...

C'est une technique de traitement basée sur une pile 'lifo' (last in, first out). Il y a d'ailleurs un langage complet basé sur cette technique, le 'Forth', que j'ai un peu pratiqué dans le passé:
https://fr.wikipedia.org/wiki/Forth_(langage) .

Ce que je vous propose ici, c'est un code avec comme priorité la simplicité et la lisibilité. J'ai donc exclu les astuces complexes qui pourraient rendre le calcul plus rapide. J'ai aussi exclu les nombreuses vérifications qu'il faudrait faire pour en faire un outil de production (en prenant en charge toutes les bêtises possibles de l'utilisateur...), mais il vous sera facile de les ajouter. J'ai aussi facilité la possibilité d'ajouter toutes les fonctions dont vous avez besoin, autant que vous le souhaitez. Vous pouvez donc vous faire une calculatrice NPI sur mesure...


DESCRIPTION DU CODE


Pile 'lifo'

Pour stocker les opérateurs, ses arguments, et son résultat, on utilise une pile 'lifo' (last in, first out). La pile ici s'appelle 'pile' (ça, c'est original ) qui est de type 'list'. On la met en variable globale pour éviter les très nombreux passages de paramètres. On pourrait aussi utiliser les listes de type 'deque' du module 'collections'. La déclaration de la pile est donc ici (il est difficile de faire plus simple...):

Dictionnaire des opérateurs et fonctions associées

Il faut pouvoir reconnaître l'opérateur ('+', '-', etc...) et savoir quelle fonction doit être exécutée. Le meilleur objet le plus rapide pour faire ça est un dictionnaire. Voilà celui utilisé ici:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
# dictionnaire des opérateurs
operateurs = {'+':plus, '-':moins, '*':mult, '/':divis, '//': divisint,
            '^':puiss, 'dup': dup, 'swap': swap, 'sin': sinus, 'cos': cosinus,
            'sqrt': mathsqrt, 'pi': mathpi, 'sum': sommeliste,
            'mult': multliste, 'factprem': factprem}
Ce dictionnaire est placé en variable globale pour les mêmes raisons que la pile.


Dictionnaire des variables et de leurs valeurs

Au court d'une session, on peut créer une variable, lui affecter une nouvelle valeur et l'utiliser dans une expression à calculer. Ces variables et leurs valeurs sont placées dans un dictionnaire pour une rapidité d'accès.

Pour déclarer une nouvelle variable ou changer sa valeur, on tape le nom de la variable précédée de ':' (exemple ':toto'). Quand on utilise la variable, on n'utilise pas ce caractère (exemple 'toto').

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
# dictionnaire des variables
variables = {}
Ce dictionnaire est placé en variable globale pour les mêmes raisons que la pile.


Fonctions de traitement associées aux opérateurs

Chaque opérateur reconnu dans l'expression à calculer lancera la fonction qui lui est associée, et qui:

- dépilera les arguments donc elle a besoin

- fera le traitement demandé

- empilera le résultat

Voilà l'exemple de la fonction associée à '+':

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
def plus():
    """ opérateur '+'
    """
    pile.append(pile.pop()+pile.pop())
Il faut, bien sûr, faire attention aux fonctions qui nécessitent que les arguments soient dans un ordre donné. Voilà l'exemple d'une fonction division associée à l'opérateur '/':

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
def divis():
    """ opérateur '/'
    """
    b, a = pile.pop(), pile.pop()
    pile.append(a/b)
Fonction d'évaluation de l'expression saisie

La fonction d'évaluation de l'expression saisie, convertit cette expression en liste si nécessaire, et teste chaque élément de cette liste (token):

- si c'est un opérateur => on exécute la fonction associée à cet opérateur, qui récupèrera les arguments dont elle a besoin sur la pile et empilera son résultat.

- si c'est une chaîne de caractère précédée de ':', il s'agit d'une nouvelle variable, ou d'une variable existante dont on veut changer la valeur. Sa valeur est celle du sommet de la pile.

- si c'est une variable existante dans le dictionnaire des variables, on la remplace par sa valeur.

- dans les autres cas, c'est un argument, et on l'empile sur la pile. Comme l'argument est une chaine (str), on utilise eval pour le convertir en objet Python. Cela permettra d'avoir, bien sûr, des nombres (int et float), mais aussi des objets plus complexes comme des listes (type list) ou des dictionnaires (type dict). Ainsi, cette calculatrice supporte le traitement des objets Python quelconques: int, float, list, dict, ...

Voilà la fonction d'évaluation NPI:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def eval_npi(expression):
    """Calcule une expression (chaine ou liste) écrite selon la notation
       polonaise inverse. Si l'expression est une chaine, les données sont
       séparées par des espaces. Possibilité d'utiliser des variables.
       Attention: une donnée ne doit pas elle-même contenir d'espaces !
    """
    # Convertit en liste si nécessaire ======================================
    if isinstance(expression, str):
        expression = expression.split()
 
    # Calcule ===============================================================
    for token in expression:
 
        if token in operateurs:
            # => lancement de la fonction associée à l'opérateur reconnu
            operateurs[token]()
 
        elif token.startswith(':'):
            # création de la variable ou affectation d'une nouvelle valeur
            variables[token[1:]] = pile[-1]
 
        elif token in variables:
            # empilage de la valeur de la variable reconnue
            pile.append(variables[token])
 
        else:
            # empilage de la donnée dans son type d'origine (quelconque)
            pile.append(eval(token))
 
    # Lit, dépile et retourne le résultat ===================================
    return pile.pop()

Interpréteur pour la console

C'est une fonction avec une boucle de type 'read-eval-print' qui ne s'arrêtera que si on le demande (ici => saisie de 'stop'), et fera donc:

- saisie de l'expression à calculer

- test pour savoir si l'utilisateur demande l'arrêt de la calculatrice

- évaluation de l'expression à calculer par la fonction précédente 'eval_npi(expression)

- affichage du résultat de cette évaluation

- retour à la saisie

Voilà la fonction proposée:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def interp_npi():
    """Interpréteur (boucle read-eval-print) de calcul en notation polonaise
       inverse. Pour la saisie, chaque donnée est séparée par un espace.
       Attention: une donnée ne doit pas contenir elle-même d'espace !
       Écrire "stop" pour arrêter le programme
    """
    while True:
 
        # saisit l'expression à calculer ====================================
        expression = input("?: ")
 
        # teste pour arrêter le programme ===================================
        if expression.strip() == "stop":
            print("Arrêt demandé")
            break
 
        # Calcule l'expression ==============================================
        try:
            resultat = eval_npi(expression)
        except Exception as msgerr:
            resultat = "Erreur: " + str(msgerr)
 
        # Affiche le résultat ===============================================
        print(resultat)
Vous voyez que, en cas d'erreur de calcul, cet interpréteur récupère l'exception et en affiche le message d'erreur. Par exemple:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
?: 1 0 /
Erreur: division by zero
Exemples d'utilisation de l'interpréteur en console

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# en Python: 2+3
?: 2 3 +
5
 
# en Python: 3+(1+2)*4
?: 3 1 2 + 4
15
 
# en Python: 2**100
?: 2 100 ^
1267650600228229401496703205376
 
# en Python: sin(5)
?: 5 sin
-0.9589242746631385
 
# en Python: sum([1,2,3,4,5])
?: [1,2,3,4,5] sum
15
 
# en Python: math.sqrt(2)
?: 2 sqrt
1.4142135623730951
 
# en Python: math.pi
?: pi
3.141592653589793
Ajout de fonctions supplémentaires

On voit bien ici la facilité avec laquelle on peut ajouter des fonctions supplémentaires:

- on ajoute au dictionnaire le nom de l'opérateur comme clé, avec comme valeur le nom de la fonction de traitement (sans ses parenthèses!).

- on ajoute la fonction de traitement

A titre d'exemple, j'ai mis comme fonction supplémentaire la décomposition d'un nombre entier en facteurs premiers, qu'on ne trouve pas souvent dans une calculatrice. Opérateur: 'factprem' et fonction à appeler: 'facteurspremiers'. Voilà un exemple d'utilisation:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
?: 12345678901234567890 factprem
[2,3,3,5,101,3541,3607,3803,27961]
On peut bien sûr vérifier que c'est juste puisque j'ai ajouté une fonction qui calcule le produit de tous les éléments d'une liste: 'multlist' qui sera appelé par l'opérateur 'mult':

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
?: [2,3,3,5,101,3541,3607,3803,27961] mult
12345678901234567890

Et voilà le code complet:

Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# -*- coding: utf-8 -*-
 
import math
 
#############################################################################◙
def facteurspremiers(n):
    """Décompose un nombre entier n en facteurs premiers par la méthode des
       divisions. Retourne la liste des facteurs premiers trouvés.
       Si la liste ne retourne qu'un seul élément égal à n: n est premier
    """
    n = int(n) # au cas où n ne serait pas entier
    if n<2:
        return [] # pas de nombre premier avant 2
    np = [] # liste pour stocker les nombres premiers trouvés
 
    # recherche de tous les facteurs 2 s'il y en a ==========================
    while n>=2:
        q, r = divmod(n, 2)
        if r==0:
            np.append(2) # on a trouvé un nouveau facteur 2
            n = q
        else:
            break # plus de facteur 2
 
    # recherche les facteurs premiers > 2 ===================================
    i = 3
    rac2n = int(math.sqrt(n))+1 # limite de recherche pour i
    while i<=n:
        if i > rac2n:
            np.append(n) # on empile le n restant
            break # on a fini
        q, r = divmod(n, i)
        if r==0:
            np.append(i) # on a trouvé un nouveau facteur premier
            n = q
        else:
            i += 2 # on ne teste que les nombres impairs
 
    # retourne la liste des nombres premiers trouvés ========================
    return np
 
#############################################################################
def plus():
    """ opérateur '+'
    """
    pile.append(pile.pop()+pile.pop())
 
#============================================================================
def moins():
    """ opérateur '-'
    """
    b, a = pile.pop(), pile.pop()
    pile.append(a-b)
 
#============================================================================
def mult():
    """ opérateur '*'
    """
    pile.append(pile.pop()*pile.pop())
 
#============================================================================
def divis():
    """ opérateur '/'
    """
    b, a = pile.pop(), pile.pop()
    pile.append(a/b)
 
#============================================================================
def divisint():
    """ opérateur '//'
    """
    b, a = pile.pop(), pile.pop()
    pile.append(int(a//b))
 
#============================================================================
def puiss():
    """ opérateur puissance '^' (** en Python)
    """
    b, a = pile.pop(), pile.pop()
    pile.append(a**b)
 
#============================================================================
def dup():
    """ duplique le dernier élément de la pile
    """
    pile.append(pile[-1])
 
#============================================================================
def swap():
    """ inverse les deux derniers éléments de la pile
    """
    b, a = pile.pop(), pile.pop()
    pile.append(b)
    pile.append(a)
 
#============================================================================
def sinus():
    """Calcule le sinus
    """
    pile.append(math.sin(pile.pop()))
 
#============================================================================
def cosinus():
    """Calcule le cosinus
    """
    pile.append(math.cos(pile.pop()))
 
#============================================================================
def mathpi():
    """Empile la valeur de pi du module math
    """
    pile.append(math.pi)
 
#============================================================================
def mathsqrt():
    """Calcule la racine carrée de la dernière valeur empilée
    """
    pile.append(math.sqrt(pile.pop()))
 
#============================================================================
def sommeliste():
    """Calcule la somme des éléments d'une liste
    """
    pile.append(sum(pile.pop()))
 
#============================================================================
def multliste():
    """Calcule le produit des éléments d'une liste
    """
    liste = pile.pop()
    produit = 1
    for elem in liste:
        produit *= elem
    pile.append(produit)
 
#============================================================================
def factprem():
    """Décompose le nombre au sommet de la pile en facteur premiers par la
       méthode des divisions. Empile les résultats sous forme de liste
    """
    pile.append(facteurspremiers(pile.pop()))
 
#############################################################################
def eval_npi(expression):
    """Calcule une expression (chaine ou liste) écrite selon la notation
       polonaise inverse. Si l'expression est une chaine, les données sont
       séparées par des espaces. Possibilité d'utiliser des variables.
       Attention: une donnée ne doit pas elle-même contenir d'espaces !
    """
    # Convertit en liste si nécessaire ======================================
    if isinstance(expression, str):
        expression = expression.split()
 
    # Calcule ===============================================================
    for token in expression:
 
        if token in operateurs:
            # => lancement de la fonction associée à l'opérateur reconnu
            operateurs[token]()
 
        elif token.startswith(':'):
            # création de la variable ou affectation d'une nouvelle valeur
            variables[token[1:]] = pile[-1]
 
        elif token in variables:
            # empilage de la valeur de la variable reconnue
            pile.append(variables[token])
 
        else:
            # empilage de la donnée dans son type d'origine (quelconque)
            pile.append(eval(token))
 
    # Lit, dépile et retourne le résultat ===================================
    return pile.pop()
 
#############################################################################
def interp_npi():
    """Interpréteur (boucle read-eval-print) de calcul en notation polonaise
       inverse. Pour la saisie, chaque donnée est séparée par un espace.
       Attention: une donnée ne doit pas contenir elle-même d'espace !
       Ecrire "stop" pour arrêter le programme
    """
    while True:
 
        # saisit l'expression à calculer ====================================
        expression = input("?: ")
 
        # teste pour arrêter le programme ===================================
        if expression.strip() == "stop":
            print("Arrêt demandé")
            break
 
        # Calcule l'expression ==============================================
        try:
            resultat = eval_npi(expression)
        except Exception as msgerr:
            resultat = "Erreur: " + str(msgerr)
 
        # Affiche le résultat ===============================================
        print(resultat)
 
#############################################################################
if __name__ == "__main__":
 
    # pile de calcul ========================================================
    pile = []
 
    # dictionnaire des opérateurs ===========================================
    operateurs = {'+':plus, '-':moins, '*':mult, '/':divis, '//': divisint,
            '^':puiss, 'dup': dup, 'swap': swap, 'sin': sinus, 'cos': cosinus,
            'sqrt': mathsqrt, 'pi': mathpi, 'sum': sommeliste,
            'mult': multliste, 'factprem': factprem}
 
    # dictionnaire des variables ============================================
    variables = {}
 
    # Lancement de l'interpréteur NPI =======================================
    interp_npi()

CONCLUSION

Vous voyez que, malgré la simplicité de son code (copieusement commenté), l'ajout possible de nouvelles fonctions et la possibilité de traiter n'importe quel objet Python donnent à cette calculatrice NPI de grosses potentialités qu'on ne trouve pas ailleurs sur le web.

Mais ce n'est qu'une calculatrice et non un langage! Pour que ça en devienne un, il faudrait ajouter beaucoup de choses, en commençant par des structures de contrôle (if, for, while, ...). On pourrait peut-être s'inspirer du langage Forth? Un simili Forth qui pourrait traiter n'importe quel objet Python. Un rève ? Et pourquoi pas un projet?

Amusez-vous bien! Et bons calculs NPI !