3
\$\begingroup\$

I have written a simple command line program in Python 3 that accepts a function (in the mathematical sense) from a user and then does various "calculus things" to it. I did it for my own purposes while taking a math class, so I was more concerned with getting it to output the right number than with security.

I was just hoping to get some feedback as to whether this program is readable, understandable, and if there are any serious issues with it.

Here is the file that defines my data structures, which is what I'm mostly concerned with at the moment (datums.py):

from fractions import Fraction
from copy import copy
from collections import namedtuple

class Point(namedtuple("Point", ["x", "y"])):
    """
    Represents a point in two dimensional space.

    Probably in R^2, but you can store anything you want.
    """
    def __str__(self):
        return "({x}, {y})".format(x=self.x, y=self.y)

class Function(object):
    """
    A Function in the mathematical sense. Basically it's just a lambda in the background
    with some fancy formatting thrown in.
    """
    def __init__(self, args, expr, name="f"):
        """
        Create the "function" object.

        args: A comma (and potentially whitespace) separated list of variable names
        expr: A mathematical expression in terms of the variables given in args.
              Any valid python expression is valid, and any function in the math module
              is available.
        name: A friendly name can be given to this function, but it only affects the
              formatting when it's printed out and not any of its actual behavior
        """
        self.name = name
        self._args = [i.strip() for i in args.split(",")];
        self._expr = expr;
        self._func = eval("lambda {v}: {x}".format(v=args, x=expr), _mathdict())

    def __str__(self):
        args = ", ".join(self._args)
        return "{name}({v}) = {e}".format(name=self.name,
                                          v=args,
                                          e=self._expr)

    def __call__(self, *args, **kwargs):
        return self._func(*args, **kwargs)


class Graph(object):
    """
    A "graph"... Precalculates a set of points in two dimensional space
    over a given interval and with a given distance from each other,
    and stores their locations to speed up certain other processes.
    """
    def __init__(self, func, low, high, steps, include_end=False):
        """
        Initialize a graph. 
        Calculates func(x) at a set of different points,
        each at a distance of ((high - low) / steps) from its neighbors,
        starting at {low} and ending at {high} (inclusive).

        func: a Function() object.
        low: the starting x coordinate of the graph.
        high: the ending x coordinate of the graph. This point will be computed.
        steps: The number of points to be computed, excluding the endpoint.
        include_end: A boolean representing whether you want an iterator over this
                     object to include the end. The last point will be computed and
                     stored regardless.
        """
        self.func = func
        self.interval = (low, high)
        self.n = steps
        self.include_end = include_end
        self.delta = Fraction((high - low) / steps).limit_denominator()

        xs = tuple(float(low + i * self.delta) for i in range(steps + 1))
        ys = tuple(func(x) for x in xs)
        self.points = tuple(Point(x, y) for x, y in zip(xs, ys))

    def __getitem__(self, sl):
        return self.points[sl]

    def __len__(self):
        return self.n + (1 if self.include_end else 0)

    def __iter__(self):
        return iter(self.points[:len(self)])

    def __min__(self):
        return min(self, key=lambda p: p[-1])

    def __max__(self):
        return max(self, key=lambda p: p[-1])

    def __str__(self):
        interval = "[{a}, {b}{close}".format(a=self.interval[0],
                                             b=self.interval[1],
                                             close="]" if self.include_end else ")")
        mesg = "interval={inter}, n={n}, delta={d}"
        return mesg.format(func=self.func,
                           inter=interval,
                           n=self.n,
                           d=self.delta)

    def with_include_end(self, include_end):
        """
        Copy this object to a new graph, with all of the same points and the same
        function, but with include_end set as given.
        """
        new = copy(self)
        new.include_end = include_end
        return new

def _mathdict():
    """
    Imports math, then pulls out all of its Functions that aren't private
    into a dict for use in eval-ing a lambda.
    """
    import math
    return {f: getattr(math, f) for f in dir(math) if not f.startswith("_")}

