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