Using basic builtin libraries

Like any language, Python has a mechanism for importing collections of functions/classes/constants into your own code for use. In Python these are called "modules" (or "packages", which are collections of modules). There are builtin modules, which are modules that are available in any interpreter/environment, as opposed to modules that need to be copied into the environment. Modules become available for use in your code via the import statement.

One of the most basic builtin libraries is math, which defines things like sqrt, floor, etc. We will use it to demonstrate an important aspect of modules: namespaces! A namespace is the collection of global name/object bindings. When you have (outside of any function/class body) code like:

def foo(x):
    return x*x
... two things happen. First, a new function with the fascinating ability to square things is created. Second, that function is bound to the name foo in the current namespace.

When you import a module as "import foobar", the only new name introduced into the current namespace is foobar. All of the named objects provided by the module, like sqrt with import math, are only accessible as "modulename-dot-objectname". So, for example, after import math the name sqrt on its own does not exist in the current namespace. You must write math.sqrt.

>>> import math
>>> math.factorial(5)
120
>>> math.sqrt(42)
6.48074069840786
>>> math.pi
3.141592653589793
You can choose specific name/object bindings from the module to pull into the current namespace with: from modulename import objectname. Note: you can actually give a comma-separated list of names if you want more than one, or from modulename import * if you want them all.
>>> from math import sqrt,factorial
>>> factorial(5)
120
>>> sqrt(42)
6.48074069840786
>>> pi
Traceback (most recent call last):
  File "<stdin>", line 1, in 
NameError: name 'pi' is not defined

Aim for code reuse: if __name__ == '__main__'

Any .py file we create is a module - it can be imported into other code just like the builtin modules. The name is simply the file name without the .py at the end. So everything you write is an opportunity for future code reuse! Consider the following example, in which the important point is that file ex1a.py defines function quadraticFormula that file ex1b.py wants to use. See how we use "import" to do that?

But there's a problem here! What if some other code wants to use the beautiful (if I do say so myself) roots function? If they do import ex1b to get access to the roots function they will get all that output about golden ratios that, presumably, they don't want!
>>> import ex1b
golden ratio r = 1.618033988749895 is a root of x^2 - x - 1.
check: r^2 - r - 1 = 0.0
>>> ex1b.roots("2 x^2 - 5 x + 1") # I wanted this, not that golden business!
[0.21922359359558485, 2.2807764064044154]
So here is the crux: We want ex1b.py to compute the "golden" stuff when we run it like "python3 ex1b.py", but not when we import it to use with import ex1b. Here's how we do this: the code with the "golden" business goes in an if-statement of the form: if __name__ == '__main__':

You can just think of this as boilerplate. The idea is this: if the file ex1bMOD.py is the main thing the interpreter is running, its __name__ is "__main__". Otherwise, its __name__ is its module name: ex1bMOD.

Note: when you create a file full of class and function definitions that you intend to use elsewhere, you can still make a __main__ for testing code.

Your job

Create a copy of ex1a.py. Use it but don't modify it! The rules are, you use it without modification, and no code of your own reimplements the quandratic formula or discriminants or anything like that.

Create a file rroots.py that can be run as a standalone script (as described below) or imported and used as part of another project (as described below):

as a module to be imported as a standalone script
When run as a module to be imported, rroots.py provides a function getRealRoots(a,b,c) that returns a list of the zero, one or two real roots of $ax^2+bx+c$.
>>> from rroots import getRealRoots
>>> getRealRoots(1,2,0.5)
[-1.7071067811865475, -0.2928932188134524]
>>> getRealRoots(1,2,1)
[-1.0]
>>> getRealRoots(1,2,1.5)
[]
Remember the rules. You cannot reimplement the quadratic formula or the discriminant. You must use ex1a.py for that (which you also cannot change!)

Because floating-point computation is inexact, two roots that come back from the quadratic formula as different numbers, but are close enough (as determined by some threshold) will be considered to be one and the same root. By default that threshold should be 0.0000001.

>>> from rroots import getRealRoots
>>> getRealRoots(.9999,2,1.0001)
[-1.0002000200022516, -0.9999999999997488]
>>> getRealRoots(.999999999,2,1.000000001)
[-1.000000001]
Hint: You might want to import math to use math.fabs( ).

Challenge: Can you add a setThreshold(·) function to to rroots.py that allows a user to change the threshold value that gets used in further computations? There is a gotcha to this!

When run as a script, rroots.py expects a single command-line argument of the form a,b,c and returns a list of the zero, one or two real roots of $ax^2+bx+c$. Of course, if the user runs the command without the argument a,b,c, a helpful usage message should be printed out.
$ python3 rroots.py 2,-4,1
[0.2928932188134524, 1.7071067811865475]
$ python3 rroots.py 2,-4,2
[1.0]
$ python3 rroots.py 2,-4,2.5
[]
$ python3 rroots.py
usage: python3 rroots.py <a>,<b>,<c> 

Hint1: If you import the sys module, sys.argv is a list of the command line arguments. Note that sys.argv[0] is the command itself, so sys.argv[1] is the first command line argument ... if one exists!

Hint2: The method split( ) of class string is really helpful. You give it a string to use as a delimeter, and it breaks the string up into an array of the substrings separated by that delimeter.

>>> foo = "red:yellow:green:blue"
>>> foo.split(":")
['red', 'yellow', 'green', 'blue']

Note: There are times when you have an array of values that you want to use as individual arguments in a function call. For example, if you have

C = [1,-1,-1]
... and you want to call getRealRoots using the elements of C as your a,b,c, it's a drag to write:
getRealRoots(C[0],C[1],C[2])
There's actually a nifty language feature that takes care of this for you, the * "unpacking" operator.
>>> from rroots import getRealRoots
>>> C = [1,-1,-1]
>>> getRealRoots(*C)
[-0.6180339887498949, 1.618033988749895]
>>> getRealRoots(C[0],C[1],C[2])
[-0.6180339887498949, 1.618033988749895]

Something more ...

random is another handy builtin library. Create a new file randpoly.py that defines a function randPolyTwoRealRoot() that generates a quadratic polynomial with two distinct real roots whose coefficients are chosen randomly in the range -99..99, but none are allowed to be zero. Once again, you can use previous code (including rroots.py), but you cannot modify it or reimplement its functionality!

Christopher W Brown