Brian said:
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.
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:
* 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:
Michael