Timing out the eval() Function in Python with subprocess

I’ve been working on a Calculator application for my Software Development class for the past week and an interesting problem came up while checking for bugs. When I tried to make it calculate the factorial of a number as large as 1000000, it obviously froze my program and made it quit responding, since the time efficiency of that solution is, well, n!. Anyways, I solved this by using the subprocess module and putting the eval() function in a completely different script.

Solution (UNIX OS’s Only AFAIK):

I have a python script named calculator.py that previously had a method I sent Entry form data to for evaluation. For the sake of this post, I’ll just hard code it and you can figure out what you want to do with it from there.

some function in calculator.py:

import subprocess
.
.
.
# our string variable to be evaluated
userFunction = "9*5-(3/2)"

# send our users function to a subprocess to be evaluated
# the check_output function returns whatever is produced by the Terminal by our evalexpression.py
# '%s' is used as a placeholder for our function string, because we need to be able to send that as a parameter
# shell=True means we run this subprocess in the shell, hence why we can call 'python3 <script name>' as long as
# python3 is in our system PATH
# timeout=3 indicates we want to throw a TimeoutExpired exception if we haven't heard back from the subprocess
# after 3 seconds try:
answer = subprocess.check_output("python3 evalexpression.py '%s'" % userFunction, shell=True, timeout=3)

# strip out commas and b's and stuff that the terminal automatically inserts into the output
answer = answer.decode('ascii')

# remove '\n' from the end of the string and display result on Entry form (Only necessary for my project, using tkinter)
self.replaceText(answer[:len(answer)-1])
except subprocess.TimeoutExpired:
# if this exception gets called after 3 seconds we know the function is too costly to evaluate, so we let the user know
self.replaceText("Operation Timed Out")

evalexpression.py:

def evalexpression(text):
# surround the evaluation in a try block so we make sure there are no errors passed through
try:
output = ""
val = eval(text)

# checks to make sure we aren't evaluating '0' or empty parenthesis
if not(text == '0') and not(isinstance(val, tuple)):
output = eval(text)
else:
output = "0"

except NameError:
output = "Illegal Characters"
except (SyntaxError, AttributeError):
output = "Syntax Error"
except TypeError:
output = "Illegal Expression"
except ZeroDivisionError:
output = "Cannot Divide by 0"
except ValueError:
output = "No Solution"

return output


var = str(sys.argv[1])

# this print statement is needed because calculator.py reads the output to the Terminal
print(evalexpression(var))

If you’d like to see exactly how I implemented this in the context of my Calculator application, the source code can be found here. The master branch of the repo doesn’t use this solution because it is only a viable solution on Unix operating systems and I’m demoing my project on a Windows PC, but I assure you it works!

Leave a Reply

Your email address will not be published. Required fields are marked *