Is there a math expressions parser + evaluator for Python?
I am not the first to ask this question, but answers usually point to eval()
. For instance, one could do this:
>>> safe_list = ['math','acos', 'asin', 'atan', 'atan2', 'ceil', 'cos', 'cosh', 'degrees', 'e', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'hypot', 'ldexp', 'log', 'log10', 'modf', 'pi', 'pow', 'radians', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'abs'] >>> safe_dict = dict([ (k, locals().get(k, None)) for k in safe_list ]) >>> s = "2+3" >>> eval(s, {"__builtins__":None}, safe_dict) 5
But this is not safe:
>>> s_badbaduser = """ ... (lambda fc=( ... lambda n: [ ... c for c in ... ().__class__.__bases__[0].__subclasses__() ... if c.__name__ == n ... ][0] ... ): ... fc("function")( ... fc("code")( ... 0,0,0,0,"KABOOM",(),(),(),"","",0,"" ... ),{} ... )() ... )() ... """ >>> eval(s_badbaduser, {"__builtins__":None}, safe_dict) Segmentation fault
Also, using eval
for parsing and evaluating mathematical expressions just seems wrong to me.
I have found PyMathParser, but it also uses eval
under the hood and is no better:
>>> import MathParser >>> m=MathParser.PyMathParser() >>> m.expression = s_badbaduser >>> m.evaluate(); Segmentation fault
Is there a library available that would parse and evaluate mathematical expression without using Python parser?
Introduction to Python Parser. In this article, parsing is defined as the processing of a piece of python program and converting these codes into machine language. In general, we can say parse is a command for dividing the given program code into a small piece of code for analyzing the correct syntax.
Check out Paul McGuire's pyparsing. He has written both the general parser and a grammar for arithmetic expressions:
from __future__ import division import pyparsing as pyp import math import operator class NumericStringParser(object): ''' Most of this code comes from the fourFn.py pyparsing example http://pyparsing.wikispaces.com/file/view/fourFn.py http://pyparsing.wikispaces.com/message/view/home/15549426 __author__='Paul McGuire' All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it more easily in other places. ''' def pushFirst(self, strg, loc, toks ): self.exprStack.append( toks[0] ) def pushUMinus(self, strg, loc, toks ): if toks and toks[0] == '-': self.exprStack.append( 'unary -' ) def __init__(self): """ expop :: '^' multop :: '*' | '/' addop :: '+' | '-' integer :: ['+' | '-'] '0'..'9'+ atom :: PI | E | real | fn '(' expr ')' | '(' expr ')' factor :: atom [ expop factor ]* term :: factor [ multop factor ]* expr :: term [ addop term ]* """ point = pyp.Literal( "." ) e = pyp.CaselessLiteral( "E" ) fnumber = pyp.Combine( pyp.Word( "+-"+pyp.nums, pyp.nums ) + pyp.Optional( point + pyp.Optional( pyp.Word( pyp.nums ) ) ) + pyp.Optional( e + pyp.Word( "+-"+pyp.nums, pyp.nums ) ) ) ident = pyp.Word(pyp.alphas, pyp.alphas+pyp.nums+"_$") plus = pyp.Literal( "+" ) minus = pyp.Literal( "-" ) mult = pyp.Literal( "*" ) div = pyp.Literal( "/" ) lpar = pyp.Literal( "(" ).suppress() rpar = pyp.Literal( ")" ).suppress() addop = plus | minus multop = mult | div expop = pyp.Literal( "^" ) pi = pyp.CaselessLiteral( "PI" ) expr = pyp.Forward() atom = ((pyp.Optional(pyp.oneOf("- +")) + (pi|e|fnumber|ident+lpar+expr+rpar).setParseAction(self.pushFirst)) | pyp.Optional(pyp.oneOf("- +")) + pyp.Group(lpar+expr+rpar) ).setParseAction(self.pushUMinus) # by defining exponentiation as "atom [ ^ factor ]..." instead of # "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right # that is, 2^3^2 = 2^(3^2), not (2^3)^2. factor = pyp.Forward() factor << atom + pyp.ZeroOrMore( ( expop + factor ).setParseAction( self.pushFirst ) ) term = factor + pyp.ZeroOrMore( ( multop + factor ).setParseAction( self.pushFirst ) ) expr << term + pyp.ZeroOrMore( ( addop + term ).setParseAction( self.pushFirst ) ) self.bnf = expr # map operator symbols to corresponding arithmetic operations epsilon = 1e-12 self.opn = { "+" : operator.add, "-" : operator.sub, "*" : operator.mul, "/" : operator.truediv, "^" : operator.pow } self.fn = { "sin" : math.sin, "cos" : math.cos, "tan" : math.tan, "abs" : abs, "trunc" : lambda a: int(a), "round" : round, # For Python3 compatibility, cmp replaced by ((a > 0) - (a < 0)). See # https://docs.python.org/3.0/whatsnew/3.0.html#ordering-comparisons "sgn" : lambda a: abs(a)>epsilon and ((a > 0) - (a < 0)) or 0} self.exprStack = [] def evaluateStack(self, s ): op = s.pop() if op == 'unary -': return -self.evaluateStack( s ) if op in "+-*/^": op2 = self.evaluateStack( s ) op1 = self.evaluateStack( s ) return self.opn[op]( op1, op2 ) elif op == "PI": return math.pi # 3.1415926535 elif op == "E": return math.e # 2.718281828 elif op in self.fn: return self.fn[op]( self.evaluateStack( s ) ) elif op[0].isalpha(): return 0 else: return float( op ) def eval(self, num_string, parseAll = True): self.exprStack = [] results = self.bnf.parseString(num_string, parseAll) val = self.evaluateStack( self.exprStack[:] ) return val nsp = NumericStringParser() print(nsp.eval('1+2')) # 3.0 print(nsp.eval('2*3-5')) # 1.0
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With