Chapter 13: Modules

With the introduction of functions, a new world of additional resources has been opened up. Python has a large number of libraries, referred to as modules, that can be imported and used by your programs. They are similar to the built-in functions, but large sets of libraries have specific purposes. This chapter will discuss a few of the available modules and will explain how to get them into your program and how to take advantage of them.

Importing modules

You've seen a number of what we've referred to as built-in functions already. These functions are a core part of the Python language. Using the len, max, or print functions is a trivial task; just type in the keyword along with the required parameters, and the correct values are returned. Even mathematical operators, such as the addition and multiplication symbols, are available right out of the box.

One of the mathematical functions that we've missed is the square root. This is a fairly common operation in math, but certainly not one that is as common as adding or multiplying. As it turns out, Python does offer a way to access the square root function. To get there, we'll need to import a module called math.

The math module is a collection of mathematical functions that are less common than the core functions, but still very likely to be required in your programs at some point. To access them, we use an import statement and specify the name of the module.

import math

It's as easy as that. Now, we've got access to the full suite of functions inside the math module. When calling a function inside of a module, you need to specify the module itself where the function exists in addition to the function name. It's not quite like writing a function and just calling getNumber. Modules define a namespace, and methods like sqrt (the square root function that we're about to encounter) exist inside that namespace. The namespace is referenced like this:

>>> import math
>>> math.sqrt(4)
2.0

This function call looks almost exactly like the other function calls except for the fact that a module is specified with the function. With the module imported, methods in math can be used just like the other functions. Let's modify the division program from earlier so that the square root function is used instead.

import math

def getNumber(minimum_value=False, input_text="Enter a number: "):
    done = False
    while not done:
        try:
            user_number = int(input(input_text))
        except ValueError:
            print("You didn't enter a number! Shame on you.")
            continue
        if minimum_value == False or user_number >= minimum_value:
            done = True
        else:
            print("Your number was too low!")
            print("The minimum value is {0}.".format(minimum_value))
    return user_number

user_number = getNumber(minimum_value=0, input_text="Enter a number to obtain the square root: ")
print("The square root of {0} is {1}.".format(user_number, math.sqrt(user_number)))

Enter a number to obtain the square root: 25
The square root of 25 is 5.0.

A call to math.sqrt requires a single numeric value as input, and a float number equal to the square root of the source parameter is returned. The math module has a whole set of useful functions, serving as an ideal example of a situation where you'd benefit from offloading a suite of related functions into a single location.

Included in the math functions are some helpful rounding operations. The round function is available as a built-in function, so we don't need to import math to use it. Two additional functions called ceil (short for ceiling) and floor can be used to round-up or round-down.

import math

x = 2.4
print("x = {0}".format(x))
print("Round: {0}".format(round(x)))
print("Ceiling: {0}".format(math.ceil(x)))
print("Floor: {0}".format(math.floor(x)))

x = 2.4
Round: 2
Ceiling: 3
Floor: 2

Another useful module called random contains several random number generators, along with other related functions including ways to randomise sequences like lists.

Let's say, for example, that you wanted to build a dice-rolling program.

import random

for i in range(10):
    print("Rolling a six sided die: {0}".format(random.randint(1, 6)))

Rolling a six sided die: 3
Rolling a six sided die: 1
Rolling a six sided die: 4
Rolling a six sided die: 5
Rolling a six sided die: 3
Rolling a six sided die: 2
Rolling a six sided die: 1
Rolling a six sided die: 6
Rolling a six sided die: 4
Rolling a six sided die: 3

The random.randint function takes two numbers and returns a random number inside the inclusive range defined by the input. What that means is that in the example above, we're asking for an integer between 1 and 6.

You can also use the random module to shuffle lists of data in place, similar to sorting or reversing a list. Let's use random to build a list of randomly-generated integers, to sort and shuffle the list, and then to return an element at random.

import random

my_list = []
for i in range(10):
    my_list.append(random.randint(1, 100))
print(my_list)

my_list.sort()
print(my_list)

random.shuffle(my_list)
print(my_list)

print(random.choice(my_list))

[52, 39, 54, 62, 59, 95, 98, 10, 45, 28]
[10, 28, 39, 45, 52, 54, 59, 62, 95, 98]
[54, 10, 39, 98, 95, 59, 52, 62, 45, 28]
39

An an interesting aside, "random" numbers, are actually "pseudo-random". These numbers only appear random. A random number generator has a seed value that acts as a way of determining the starting random number, along with all successive random values after that. If you start with the same seed every time, you actually get the same set of random numbers. Generally, the computer will use its clock time as the starting seed, so unless you have the ability to travel back in time to the same exact moment that your code was executed the first time, you're extremely unlikely to have the same random values showing up. You can play with this quirk by setting the random seed manually and generating lists of random values.

import random

print("Random lists with a random seed:")
for x in range(5):
    my_list = []
    for i in range(10):
        my_list.append(random.randint(1, 100))
    print(my_list)

print("Random lists with a non-random seed:")
for x in range(5):
    my_list = []
    random.seed(0)
    for i in range(10):
        my_list.append(random.randint(1, 100))
    print(my_list)

Random lists with a random seed:
[87, 10, 92, 61, 42, 3, 21, 32, 85, 11]
[63, 96, 27, 95, 54, 97, 94, 77, 3, 85]
[93, 13, 7, 58, 69, 3, 30, 48, 40, 30]
[47, 79, 13, 74, 14, 95, 41, 8, 60, 66]
[90, 18, 41, 72, 8, 77, 24, 45, 6, 85]
Random lists with a non-random seed:
[85, 76, 43, 26, 52, 41, 79, 31, 48, 59]
[85, 76, 43, 26, 52, 41, 79, 31, 48, 59]
[85, 76, 43, 26, 52, 41, 79, 31, 48, 59]
[85, 76, 43, 26, 52, 41, 79, 31, 48, 59]
[85, 76, 43, 26, 52, 41, 79, 31, 48, 59]