Notice in the Function's __init__() method, it takes the argument list and the expression, then eval's a lambda. I know that running eval() on unsanitized user input is an enormous no-no. (eval("[i for i in open('/dev/zero')]") for instance). But in this case, the user is me only so "user data" can be considered safe. If I ever go big-time with this, then I'll look in to sanitizing it but for now, in this case, that's not an issue of concern

The original version of this program's Function just stored expr and then eval'd it in __call__, but that was orders of magnitude slower than this way (and in this case, it's not premature optimization, since if I'm doing Simpson's method with n=9000000, it can take some time).

Is there a better way to have arbitrary expressions created and evaluated at runtime than I'm doing? It feels a little odd to do it this way.

Here's the driver file (__main__.py):

from sys import argv
import integrals
import diffapprox

_commands = {"integrate": integrals,
             "diffapprox": diffapprox}

def show_help():
    print("Need a command. One of:")
    for name, pkg in _commands.items():
        print("{name}: {desc}".format(name=name, desc=pkg.desc))


def main():
    try:
        command = argv[1]
        job = _commands[command]
    except:
        show_help()
        return

    job.main(*argv[2:])

if __name__ == "__main__":
    main()

Here's an example use file (integrals.py):

from datums import Function, Graph, Point

desc = "Perform approximate numerical integration"

def left_rectangles(graph):
    g = graph.with_include_end(False)
    return g.delta * sum(p.y for p in g)

def right_rectangles(graph):
    g = graph.with_include_end(True)
    pts = g[1:]
    return g.delta * sum(p.y for p in pts)

def midpoint_rule(graph):
    g = graph.with_include_end(False)
    f = graph.func
    halfdelta = graph.delta / 2
    midpoints = [p.x + halfdelta for p in g]
    return g.delta * sum(f(x) for x in midpoints)

def trapezoid_rule(graph):
    g = graph.with_include_end(True)
    s = g[0].y + g[-1].y + 2 * sum(p.y for p in g[1:-1])
    return s * g.delta / 2

def simpsons_rule(graph):
    g = graph.with_include_end(True)
    s = g[0].y + g[-1].y
    s += 4 * sum(p.y for p in g[1:-1:2])
    s += 2 * sum(p.y for p in g[2:-1:2])
    return s * g.delta / 3

def parseargs(a, e, l, h, n):
    """
    Return a tuple of arguments.
    a = arguments to the function
    e = expression of the function
    l = lowpoint
    h = highpoint
    n = n
    """
    return (a,
            e,
            float(l),
            float(h),
            int(n),)

__all__ = [left_rectangles,
           right_rectangles,
           midpoint_rule,
           trapezoid_rule,
           simpsons_rule]

def show_help():
    print("Usage:")
    print()
    print("integrate {vars} {expression} {low} {high} {n}")
    print()
    print("Integrates f({vars}) from {low} to {high} with the given integer n.")

def main(*args):
    try:
        args, expr, low, hi, n = parseargs(*args)
    except:
        show_help()
        return

    function = Function(args, expr)
    graph = Graph(function, low, hi, n)
    # methods will be mapped to their names, formatted for the user's viewing pleasure
    methods = {method.__name__.replace("_", " ").title(): method for method in __all__}
    longest = max(len(name) for name in methods.keys())

    print(function)
    print(graph)
    print()

    for name, method in methods.items():
        print("{name:<{len}}: {approx:.10n}".format(name=name, len=longest, approx=float(method(graph))))

There is more to this program but that's the gist of it. Is this okay? Terrible? Any feedback would be appreciated.

Sample input:

python MathJunk x "x ** 2" 0 1 10

f(x) = x ** 2
interval=[0.0, 1.0), n=10, delta=1/10

Right Rectangles: 0.385
Midpoint Rule   : 0.3325
Left Rectangles : 0.285
Simpsons Rule   : 0.3333333333
Trapezoid Rule  : 0.335
\$\endgroup\$
1
\$\begingroup\$

Looks well organized, understandable, and suited for the domain. You might want to take care with naming to improve readability. For example, name the class Function to be MathFunction to prevent confusion of purpose. Also single-letter variable names should normally be avoided, particularly lower-case "L", except for obvious increment counters.

For more on Python naming style see the PEP guides.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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