views:

107

answers:

3

I'm using python, and I want a function that takes a string containing a mathematical expression of one variable (x) and returns a function that evaluates that expression using lambdas. Syntax should be such:

f = f_of_x("sin(pi*x)/(1+x**2)")
print f(0.5)
0.8

syntax should allow ( ) as well as [ ] and use standard operator precedence. Trig functions should have precedence lower than multiplication and higher than addition. Hence the string 'sin 2x + 1' would be equivalent to sin(2x)+1, though both are valid. This is for evaluating user input of algebraic and trigonometric expressions, so think math syntax not programming syntax. The list of supported functions should be easily extensible and the code should be clear and easy to understand. It's OK not to collapse constant expressions.

The sample function here is incomplete. It takes a nested list representing the expression and generates an appropriate function. While somewhat easy to understand, even this seems ugly for python.

import math

def f_of_x(op):
    if (isinstance((op[0]), (int, long, float, complex)) ):
        return (lambda x:op[0])
    elif op[0]=="pi": return lambda x: 3.14159265358979
    elif op[0]=="e": return lambda x: 2.718281828459
    elif op[0]=="x": return lambda x: x
    elif op[0]=="sin": return lambda x: math.sin(f_of_x(op[1])(x))
    elif op[0]=="cos": return lambda x: math.cos(f_of_x(op[1])(x))
    elif op[0]=="tan": return lambda x: math.tan(f_of_x(op[1])(x))
    elif op[0]=="sqrt": return lambda x: math.sqrt(f_of_x(op[1])(x))
    elif op[0]=="+": return lambda x: (f_of_x(op[1])(x))+(f_of_x(op[2])(x))
    elif op[0]=="-": return lambda x: (f_of_x(op[1])(x))-(f_of_x(op[2])(x))
    elif op[0]=="*": return lambda x: (f_of_x(op[1])(x))*(f_of_x(op[2])(x))
    elif op[0]=="/": return lambda x: (f_of_x(op[1])(x))/(f_of_x(op[2])(x))
    elif op[0]=="**": return lambda x: (f_of_x(op[1])(x))**(f_of_x(op[2])(x))
    # should never get here with well formed input
    return

def test():
    # test function f(x) = sin(pi*x)/(1+x**2)
    s = ['/',['sin',['*',['pi'],['x']]],['+',[1],['**',['x'],[2]]]]
    f = f_of_x(s)
    for x in range(30):
        print " "*int(f(x*0.2)*30+10)+"x"

As a general guideline, think of your solution as a tutorial on lambdas and parsers - not code golf. The sample code is just that, so write what you feel is clearest.

+4  A: 

How about this:

import math
def f_of_x(op):
    return eval("lambda x:" + op, math.__dict__)

It can easily be made to support [] as well as () and uses standard operator precedence. It does not let you use trig functions without parens, though, nor does it let you imply multiplication by juxtaposition (like 2x). The list of supported functions is easily extensible, however, and the code is probably as clear and easy to understand as you can get.

If you absolutely need the extra features, look at http://christophe.delord.free.fr/tpg/. The example given on that page can be easily modified to do everything you want.

Gabe
Brilliant. I still need to do timing analysis, but my approach was expected to be faster than calling eval for every value of x. However using eval on a lambda to build a native python function is an awesome idea. Thanks.
phkahler
phkahler: I'd love to see a timing analysis. Please post it if you get a chance.
Gabe
A: 

If you insist on using, for your expressions, a language drastically different from Python (in which all functions are always called with parentheses, while you want to allow several functions to be called without -- allow 2x to stand for 2 * x -- and so forth), you'll first need to parse the string, e.g. with pyparsing (a self-contained solution), or ply (the "python lexx and yacc") if you want a more traditional approach based on a lexical analyzer and a separate "compiler's compiler" (that's what the two "c"s in yacc stand for: Yet Another Compiler's Compiler).

From this parser's output, you then might generate Python syntax to be compiled -- however, there is no real reason to generate a lambda rather than a plain def, since you'll have to compile either of them anyway. So, thinking of the approach as a "tutorial on lambdas" would be really weird, since the decision to use lambda would be arbitrary and very debatable.

We're talking about a few hours' worth of programming, and probably well over 100 lines of resulting Python code if you hope for any clarity whatsoever. I consider this definitely out of scope for what's intended as the normal scope for a SO question and answer.

Substantially less work would be to generate a private kind of bytecode (and have a simple interpreter in Python for that private code) -- but then lambda (which is even in the title of your question, clarifying that you're all-consumed with the importance of using that keyword in the solution) would be even crazier to use (since the obvious approach to implement this variant would be to return, as the "function", the bound method of an instance of an appropriate custom class instead -- so the "bytecode" data and the simple interpreter in python could be appropriately bound together).

lambda survived in Python (and reluctantly, because Guido was originally keen to remove it in the transition to Python 3, and subsided only when faced with a "mass rebellion" invoking the following point...;-) for a single reason: because there's a large number of trivially simple tasks (a function returning a constant, one returning exactly its argument, and so forth) that a very short lambda, with all the limitations connected with its body being just an expression, is handy to perform. Stretching lambdas in Python anywhere beyond this extremely limited role (as you're apparently super-keen to do) is therefore a pretty bad idea.

Alex Martelli
It's not the best, but I would use TPG (http://christophe.delord.free.fr/tpg/) for this task. Do you know how it compares to your options?
Gabe
As stated, converting to python syntax and "compiling" is not an option, although eval() might work. lambda can do the job at runtime (as demonstrated). But yes, the scope might be too large for SO to offer a complete solution in code. My lambdas are all simple - wrappers around operators and functions, just nested.
phkahler
A: 

The example fourFn.py code will give you a pretty good lead on writing this with pyparsing. Adapting that code to meet the OP's specific requirements is left as an exercise for the OP.

-- Paul

Paul McGuire