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
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Aug 6, 2013 at 5:18
\$\endgroup\$

1 Answer 1

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.

answered Aug 7, 2013 at 2:25
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.