web-dev-qa-db-fra.com

Comment puis-je scinder une chaîne d'expressions mathématiques en python?

J'ai créé un programme permettant de convertir infix en postfix en python. Le problème est quand je présente les arguments. Si j'introduis quelque chose comme ceci: (ce sera une chaîne)

( ( 73 + ( ( 34 - 72 ) / ( 33 - 3 ) ) ) + ( 56 + ( 95 - 28 ) ) )

il sera divisé avec .split () et le programme fonctionnera correctement. Mais je veux que l'utilisateur puisse introduire quelque chose comme ceci:

((73 + ( (34- 72 ) / ( 33 -3) )) + (56 +(95 - 28) ) )

Comme vous pouvez le constater, je souhaite que les espaces vides puissent être triviaux, mais le programme continue de scinder la chaîne en parenthèses, en nombres entiers (et non en chiffres) et en opérandes.

J'essaie de le résoudre avec un for mais je ne sais pas saisir le nombre entier (73, 34, 72), mais un chiffre à la fois (7, 3, 3, 4, 7, 2)

En résumé, ce que je veux, c'est scinder une chaîne telle que ((81 * 6) /42+ (3-1)) en:

[(, (, 81, *, 6, ), /, 42, +, (, 3, -, 1, ), )]
15
Fernaku

Arbre avec ast

Vous pouvez utiliser ast pour obtenir un arbre de l'expression:

import ast

source = '((81 * 6) /42+ (3-1))'
node = ast.parse(source) 

def show_children(node, level=0):
    if isinstance(node, ast.Num):
        print(' ' * level + str(node.n))
    else:
        print(' ' * level + str(node))
    for child in ast.iter_child_nodes(node):
        show_children(child, level+1)

show_children(node)

Il produit:

<_ast.Module object at 0x7f56abbc5490>
 <_ast.Expr object at 0x7f56abbc5350>
  <_ast.BinOp object at 0x7f56abbc5450>
   <_ast.BinOp object at 0x7f56abbc5390>
    <_ast.BinOp object at 0x7f56abb57cd0>
     81
     <_ast.Mult object at 0x7f56abbd0dd0>
     6
    <_ast.Div object at 0x7f56abbd0e50>
    42
   <_ast.Add object at 0x7f56abbd0cd0>
   <_ast.BinOp object at 0x7f56abb57dd0>
    3
    <_ast.Sub object at 0x7f56abbd0d50>
    1

Comme @ user2357112 l'a écrit dans les commentaires: ast.parse interprète la syntaxe Python et non les expressions mathématiques. (1+2)(3+4) serait analysé comme un appel de fonction et les interprétations de liste seraient acceptées même si elles ne devraient probablement pas être considérées comme une expression mathématique valide.

Liste avec une regex

Si vous voulez une structure plate, une regex pourrait fonctionner:

import re

number_or_symbol = re.compile('(\d+|[^ 0-9])')
print(re.findall(number_or_symbol, source))
# ['(', '(', '81', '*', '6', ')', '/', '42', '+', '(', '3', '-', '1', ')', ')']

Il cherche soit:

  • plusieurs chiffres
  • ou tout caractère qui n'est pas un chiffre ou un espace

Une fois que vous avez une liste d'éléments, vous pouvez vérifier si la syntaxe est correcte, par exemple avec un stack pour vérifier si les parenthèses correspondent ou si chaque élément est connu.

20
Eric Duminil

Vous devez implémenter un tokenizer très simple pour votre entrée. Vous avez les types de jetons suivants:

  • (
  • )
  • +
  • -
  • *
  • /
  • \ d +

Vous pouvez les trouver dans votre chaîne d'entrée séparée par toutes sortes d'espaces.

Une première étape consiste donc à traiter la chaîne du début à la fin, à extraire ces jetons, puis à analyser les jetons, plutôt que la chaîne elle-même.

Une façon astucieuse de procéder consiste à utiliser l'expression régulière suivante: '\s*([()+*/-]|\d+)'. Vous pouvez alors:

import re

the_input='(3+(2*5))'
tokens = []
tokenizer = re.compile(r'\s*([()+*/-]|\d+)')
current_pos = 0
while current_pos < len(the_input):
  match = tokenizer.match(the_input, current_pos)
  if match is None:
     raise Error('Syntax error')
  tokens.append(match.group(1))
  current_pos = match.end()
print(tokens)

Ceci imprimera ['(', '3', '+', '(', '2', '*', '5', ')', ')']

Vous pouvez également utiliser re.findall ou re.finditer, mais vous éviterez alors les non-correspondances, qui sont des erreurs de syntaxe dans ce cas.

12
Horia Coman

Il serait plutôt trivial de lancer à la main un tokenizer d’expression simple. Et je pense que vous en apprendrez davantage de cette façon aussi. 

Donc, dans un souci d'éducation et d'apprentissage, voici une implémentation triviale de tokenizer d'expression qui peut être étendue. Cela fonctionne sur la règle "maximum-much" . Cela signifie qu'il agit "gourmand" en essayant de consommer autant de caractères que possible pour construire chaque jeton.

