Whole modules versus module items

There are two approaches to designing the contents of a library module. Some modules are an integrated whole, while others are more like a collection of loosely related items. When we've designed a module as a whole, it will often have a few classes or functions that are the public-facing API of the module. When we've designed a module as a collection of loosely related items, each individual class or function tends to stand alone.

We often see this distinction in the way we import and use a module. We'll look at three variations:

  • Using the import some_module command: This leads to the some_module.py module being evaluated and the resulting objects are collected into a single namespace called some_module. This requires us to use qualified names for all of the objects in the module, for example, some_module.this and some_module.that. This use of qualified names makes the module an integrated whole.
  • Using the from some_module import this, that command: This leads to the some_module.py module file being evaluated and only the named objects are created in the current local namespace. We can now use this or that without the module namespace as a qualifier. This use of unqualified names is why a module can seem like a collection of disjointed objects. A common example is a statement like from math import sqrt, sin, cos to import a few math functions.
  • Using the from some_module import * command: This will import the module and make all non-private names part of the namespace performing the import. A private name begins with _, and will not be retained as one of the imported names. We can explicitly limit the number of names imported by a module by providing an __all__ list within the module. This list of string object names will be elaborated by the import * statement. We often use the __all__ variable to conceal the utility functions that are part of building the module, but not part of the API that's provided to clients of the module.

When we look back at our design for decks of cards, we could elect to keep the suits as an implementation detail that's not imported by default. If we had a cards.py module, we could include the following code:

from enum import Enum

__all__ = ["Deck", "Shoe"]

class Suit(str, Enum):
Club = "N{BLACK CLUB SUIT}"
Diamond = "N{BLACK DIAMOND SUIT}"
Heart = "N{BLACK HEART SUIT}"
Spade = "N{BLACK SPADE SUIT}"

class Card: ...
def card(rank: int, suit: Suit) -> Card: ...
class Deck: ...

class Shoe(Deck): ...

The use of the __all__ variable makes the Suit and Card names visible. The card() function, the Suit class, and the Deck class are implementation details not imported by default. For example, suppose we perform the following code:

from cards import *    

The preceding statement will only create Deck and Shoe in an application script, as those are the only explicitly given names in the __all__ variable.

When we execute the following command, it will import the module without putting any names into the global namespace:

import cards 

Even though it's not imported into the namespace, we can still access the qualified cards.card() method to create a Card instance.

There are advantages and disadvantages of each technique. A whole module requires using the module name as a qualifier; this makes the origin of an object explicit. Importing items from a module shortens their names, which can make complex programming more compact and easier to understand.

In the next section, we'll see how to design a package.

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

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