Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

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"

        #Each level signifies the ordering of operations
        for level in self.operators:
            for op, func in level.items():
                #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

share|improve this question

1 Answer 1

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.

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.