Sans plus tarder, voici le tokenizer:

class ExpressionTokenizer:
    def __init__(self, expression, operators):
        self.buffer = expression
        self.pos = 0
        self.operators = operators

    def _next_token(self):
        atom = self._get_atom()

        while atom and atom.isspace():
            self._skip_whitespace()
            atom = self._get_atom()

        if atom is None:
            return None
        Elif atom.isdigit():
            return self._tokenize_number()
        Elif atom in self.operators:
            return self._tokenize_operator()
        else:
            raise SyntaxError()

    def _skip_whitespace(self):
        while self._get_atom():
            if self._get_atom().isspace():
                self.pos += 1
            else:
                break

    def _tokenize_number(self):
        endpos = self.pos + 1
        while self._get_atom(endpos) and self._get_atom(endpos).isdigit():
            endpos += 1
        number = self.buffer[self.pos:endpos]
        self.pos = endpos
        return number

    def _tokenize_operator(self):
        operator = self.buffer[self.pos]
        self.pos += 1
        return operator

    def _get_atom(self, pos=None):
        pos = pos or self.pos
        try:
            return self.buffer[pos]
        except IndexError:
            return None

    def tokenize(self):
        while True:
            token = self._next_token()
            if token is None:
                break
            else:
                yield token

Voici une démonstration de l'utilisation:

tokenizer = ExpressionTokenizer('((81 * 6) /42+ (3-1))', {'+', '-', '*', '/', '(', ')'})
for token in tokenizer.tokenize():
    print(token)

Qui produit la sortie:

(
(
81
*
6
)
/
42
+
(
3
-
1
)
)
5
Christian Dean

Réponse regex rapide: re.findall(r"\d+|[()+\-*\/]", str_in)

Manifestation:

>>> import re
>>> str_in = "((81 * 6) /42+ (3-1))"
>>> re.findall(r"\d+|[()+\-*\/]", str_in)
['(', '(', '81', '*', '6', ')', '/', '42', '+', '(', '3', '-', '1', 
')', ')']

Pour la partie des parenthèses imbriquées, vous pouvez utiliser une pile pour garder une trace du niveau.

2
Jingjie YANG

Cela ne donne pas tout à fait le résultat souhaité mais pourrait intéresser les autres utilisateurs de cette question. Il utilise la bibliothèque pyparsing .

# Stolen from http://pyparsing.wikispaces.com/file/view/simpleArith.py/30268305/simpleArith.py
# Copyright 2006, by Paul McGuire
# ... and slightly altered

from pyparsing import *

integer = Word(nums).setParseAction(lambda t:int(t[0]))
variable = Word(alphas,exact=1)
operand = integer | variable

expop = Literal('^')
signop = oneOf('+ -')
multop = oneOf('* /')
plusop = oneOf('+ -')
factop = Literal('!')

expr = operatorPrecedence( operand,
    [("!", 1, opAssoc.LEFT),
     ("^", 2, opAssoc.RIGHT),
     (signop, 1, opAssoc.RIGHT),
     (multop, 2, opAssoc.LEFT),
     (plusop, 2, opAssoc.LEFT),]
    )

print (expr.parseString('((81 * 6) /42+ (3-1))'))

Sortie:

[[[[81, '*', 6], '/', 42], '+', [3, '-', 1]]]
2
Bill Bell

Si vous ne voulez pas utiliser le module re, vous pouvez essayer ceci:

s="((81 * 6) /42+ (3-1))"

r=[""]

for i in s.replace(" ",""):
    if i.isdigit() and r[-1].isdigit():
        r[-1]=r[-1]+i
    else:
        r.append(i)
print(r[1:])

Sortie:

['(', '(', '81', '*', '6', ')', '/', '42', '+', '(', '3', '-', '1', ')', ')']
2
McGrady

En utilisant grako:

start = expr $;
expr = calc | value;
calc = value operator value;
value = integer | "(" @:expr ")" ;
operator = "+" | "-" | "*" | "/";
integer = /\d+/;

grako transpile en python. 

Pour cet exemple, la valeur de retour ressemble à ceci:

['73', '+', ['34', '-', '72', '/', ['33', '-', '3']], '+', ['56', '+', ['95', '-', '28']]]

Normalement, vous utiliseriez la classe de sémantique générée comme modèle pour un traitement ultérieur.

2
Michael Grazebrook

Pour fournir une approche regex plus verbeuse que vous pouvez facilement étendre:

import re

solution = []
pattern = re.compile('([\d\.]+)')

s = '((73 + ( (34- 72 ) / ( 33 -3) )) + (56 +(95 - 28) ) )'

for token in re.split(pattern, s):
    token = token.strip()
    if re.match(pattern, token):
        solution.append(float(token))
        continue
    for character in re.sub(' ', '', token):
        solution.append(character)

Ce qui vous donnera le résultat:

 solution = ['(', '(', 73, '+', '(', '(', 34, '-', 72, ')', '/', '(', 33, '-', 3, ')', ')', ')', '+', '(', 56, '+', '(', 95, '-', 28, ')', ')', ')']
1
clintval