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