3
\$\begingroup\$

I wrote the following code as part of a larger application. This code takes a string that defines a mathematical function on such as r,y x[0], x[1] etc. or any other variable that additionally can be subscripted (indicated by the x[0] notation) and returns valid python function that accepts the the arguments and computes the final value.

For instance, the string "x+y" gets converted to lambda x,y: x+y.

Since the larger application will be calling the returned function repeatedly I would like to optimise the calling of the returned function. (The overhead for converting the function isn't much of any issue). That is when I noticed a disparity: the equivalent function hard coded into python is over 45 times faster!

Is there a way to reduce the time it takes to call the returned function?

Aside from the performance reason for posting this I would appreciate some constructive criticism regarding the quality and clarity of my code. I am aware that the current version doesn't indicate syntax errors in the entered expression, however I am working on the assumption that some exception will be thrown if that is the case. If this doesn't work in some cases I've missed please let me know.

import math
class Parser:
 """A recursive parser of mathematical expressions that
 supports variable and subscripted variables"""
 #Defines the binary operators that can be used in the expressions
 operators=[['+', lambda x, y: x+y],
 ['-', lambda x, y: x-y],
 ['*', lambda x, y: x*y],
 ['/', lambda x, y: x/y],
 ['^', lambda x, y: x**y]]
 #Defines the functions that can be used in the expressions
 functions={'sin':math.sin,
 'cos':math.cos,
 'tan':math.tan,
 'asin':math.asin,
 'acos':math.acos,
 'atan':math.atan,
 'sqrt':math.sqrt,
 'abs':math.fabs,
 'floor':math.floor,
 'ln': (lambda x: math.log(x,math.e))}
 #Defines the constants that can be used in the expresions
 constants={'pi': math.pi, 'e': math.e}
 def __init__(self,st):
 #Accepts the mathematical string in st
 st=st.strip()#removes whitespace 
 #This provides leading minuses with the implied argument to the minus operator
 if(st[0]=='-'):
 st='0'+st
 #look for any operators that can be tackled
 found_match=self.operator_handle(st)
 if(found_match == 1): return
 #look for additional brackets e.g. ((1+2))
 if(st[0]=='(' and st[-1]==')'):
 #If found replace this nodes content with that inside the ()
 inside_tree=self.__class__(st[1:-1])
 self.function=inside_tree.function
 self.leaves=inside_tree.leaves
 return
 #look for any functions that can be tackled
 found_match=self.function_handle(st)
 if(found_match == 1): return
 self.leaves=[] #make sure the self has "leaves" attribute
 #the expression a singular entity e.g. number or variable
 self.singles_handle(st)
 def operator_handle(self,st):
 "Searches for operators that can be handled"
 #Look for operators in standard order of operations order
 for op, func in self.operators:
 #this variable ensures we don't parse any operators inside parenthesis yet
 braket_level=0
 #the string is revesed to make equal levels of operations be processed left to right
 for char in reversed(range(len(st))):
 #change braket levels when needed
 if(st[char]==')'): braket_level+=1
 elif(st[char]=='('): braket_level-=1
 #operator found outside any brakets
 elif(st[char]==op and braket_level==0):
 #save the associated function in self.function
 self.function=func
 self.leaves=[]
 #leaves contain further levels of recursion on the arguments
 self.leaves.append(self.__class__(st[:char]))
 self.leaves.append(self.__class__(st[char-len(st)+1:]))
 #indicate success to __init__ functions
 return 1
 def function_handle(self,st):
 "Searches for functions that can be handled"
 #tests if the function name matches up with the string beginning
 for fu,func in self.functions.items():
 if(st[:len(fu)] == fu):
 self.function=func
 #still need functions with multiple arguments
 self.leaves=[self.__class__(st[len(fu)+1:-1])]
 #indicate success to __init__ functions
 return 1
 def singles_handle(self,st):
 "Handles static expressions"
 #looks if the string is meant to represent a constant
 if(st.lower() in self.constants):
 self.function=self.constants[st.lower()]
 return
 try:
 #If it is a number set function equal to it
 self.function=float(st)
 except ValueError:
 #Otherwise assume its a variable
 self.function=st
 def travel(self):
 "Returns a python function that reflect the tree of self"
 #if function type is a simple number return a function that returns it
 if(type(self.function)==type(1.0) or type(self.function)==type(1)):
 def actual_function(**kargs):
 return self.function
 elif(type(self.function)==type(' ')):
 #if the function contains [ and ] assume its a subscripted variable
 if('[' in self.function and ']' in self.function):
 def actual_function(**kargs):
 #the variables index "picked" from the list in the user arguments
 return kargs[self.function.split('[')[0]][int(self.function.split('[',1)[1][:-1])]
 else:
 #other wise return a function that returns its variable "picked" from the user arguments
 def actual_function(**kargs):
 return kargs[self.function]
 else:
 #return a function that returns the value of this nodes function using the leaves as arguments
 def actual_function(**kargs):
 return self.function(* list(map(lambda x: x.travel()(**kargs), self.leaves)) )
 #return the function that we determined in the previous step
 return actual_function
if __name__ == '__main__':
 #simple example demoing all the features
 text_function="-2^x[0]*(sin(pi*y))+2*x[1]"
 python_function=Parser(text_function).travel()
 #showing it works
 print(python_function(x=[0,0],y=0.5))
 print(python_function(x=[1,2],y=1.5))
 print(python_function(x=[0,1],y=1.5))
 #create a regular python function for performance testing
 def non_parser_function(x,y):
 return -2**x[0]*(math.sin(math.pi*y))+2*x[1]
 import timeit
 a=timeit.Timer(lambda: python_function(x=[0,0],y=0.5))
 b=timeit.Timer(lambda: non_parser_function(x=[0,0],y=0.5) )
 print("timeit results")
 print("parser: ",a.timeit(1000))
 print("python: ",b.timeit(1000))

Pasteall for better formating

asked Mar 8, 2015 at 21:51
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

While I'm still unsatisfied with the code I managed to take it from 45 times slower to 15 time slower then hard coding.

def travel(self):
 "Returns a python function that reflect the tree of self"
 #if function type is a simple number return a function that returns it
 if(type(self.function)==type(1.0) or type(self.function)==type(1)):
 def actual_function(**kargs):
 return self.function
 elif(type(self.function)==type(' ')):
 #if the function contains [ and ] assume its a subscripted variable
 if('[' in self.function and ']' in self.function):
 var_name=self.function.split('[')[0]
 var_index=int(self.function.split('[',1)[1][:-1])
 def actual_function(**kargs):
 #the variables index "picked" from the list in the user arguments
 return kargs[var_name][var_index]
 else:
 #other wise return a function that returns its variable "picked" from the user arguments
 def actual_function(**kargs):
 return kargs[self.function]
 else:
 #return a function that returns the value of this nodes function using the leaves as arguments
 function_list=list(map(lambda x: x.travel(), self.leaves))
 def actual_function(**kargs):
 return self.function(* [x(**kargs) for x in function_list] )
 # def actual_function(**kargs):
 # return self.function(* list(map(lambda x: x.travel()(**kargs), self.leaves)) )
 #return the function that we determined in the previous step
 return actual_function

The key lies in redefining travel such that as much as possible is calculated outside actual_function.

answered Mar 9, 2015 at 0:52
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.