Partage
  • Partager sur Facebook
  • Partager sur Twitter

Meilleure méthode pour 'tokeniser' une chaîne

    25 décembre 2010 à 23:12:26

    Bonjour à tous, pour m'exercer avec Python (qui est un langage que je découvre depuis peu et que j'adore) j'ai décidé de refaire l'atelier calculatrice en utilisant l'algo de Dijkstra : Shunting Yard. Le problème est que je me demande qu'elle est la meilleur façon de 'parser' la chaîne car pour l'instant je fais ça à la bourrin => résultat c'est pas beau et surtout pas robuste.

    En gros je reprends tous mes identificateurs : noms de fonctions, de constantes ou d'opérateurs et je fais une régex avec tout ça, mais j'utilise un join pas très beau car je compte sur le fait qu'aucun opérateur n'utilise la lettre 'Z' dans son nom (pas cool quoi) :

    #!/usr/bin/env python
    # -*- coding: utf-8 -*-
    
    from operator import *
    from collections import namedtuple
    from functools import reduce
    import re, string, math
    
    Op = namedtuple('Op', 'fun arity prec left_assoc')
    Const = namedtuple('Const', 'val')
    Fun = namedtuple('Fun', 'fun arity')
    Par = namedtuple('Par', 'val')
    Comma = namedtuple('Comma', '')
    Token = namedtuple('Token', 'type val')
    
    funs = { 'max': Fun(max, 2),
             'min': Fun(min, 2),
             'ceil': Fun(math.ceil, 1),
             'floor': Fun(math.floor, 1),
             'log': Fun(math.log, 2),
             'exp': Fun(math.exp, 2),
             'complex': Fun(lambda x, y: x + y * 1j, 2) }
    
    consts = { 'pi': Const(math.pi),
               'e': Const(math.e) }
    
    #ops shouldn't not contain any 'Z' see definition of pattern in tokenize (yes it sucks)
    ops = { '**': Op(pow, 2, 10000, False),
            '//': Op(floordiv, 2, 100, True),
            '.-': Op(neg, 1, 1000, False),
            '-': Op(sub, 2, 10, True),
            '*': Op(mul, 2, 100, True),
            '+': Op(add, 2, 10, True),
            '/': Op(truediv, 2, 100, True),
            '%': Op(mod, 2, 100, True),
            '~': Op(invert, 1, 1000, False),
            '<<': Op(lshift, 2, 9, True),
            '>>': Op(rshift, 2, 9, True),
            '&': Op(and_, 2, 8, True),
            '^': Op(xor, 2, 7, True),
            '|': Op(or_, 2, 6, True),
            'not': Op(not_, 1, 5, True),
            '<': Op(lt, 2, 5, True),
            '<=': Op(le, 2, 5, True),
            '>': Op(gt, 2, 5, True),
            '>=': Op(ge, 2, 5, True),
            '!=': Op(ne, 2, 5, True),
            '==': Op(eq, 2, 5, True),
            'and': Op(lambda x, y: bool(x and y), 2, 4, True),
            'or': Op(lambda x, y: bool(x or y), 2, 3, True),
            '!': Op(lambda x: reduce(mul, list(range(1, x + 1))), 1, 100000, True) }
    
    def tokenize(exp):
        """
            Return exp as a list of tokens
        """
        o = list(ops.keys())
        o.sort()
        o.reverse() #pour avoir les doubles ops avant les simples
        pattern = r'\d+\.?\d*|\.\d+|[()]|' + '|'.join(funs) + '|' + '|'.join(consts) + '|' + re.escape('Z'.join(o)).replace('Z', '|')
        
        exp = exp.replace(string.whitespace, '')
    
        if exp == '':
            raise Exception('Empty expression.')
    
        tokens = re.findall(pattern, exp)
    
        for e in tokens:
            if re.match(r'^\d+$', e):
                yield Token(Const, Const(int(e)))
            elif re.findall(r'\d+\.?\d*|\.\d+', e):
                yield Token(Const, Const(float(e)))
            elif e in consts:
                yield Token(Const, consts[e])
            elif e in funs:
                yield Token(Fun, funs[e])
            elif e == ',':
                yield Token(Comma, Comma())
            elif e in '()':
                yield Token(Par, Par(e))
            elif e in ops:
                yield Token(Op, ops[e])
            else:
                raise Exception('Unknown operator: ' + e + '.')
    
    def shunting_yard(exp):
        """
            Apply the shunting-yard algorithm to exp, it transform an infix
            expression to a postfix one
        """
        out, stack = [], []
        for token in tokenize(exp):
            if token.type == Const:
                out.append(token)
            elif token.type == Fun:
                stack.append(token)
            elif token.type == Comma:
                while stack != [] and stack[-1].type != Par: #we never add ')' to the stack...
                    out.append(stack.pop())
    
                if stack == []:
                    raise Exception('Misplaced separator, or mismatched parentheses.')
            elif token.type == Op:
                o1 = token.val
    
                while stack != [] and stack[-1].type == Op:
                    o2 = stack[-1].val
    
                    if (o1.left_assoc and o1.prec <= o2.prec) or (not o1.left_assoc and o1.prec < o2.prec):
                        out.append(stack.pop())
                    else:
                        break
                stack.append(token)
            elif token.type == Par:
                if token.val.val == '(':
                    stack.append(token)
                else:
                    while stack != [] and stack[-1].type == Op:
                        out.append(stack.pop())
                    if stack == [] or stack[-1].type != Par:
                        raise Exception('Mismatched parentheses.')
                    
                    stack.pop()
    
                    if stack != [] and stack[-1].type == Fun:
                        out.append(stack.pop())
    
        while stack != []:
            if stack[-1].type == Par:
                raise Exception('Mismatched parentheses.')
            out.append(stack.pop())
    
        return out
    
    def rpn(out):
        """
            Evaluate the rpn
        """
        v = []
    
        for i in out:
            if i.type == Const:
                v.append(i)
            elif i.val.arity == 0:
                v.append(Token(Const, Const(i.val.fun())))
            elif i.val.arity <= len(v):
                args = v[-i.val.arity:]
                v = v[0:-i.val.arity]
                v.append(Token(Const, Const(
                                i.val.fun(
                                    *(list(map(lambda x: x.val.val, args)))
                                    )
                                )))
            else:
                raise Exception('Not enough args.')
        
        if len(v) != 1:
            raise Exception('Hummm strange problem.')
    
        return v[0].val.val
    
    def calc(exp):
        """
            Evaluate an exp, for instance if exp = 3 * (2 + 4), the function will
            return 18.
        """
        try:
            return rpn(shunting_yard(exp))
        except:
            raise
    
    #(1 / max(max(1, 12), max(23, 3))) * ((4 * 5) - 6) * (78 - 9) * (ceil(pi) // floor(pi))
    #(1<<1)*(2^1)*((3!*2+3)&~.-2**3)
    exp = ''
    while True:
        print('Enter an expression to evaluate (type quit to quit!): ', end = '')
        exp = input()
    
        if exp == 'quit':
            break
        try:
            print('The answer is:', calc(exp))
        except Exception as e:
            print('Sorry there is an error in your expression:')
            print(e)
    


    J'ai bien vu qu'il y a des modules token, tokenize ou même parser mais je ne vois pas comment les utiliser...

    De plus le problème de générer les tokens de cette manière est que même si des expressions bien formées sont correctement évaluées (ouf), des expressions mal formées peuvent passer au travers de ma régex (logique je ne regarde que ce qui "matche") :

    Enter an expression to evaluate (type quit to quit!): (1 / max(max(1, 12), max(23, 3))) * ((4 * 5) - 6) * (78 - 9) * (ceil(pi) // floor(pi))
    The answer is: 42.0
    Enter an expression to evaluate (type quit to quit!): (1<<1)*(2^1)*((3!*2+3)&~.-2**3)
    The answer is: 42
    Enter an expression to evaluate (type quit to quit!): crash i**2
    The answer is: (-1+0j)
    Enter an expression to evaluate (type quit to quit!): quit
    
    Appuyez sur ENTRÉE ou tapez une commande pour continuer


    (btw le code n'est pas forcément optimisé, mais c'est parce que je découvre Python alors j'essaie d'utiliser/tester un peu tout ce que je découvre, lambda, map etc :))
    • Partager sur Facebook
    • Partager sur Twitter

    Meilleure méthode pour 'tokeniser' une chaîne

    × Après avoir cliqué sur "Répondre" vous serez invité à vous connecter pour que votre message soit publié.
    × Attention, ce sujet est très ancien. Le déterrer n'est pas forcément approprié. Nous te conseillons de créer un nouveau sujet pour poser ta question.
    • Editeur
    • Markdown