Turning String into Numerical Equation

Michael Spencer mahs at telcopartners.com
Sun Mar 13 02:10:28 EST 2005


Brian Kazian wrote:
> Thanks for the help, I didn't even think of that.
> 
> I'm guessing there's no easy way to handle exponents or logarithmic 
> functions?  I will be running into these two types as well.
> "Artie Gold" <artiegold at austin.rr.com> wrote in message 
> news:39hrh2F61l1n2U1 at individual.net...
> 

eval will handle exponents just fine: try eval("2**16")
in fact, it will evaluate any legal python expression*

logarithmic functions live in the math module, so you will either need to import 
the functions/symbols you want from math, or give that namespace to eval:

  >>> import math
  >>> eval("log(e)", vars(math))
  1.0
  >>>

* this means that, eval("sys.exit()") will likely stop your interpreter, and 
there are various other inputs with possibly harmful consequences.

Concerns like these may send you back to your original idea of doing your own 
expression parsing.  The good news is that the compiler package will parse any 
legal Python expression, and return an Abstract Syntax Tree.  It's 
straightforward to walk the tree and achieve fine-grain control over evaluation.

Here's an example of a math calculator that doesn't use eval.  It evaluates any 
Python scalar numeric expression (i.e., excludes container types), and only 
those symbols and functions that are explicity specified. This code is barely 
tested and probably not bullet-proof.  But with care and testing it should be 
possible to achieve a good balance of functionality and security.


import compiler
import types
import math

# create a namespace of useful funcs
mathfuncs = {"abs":abs, "min": min, "max": max}
mathfuncs.update((funcname, getattr(math,funcname)) for funcname in vars(math)
             if not funcname.startswith("_"))

mathsymbols = {"pi":math.pi, "e":math.e}

# define acceptable types - others will raise an exception if
# entered as literals
mathtypes = (int, float, long, complex)

class CalcError(Exception):
     def __init__(self,error,descr = None,node = None):
         self.error = error
         self.descr = descr
         self.node = node
         #self.lineno = getattr(node,"lineno",None)

     def __repr__(self):
         return "%s: %s" % (self.error, self.descr)
     __str__ = __repr__


class EvalCalc(object):

     def __init__(self):
         self._cache = {} # dispatch table

     def visit(self, node,**kw):
         cls = node.__class__
         meth = self._cache.setdefault(cls,
             getattr(self,'visit'+cls.__name__,self.default))
         return meth(node, **kw)

     def visitExpression(self, node, **kw):
         return self.visit(node.node)

     def visitConst(self, node, **kw):
         value = node.value
         if isinstance(value, mathtypes):
             return node.value
         else:
             raise CalcError("Not a numeric type", value)

     # Binary Ops
     def visitAdd(self,node,**kw):
         return self.visit(node.left) + self.visit(node.right)
     def visitDiv(self,node,**kw):
         return self.visit(node.left) / self.visit(node.right)
     def visitFloorDiv(self,node,**kw):
         return self.visit(node.left) // self.visit(node.right)
     def visitLeftShift(self,node,**kw):
         return self.visit(node.left) << self.visit(node.right)
     def visitMod(self,node,**kw):
         return self.visit(node.left) % self.visit(node.right)
     def visitMul(self,node,**kw):
         return self.visit(node.left) * self.visit(node.right)
     def visitPower(self,node,**kw):
         return self.visit(node.left) ** self.visit(node.right)
     def visitRightShift(self,node,**kw):
         return self.visit(node.left) >> self.visit(node.right)
     def visitSub(self,node,**kw):
         return self.visit(node.left) - self.visit(node.right)

     # Unary ops
     def visitNot(self,node,*kw):
         return not self.visit(node.expr)
     def visitUnarySub(self,node,*kw):
         return -self.visit(node.expr)
     def visitInvert(self,node,*kw):
         return ~self.visit(node.expr)
     def visitUnaryAdd(self,node,*kw):
         return +self.visit(node.expr)

     # Logical Ops
     def visitAnd(self,node,**kw):
         return reduce(lambda a,b: a and b,[self.visit(arg) for arg in node.nodes])
     def visitBitand(self,node,**kw):
         return reduce(lambda a,b: a & b,[self.visit(arg) for arg in node.nodes])
     def visitBitor(self,node,**kw):
         return reduce(lambda a,b: a | b,[self.visit(arg) for arg in node.nodes])
     def visitBitxor(self,node,**kw):
         return reduce(lambda a,b: a ^ b,[self.visit(arg) for arg in node.nodes])
     def visitCompare(self,node,**kw):
         comparisons = {
             "<": operator.lt, # strictly less than
             "<=": operator.le,# less than or equal
             ">": operator.gt, # strictly greater than
             ">=": operator.ge, # greater than or equal
             "==": operator.eq, # equal
             "!=": operator.ne, # not equal
             "<>": operator.ne, # not equal
             "is": operator.is_, # object identity
             "is not": operator.is_not # negated object identity
             }
         obj = self.visit(node.expr)
         for op, compnode in node.ops:
             compobj = self.visit(compnode)
             if not comparisons[op](obj, compobj):
                 return False
             obj  = compobj
         return True
     def visitOr(self,node,**kw):
         return reduce(lambda a,b: a or b,[self.visit(arg) for arg in node.nodes])

     def visitCallFunc(self,node,**kw):

         func = self.visit(node.node, context = "Callable")
         # Handle only positional args
         posargs = [self.visit(arg) for arg in node.args]

         return func(*posargs)

     def visitName(self, node, context = None, **kw):
         name = node.name
         if context == "Callable":
             # Lookup the function only in mathfuncs
             try:
                 return mathfuncs[name]
             except KeyError:
                 raise CalcError("Undefined function", name)
         else:
             try:
                 return mathsymbols[name]
             except KeyError:
                 raise CalcError("Undefined symbol",name)

     def default(self, node, **kw):
         """Anything not expressly allowed is forbidden"""
         raise CalcError("Syntax Error",
                                 node.__class__.__name__,node)


def calc(source):
     walker = EvalCalc()
     try:
         ast = compiler.parse(source,"eval")
     except SyntaxError, err:
         raise
     try:
         return walker.visit(ast)
     except CalcError, err:
         return err

Examples:

  >>> calc("2+3*(4+5)*(7-3)**2")
  434
  >>> eval("2+3*(4+5)*(7-3)**2") # Check
  434
  >>> calc("sin(pi/2)")
  1.0
  >>> calc("sys.exit()")
  Syntax Error: Getattr
  >>> calc("0x1000 | 0x0100")
  4352
  >>>


  Michael







More information about the Python-list mailing list