tags:

views:

314

answers:

7
stringExp = "2^4"
intVal = int(stringExp)      # Expected value: 16

This returns the following error:

Traceback (most recent call last):  
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int()
with base 10: '2^4'

I know that eval can work around this, but isn't there a better and - more importantly - safer method to evaluate a mathematical expression that is being stored in a string?

A: 

What is wrong with eval? If you have some expression as a string, then eval is the way to go.

Just run:

try:
  result = eval('2**4')
except SyntaxError:
  result = 0

Or something like this.

If you want to sanitaze your code, you can use compiler package and parse the given code. If you notice anything but mathematical expressions, just refuse to evaluate the code.

gruszczy
The problem is that the string might really be coming from user input, and eval can execute arbitrary commands.
Tim Goodman
Although in that case he should probably validate that the input has the form of a mathematical expression before passing it to eval()
Tim Goodman
Then he needs a math syntax parser and validate the code. This is easy in Python, though.
gruszczy
+4  A: 

You need to use the eval() function, as in

#btw that's probably what you meant, 2 to the 4th power, not the XOR operation
>>>stringExp = "2**4"  
>>>print(eval(stringExp))
16

Beware that eval(), and its cousin exec() are dangerous tools in the Python's "workshop" because, depending on the origin of the string to be evaluated, the expression could at best simply generate an exception, and at worse, well..., take over the computer or something like that ;-)

Therefore you typically need to

  • parse (if only roughly) the string for "dangerous" keywords etc.; For simple expressions, a simple regex which checks for the absence of letters may just suffice.
  • run the eval in the context of a try-except construct

Better yet, to check the input string against erroneous syntax and malicious code, you could use functions from the ast module (Abstract Syntax Tree)
For example, after ast.parse()ing the expression use ast.walk() to check that the tree only contains an ast.Expr, void of ast.Assign and such.

mjv
A: 

lybniz shows how to use eval() with untrustworthy input.

Ignacio Vazquez-Abrams
A: 

If you don't want to use eval, then the only solution is to implement the appropriate grammar parser. Have a look at pyparsing.

kgiannakakis
+1  A: 

I think I would use eval(), but would first check to make sure the string is a valid mathematical expression, as opposed to something malicious. You could use a regex for the validation.

eval() also takes additional arguments which you can use to restrict the namespace it operates in for greater security.

Tim Goodman
You seem to mean "opposed", not "supposed".
Svante
But, of course, don't rely on regular expressions to validate arbitrary mathematical expressions.
High Performance Mark
@Svante: Right, that was a typo ... fixed now, thanks
Tim Goodman
@High-Performance Mark: Yes, I guess it depends on what sort of mathematical expressions he has in mind . . . e.g., just simple arithmetic with numbers and `+`,`-`,`*`,`/`,`**`,`(`,`)` or something more complicated
Tim Goodman
@Tim -- it's the () I'm worried about, or rather the (((((())))))). In truth, I think OP should worry about them, my brow is unfurrowed by OP's problems.
High Performance Mark
@Mark: Yeah, that could get kind of hairy . . . . Maybe it'd be simpler to just make sure that the string contains only those characters (in whatever order), and then run it in a try-except. Of course one could also use a parser for mathematical expressions, as others have suggested.
Tim Goodman
+4  A: 

Pyparsing can be used to parse mathematical expressions. In particular, fourFn.py shows how to parse basic arithmetic expressions. Below, I've rewrapped fourFn into a numeric parser class for easier reuse.

from __future__ import division
from pyparsing import (Literal,CaselessLiteral,Word,Combine,Group,Optional,
                       ZeroOrMore,Forward,nums,alphas,oneOf)
import math
import operator

__author__='Paul McGuire'
__version__ = '$Revision: 0.0 $'
__date__ = '$Date: 2009-03-20 $'
__source__='''http://pyparsing.wikispaces.com/file/view/fourFn.py
http://pyparsing.wikispaces.com/message/view/home/15549426
'''
__note__='''
All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it
more easily in other places.
'''

class NumericStringParser(object):
    '''
    Most of this code comes from the fourFn.py pyparsing example

    '''
    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 = Literal( "." )
        e     = CaselessLiteral( "E" )
        fnumber = Combine( Word( "+-"+nums, nums ) + 
                           Optional( point + Optional( Word( nums ) ) ) +
                           Optional( e + Word( "+-"+nums, nums ) ) )
        ident = Word(alphas, alphas+nums+"_$")       
        plus  = Literal( "+" )
        minus = Literal( "-" )
        mult  = Literal( "*" )
        div   = Literal( "/" )
        lpar  = Literal( "(" ).suppress()
        rpar  = Literal( ")" ).suppress()
        addop  = plus | minus
        multop = mult | div
        expop = Literal( "^" )
        pi    = CaselessLiteral( "PI" )
        expr = Forward()
        atom = ((Optional(oneOf("- +")) +
                 (pi|e|fnumber|ident+lpar+expr+rpar).setParseAction(self.pushFirst))
                | Optional(oneOf("- +")) + 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 = Forward()
        factor << atom + ZeroOrMore( ( expop + factor ).setParseAction( self.pushFirst ) )
        term = factor + ZeroOrMore( ( multop + factor ).setParseAction( self.pushFirst ) )
        expr << term + ZeroOrMore( ( addop + term ).setParseAction( self.pushFirst ) )
        # addop_term = ( addop + term ).setParseAction( self.pushFirst )
        # general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term)
        # expr <<  general_term       
        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,
                "sgn" : lambda a: abs(a)>epsilon and cmp(a,0) or 0}
    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

You can use it like this:

nsp=NumericStringParser()
result=nsp.eval('2^4')
print(result)
# 16.0
unutbu
Now, that's probably more than what the OP had in mind, but +1 for the relatively compact pyparsing application! Thanks.
mjv
+1  A: 

Use eval in a clean namespace:

>>> ns = {'__builtins__': None}
>>> eval('2 ** 4', ns)
16

The clean namespace should prevent injection. For instance:

>>> eval('__builtins__.__import__("os").system("echo got through")', ns)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute '__import__'

Otherwise you would get:

>>> eval('__builtins__.__import__("os").system("echo got through")')
got through
0

You might want to give access to the math module:

>>> import math
>>> ns = vars(math).copy()
>>> ns['__builtins__'] = None
>>> eval('cos(pi/3)', ns)
0.50000000000000011
krawyoti
+1 for limiting the namespace. Just like in normal code, using `__builtins__.*` is unnecessary.
kaizer.se