4

Is there a way to create a python function from a string? For example, I have the following expression as a string:

dSdt = "-1* beta * s * i"

I've found a way to tokenize it:

>>> import re
>>> re.findall(r"(\b\w*[\.]?\w+\b|[\(\)\+\*\-\/])", dSdt)
['-', '1', '*', 'beta', '*', 's', '*', 'i']

And now I want to (somehow - and this is the part I don't know) convert it to something with the same behavior as:

def dSdt(beta, s, i):
    return -1*beta*s*i

I've thought about something like eval(dSdt), but I want it to be more general (the parameters beta, s and i would have to be known to exist ahead of time).


Some close requests have linked to this question for evaluating a mathematical expression in a string. This is not quite the same as this question, as I'm looking to define a function from that string.

Clemson
  • 478
  • 5
  • 20
  • Great question - you need a clear set of syntax rules for this - as @Code-Apprentice points out, this is exactly what a compiler does. Good luck! :) – jtlz2 May 22 '20 at 06:49
  • Does this answer your question? [Evaluating a mathematical expression in a string](https://stackoverflow.com/questions/2371436/evaluating-a-mathematical-expression-in-a-string) – jtlz2 May 22 '20 at 06:52

3 Answers3

3

One way, using exec to define a new function from string

 expr = "-1* beta * s * i" 
 name = "dSdt" 
 params = ["beta","s","i"]  # Figure out how to build this array from expression
 param_str = ",".join(params) 
 exec (f"def {name}({param_str}): return {expr}")  

 dSdt(1,2,3)                                                                                                                                                            

 Out[]: -6

If you don't care about defining a reusable function can also use eval with the global object argument.

 expr = "-1* beta * s * i" 
 param= {"beta":1, "s":2, "i":3}  # Find way to build this.
 eval(expr,param)


 Out[]: -6
visibleman
  • 3,175
  • 1
  • 14
  • 27
1

This is exactly what a compiler or interpreter does: translate from one language syntax to another. The main question here is when do you want to be able to execute the resulting function? Is it enough to write the function to a file to be used later by some other program? Or do you need to use it immediately by the parser in some way? For both situations, I would write a parser that creates an Abstract Syntax Tree from the tokens. This means you will need to make a more complex tokenizer that labels each token as an "operator", "number", or "variable". Usually this is done by writing a single regular expression for each type of token.

Then you can build a parser that consumes each token one at a time and builds an Abstract Syntax Tree that represents the expression. There is plenty of material online explaining how to do this, so I suggest some googling. You might also want to look for libraries that help with this.

Finally, you can traverse the AST and either write out the corresponding Python syntax to a file or evaluate the expression with some input for values of variables.

Code-Apprentice
  • 81,660
  • 23
  • 145
  • 268
0

You're talking about how you cannot know the arguments beforehand - that's where *args and **kwargs are very useful!

I like this idea of yours and the tokenize function you made works pretty good.

I made a very general function for you that can handle any expression as long as you add the operators and functions you want to use inside the 'ignore' list. Then you simply need to add the variable values in the order that they appear in the expression.

import re
from math import sqrt

ignore = ["+", "-", "*", "/", "(", ")", "sqrt"]

def tokenize(expression):
    return re.findall(r"(\b\w*[\.]?\w+\b|[\(\)\+\*\-\/])", expression)

def calculate(expression, *args):
    seenArgs = {}
    newTokens = []
    tokens = tokenize(expression)
    for token in tokens:
        try:
            float(token)
        except ValueError:
            tokenIsFloat = False
        else:
            tokenIsFloat = True

        if token in ignore or tokenIsFloat:
            newTokens.append(token)
        else:
            if token not in seenArgs:
                seenArgs[token] = str(args[len(seenArgs)])
            newTokens.append(seenArgs[token])
    return eval("".join(newTokens))

print(calculate("-1* beta * s * i", 1, 2, 3))
print(calculate("5.5 * x * x", 3))
print(calculate("sqrt(x) * y", 9, 2))

Results in:

-6
49.5
6.0
Mandera
  • 2,647
  • 3
  • 21
  • 26