Wacky, eh?

Writing your own modules

We'll talk more about additional modules as we move forward. For now, let's look at how you can write your own modules to save time by not copying and pasting the useful functions you develop. Naturally, modules can be defined by a programmer. If you've got a few input function helpers to get numbers or other values, you can set up your own importable module that will make it easier to write the code you want to write.

To begin, let's create a new file in IDLE. Save it with the name getinput.py, and don't forget the .py extension! This new file will become the module to import. You can think of the import statement as a way of asking Python to include an existing file right into your code at the point where you include the import statement. Any functions that you define in getinput.py will then become accessible to your source code that imports getinput in the future. Let's start with the previous getNumber function. Copy and paste getNumber into the new getinput.py source file, and save it.

Now go back to your other source code file, and at the very top of the file, write the following lines:

import getinput

getinput.getNumber()

You don't need the .py extension in import; Python is able to infer that an import is asking for a file that ends in .py, so you can just omit the extension. Now try to run your file. If all goes well, you should see the following:

Enter a number:

If you see that, congratulations! You've written a module! You no longer have to copy and paste getNumber in each of your future source files. You can just import the getinput module, a Python source file that you've written yourself and placed in the same directory as your source file, and you've got access to the right functions. Remember, you need the getinput prefix for the getNumber function, and the module has to be in the same directory as your current working file so that Python can find it.

Let's add some more code to getinput. How about a function that asks the user for a minimum and a maximum value, and returns a random number in that range using the random module. In getinput.py, add the following line to the top of the file.

import random

And at the bottom of the file, add the following new function.

def getRandomRangeNumber():
    min_number = getNumber(input_text="Enter the minimum value: ")
    max_number = getNumber(minimum_value=min_number, input_text="Enter the maximum value: ")
    return random.randint(min_number, max_number)

This is a new function that asks the user for a minimum value and a maximum value that must be at least as big as the minimum they just entered. Using this range, it queries random.randint for a random number, and returns it.

Why doesn't the new getRandomRangeNumber need to prefix the calls to getNumber with the getinput namespace? It's because getRandomRangeNumber is in the same namespace; all the functions in the same namespace know about each other, so if you're calling one from the other, you don't need to specify that we're in the current file.

Now let's modify the original source that uses getInput so that it calls the new method we just wrote.

import getinput
print(getinput.getRandomRangeNumber())

Enter the minimum value: 5
Enter the maximum value: 25
18

If you want to add in additional methods to your module, just place them in getinput.py and they'll be accessible to other Python source files that import the module. Awfully convenient, isn't it?

Importing subsets of modules

In some rare instances, you might want to only import a small section of the module. It might be the case that you feel you'll only need the sqrt function from math, or that you know that a module is itself broken down into further modules -- modules can actually be nested inside one-another, and we'll see an example of this.

One module that you might get a lot of use from is datetime. The datetime collection of functions and objects gives you a way to collect and manipulate dates and times. It gives you a set of methods for formatting date strings for printing to the screen. It also allows you to perform arithmetic on dates, as you might do when asking for the date two days from now. The now function gives you the current date and time.

import datetime
print(datetime.datetime.now())

2010-11-24 16:17:58.521000

It looks a little unusual when datetime is entered twice like that. This is an artifact of the datetime module and how it's been structured in the original source. It turns out that there's a datetime object inside the datetime module that is responsible for getting values like the current date. The reason for this is that the datetime module does something analogous to defining datetime as a type. It sets up an object called datetime, and that new datetime object is assigned a value corresponding to the current time.

What? This might be a little confusing, and that's completely fair. The next chapter will talk about classes, and will open up some more of these details. For the moment, consider that importing the datetime module defines the datetime namespace (the first datetime), and inside that namespace is a datetime object (the second datetime) upon which the now function is called. Confused yet?

For this very reason, we can use a slightly different form of the import statement. We can ask Python to import the datetime object (not the module) directly into the current namespace, instead of into the datetime namespace.

from datetime import datetime
print(datetime.now())

2010-11-24 16:17:58.521000

As a simplification of this, we can also write the following code:

from math import sqrt
sqrt(4)

2.0

The from statement used in conjunction with import asks Python to import a subset of the module instead of the whole thing. In the case of the math module, we ask Python to just bring in the sqrt function, but not the whole module.

Summary

Modules offer a way of organizing large sets of code in the same way that functions allow you to place related lines of code in a single location. They save you the hassle of copying and pasting the functions you use most frequently by tucking them away behind an import statement, ready to be used at your demand. If you don't need to use them, they're still safely organized in your source code directory.

These abstractions move programming from a hacky collection of random lines of code towards a well-structured piece of engineering. In fact, when we refer to software engineering as a practice, it is with the intent of approaching the rigour that professional engineers in the physical sciences apply to their work. When building a bridge, the designers go to great lengths to ensure that everything makes sense, to prevent the bridge from collapsing. It might seem strange to think about software in the same way at first, but when you consider that software is now found in cars, airplanes, and hospital equipment, the demand on software developers to write well-structured and maintainable code is greater than ever. Functions and modules are a step in this direction, and a step away from the chaotic development practices that many developers fall in to.

With practice, you'll figure out the best ways to structure your code, giving you an ideal amount of reuse and a minimal amount of rewriting. It's always really nice to find yourself in need of a function, and then realizing that you're already written it and can import it with a single line of code.

Exercises

1) Go back and look at some of the code that you're written previously when working through this book. Find some examples that you might like to use in other programs that you write, and extract them out to a module. Save the file in your working directory, and write some test programs to import the module and use the new functions.