Python's module system

Python's module system is something we have been using throughout the book. It is what lies behind the import statements.

All that we need to do in order to create a module is make a Python file. It really is that simple. Let's take a small example. Create a new folder to hold our example and add a short simple file:

# mymod.py
myvariable = 15

def do_a_thing():
print('mymod is doing something')

def do_another_thing():
print('mymod is doing something else, and myvariable is', myvariable)

This may look the same as any normal Python file, but we can treat this as a reusable module if we want to. To demonstrate, open up a terminal window, change into the directory you have just created with this file in, and then run the Python REPL:

>>> import mymod
>>> mymod.do_a
mymod.do_a_thing( mymod.do_another_thing(
>>> mymod.do_a_thing()
mymod is doing something
>>> mymod.do_another_thing()
mymod is doing something else, and myvariable is 15

Since we are in the directory in which the mymod.py file is stored, we are able to import it in the same way as anything from the standard library. This is due to how importing works.

When you add import mymod to a Python file, you are telling it to look for a file or package with the name mymod. Python won't scan the whole computer though, only the places defined in your Python install's default path, as well as in a special os variable called PYTHONPATH. These variables simply tell the Python interpreter which folders to check for the imported files or packages.

To check out our path and PYTHONPATH, we can go back to our REPL and do this:

>>> import sys
>>> import os
>>> sys.path
['', '/usr/lib/python36.zip', '/usr/lib64/python3.6', '/usr/lib64/python3.6/lib-dynload', '/usr/lib64/python3.6/site-packages', '/usr/lib64/python3.6/_import_failed', '/usr/lib/python3.6/site-packages']
>>> os.environ['PYTHONPATH']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib64/python3.6/os.py", line 669, in __getitem__
raise KeyError(key) from None
KeyError: 'PYTHONPATH'

From the previous code, we can note that my machine does not have a PYTHONPATH environment variable configured, so all modules will be located from the default path found in sys.path.

Speaking of which, the locations listed in sys.path are all displayed, the first of which is an empty string, signaling that the first place that will be searched is the current directory that the file is being executed from. This is why the interpreter was able to find my mymod.py file even though we had just created it in a normal folder.

Once a module has been imported, its classes, functions, and variables are all accessible to the importer (unless specifically protected). Depending on the type of import performed, they will be accessed differently.

To better demonstrate this point, we'll need to change myvariable to a list:

myvariable = ['important', 'list']

Now let's have a look at importing mymod the regular way:

>>> import mymod
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'mymod']
>>> dir(mymod)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'do_a_thing', 'do_another_thing', 'myvariable']
>>> mymod.myvariable
['important', 'list']
>>> mymod.do_another_thing()
mymod is doing something else, and myvariable is ['important', 'list']
>>> myvariable = ['silly', 'list']
>>> mymod.myvariable
['important', 'list']
>>> mymod.do_another_thing()
mymod is doing something else, and myvariable is ['important', 'list']

When importing with a plain import mymod statement, we are able to inspect the module via Python's dir function. This lists all of the available attributes, methods, and variables within the module.

We also use dir to inspect the global namespace to see that we do not have direct access to anything inside mymod.  To use its features, we must prepend mymod. to them.

You will notice that the variable myvariable is accessed as mymod.myvariable, meaning we are free to define another myvariable without affecting the one used by our mymod module. Even when we redefine myvariable to ['silly', 'list'], our mymod module will still have access to the original value of ['important', 'list'].

Let's try the other import type using from and *:

>>> from mymod import *
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'do_a_thing', 'do_another_thing', 'myvariable']
>>> myvariable
['important', 'list']
>>> myvariable.append('junk')
>>> do_another_thing()
mymod is doing something else, and myvariable is ['important', 'list', 'junk']
>>> myvariable.remove('important')
>>> do_another_thing()
mymod is doing something else, and myvariable is ['list', 'junk']

When using a wildcard import, all features are added to the global namespace. Whereas before we had to use mymod. to access parts of it, they are now available without that prefix.

While this offers a bit of brevity, the trade-off is the ability to accidentally overwrite parts of a module without realizing it. In this example, we were able to access myvariable from the global namespace and change it. In doing this, we have also changed the result produced by the do_another_thing function. Needless to say, this could cause some unwanted effects and difficult-to-trace bugs.

While in the first example we still could have changed the value of myvariable and affected the result of do_another_thing, the need to place the modulus's name directly in front of it acts as a safeguard against accidental modification. This means the regular import statement is just as capable as the wildcard, but safer to do. This is why the plain import is considered preferable to a wildcard.

If we find ourselves with many different modules that belong to an overall group, we can make them easier to import by combining them into a package.

A package is defined by a folder containing a file named __init__.py. This __init__.py file is run when the package is imported and anything within its namespace becomes available to the importer.

To try creating a package, let's make a new folder called counter and place three files inside it:

# counter/countdown.py
def count_down(max):
numbers = [i for i in range(max)]
for num in numbers[::-1]:
print(num, end=', ')

This module provides a function that will simply count down from the specified number.

If you are not familiar with the syntax, [::-1] is used to reverse an iterable:

# counter/countup.py
def count_up(max):
for i in range(max):
print(i, end=', ')

This module gives us a function that will count up from the given number.

The final file to make is __init__.py, which tells the Python interpreter that the folder containing this file is a package that is importable. In our case, this file can be completely blank, it just needs to exist.

Now we have created a package called counter. If we launch the REPL from the folder containing our counter folder (not the counter folder itself), we are able to make use of the counter package:

>>> from counter import countdown
>>> countdown.count_down(10)
9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
>>> from counter.countup import count_up
>>> count_up(10)
0, 1, 2, 3, 4, 5, 6, 7, 8, 9,
>>> import counter
>>> counter.countdown.count_down(3)
2, 1, 0,

From the preceding example it is clear that we can use the functions provided by our modules in three ways.

Firstly, we can use a from statement to import just the module we require from the countdown package. This gives us access to the functions provided by that module, but this requires the module's name as a prefix much like with our first mymod example. We demonstrate this by executing the count_down function.

Secondly, the function can be brought into the global namespace by importing it directly from the package and the module. We access a module within a package by putting a dot between them, as we have with counter.countup. This allows us to use the count_up function without the need to prepend the countup module's name, but does not expose any underlying variables that may be used by the function. We again demonstrate this by executing the function.

Finally, we can import the counter package, as a whole. This provides the most protection against accidentally changing parts of a program used by modules, but requires the most amount of typing in order to access functions and variables exposed by the package's modules.

After importing the whole counter package we need to prepend the package name first, followed by the module name, and finally the function name. This is a lot more verbose, but ensures the programmer knows exactly which function is being run.

That's all there is to grouping modules into packages. We will be using packages to split up some aspects of our blackjack game so that we do not get distracted by one very large file as we add more and more depth to the game.

We will also then have created a package that allows us to quickly emulate another casino card game in the future without re-writing any of the underlying structural pieces.

Let's begin creating the packages needed for the new and improved blackjack game.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset