Functional programming, which we explored in the previous chapter, is one of the knottiest topics you’ll encounter in the programming world. I’m happy to tell you that this chapter, about Python’s modules, will provide a stark contrast, and will be one of the easiest in this book. Modules are important, but they’re also very straightforward to create and use. So if you find yourself reading this chapter and thinking, “Hey, that’s pretty obvious,” well, that’s just fine.
What are modules in Python, and how do they help us? I’ve already mentioned the acronym DRY, short for “Don’t repeat yourself,” several times in this book. As programmers, we aim to “DRY up” our code by taking identical sections of code and using them multiple times. Doing so makes it easier to understand, manage, and maintain our code. We can also more easily test such code.
When we have repeated code in a single program, we can DRY it up by writing a function and then calling that function repeatedly. But what if we have repeated code that’s used across multiple programs? We can then create a library--or, as it’s known in the world of Python, a module.
Modules actually accomplish two things in Python. First, they make it possible for us to reuse code across programs, helping us to improve the reusability and maintainability of our code. In this way, we can define functions and classes once, stick them into a module, and reuse them any number of times. This not only reduces the amount of work we need to do when implementing a new system, but also reduces our cognitive load, since we don’t have to worry about the implementation details.
For example, let’s say that your company has come up with a special pricing formula that combines the weather with stock-market indexes. You’ll want to use that pricing formula in many parts of your code. Rather than repeating the code, you could define the function once, put it into a module, and then use that module everywhere in your program that you want to calculate and display prices.
You can define any Python object--from simple data structures to functions to classes--in a module. The main question is whether you want it to be shared across multiple programs, now or in the future.
Second, modules are Python’s way of creating namespaces. If two people are collaborating on a software project, you don’t want to have to worry about collisions between their chosen variable and function names, right? Each file--that is, module--has its own namespace, ensuring that there can’t be conflicts between them.
Python comes with a large number of modules, and even the smallest nontrivial Python program will use import
(http://mng.bz/xWme), to use one or more of them. In addition to the standard library, as it’s known, Python programmers can take advantage of a large number of modules available on the Python Package Index (https://pypi.org). In this chapter, we’ll explore the use and creation of modules, including packages.
Hint If you visit PyPI at https://pypi.org, you’ll discover that the number of community-contributed, third-party packages is astonishingly large. Just as of this writing, there are more than 200,000 packages on PyPI, many of which are buggy or unmaintained. How can you know which of these packages is worthwhile and which isn’t? The site “Awesome Python,” at http://mng.bz/ AA0K, is an attempt to remedy this situation, with edited lists of known stable, maintained packages on a variety of topics. This is a good first place to check for packages before going to PyPI. Although it doesn’t guarantee that the package you use will be excellent, it certainly improves the chances of this being the case.
Re-imports an already loaded module, typically to update definitions during development |
|||
Modules allow us to concentrate on higher-level thinking and avoid digging into the implementation details of complex functionality. We can thus implement a function once, stick it into a module, and use it many times to implement algorithms that we don’t want to think about on a day-to-day basis. If you had to actually understand and wade through the calculations involved in internet security, for example, just to create a web application, you would never finish.
In this exercise, you’ll implement a somewhat complex (and whimsical) function, in a module, to implement tax policy in the Republic of Freedonia. The idea is that the tax system is so complex that the government will supply businesses with a Python module implementing the calculations for them.
Sales tax on purchases in Freedonia depends on where the purchase was made, as well as the time of the purchase. Freedonia has four provinces, each of which charges its own percentage of tax:
Yes, the taxes are quite high in Freedonia (so high, in fact, that they’re said to have a Marxist government). However, these taxes rarely apply in full. That’s because the amount of tax applied depends on the hour at which the purchase takes place. The tax percentage is always multiplied by the hour at which the purchase was made. At midnight (i.e., when the 24-hour clock is 0), there’s no sales tax. From 12 noon until 1 p.m., only 50% (12/24) of the tax applies. And from 11 p.m. until midnight, 95.8% (i.e., 23/24) of the tax applies.
Your job is to implement that Python module, freedonia.py
. It should provide a function, calculate_tax
, that takes three arguments: the amount of the purchase, the province in which the purchase took place, and the hour (an integer, from 0-24) at which it happened. The calculate_tax
function should return the final price, as a float.
calculate_tax(500, 'Harpo', 12)
a $500 purchase in Harpo province (with 50%) tax would normally be $750. However, because the purchase was done at 12 noon, the tax is only half of its maximum, or $125, for a total of $625. If the purchase were made at 9 p.m. (i.e, 21:00 on a 24-hour clock), then the tax would be 87.5% of its full rate, or 43.75%, for a total price of $718.75.
Moreover, I want you to write this solution using two separate files. The calculate
_tax
function, as well as any supporting data and functions, should reside in the file freedonia.py
, a Python module. The program that calls calculate_tax
should be in a file called use_freedonia.py
, which then uses import
to load the function.
The freedonia
module does precisely what a Python module should do. Namely, it defines data structures and functions that provide functionality to one or more other programs. By providing this layer of abstraction, it allows a programmer to focus on what’s important to them, such as the implementation of an online store, without having to worry about the nitty-gritty of particular details.
While some countries have extremely simple systems for calculating sales tax, others--such as the United States--have many overlapping jurisdictions, each of which applies its own sales tax, often at different rates and on different types of goods. Thus, while the Freedonia example is somewhat contrived, it’s not unusual to purchase or use libraries to calculate taxes.
Our module defines a dict (RATES
), in which the keys are the provinces of Freedonia, and the values are the taxation rates that should be applied there. Thus, we can find out the rate of taxation in Groucho province with RATES['Groucho']
. Or we can ask the user to enter a province name in the province
variable, and then get RATES[province]
. Either way, that will give us a floating-point number that we can use to calculate the tax.
A wrinkle in the calculation of Freedonian taxation is the fact that taxes get progressively higher as the day goes on. To make this calculation easier, I wrote a time_percentage
function, which simply takes the hour and returns it as a percentage of 24 hours.
Note In Python 2, integer division always returns an integer, even when that means throwing away the remainder. If you’re using Python 2, be sure to divide the current hour not by 24
(an int) but by 24.0
(a float).
Finally, the calculate_tax
function takes three parameters--the amount of the sale, the name of the province in which the sale took place, and the hour at which the sale happened--and returns a floating-point number indicating the actual, current tax rate.
Here’s a program that uses our freedonia
module:
from freedonia import calculate_tax tax_at_12noon = calculate_tax(100, 'Harpo', 12) tax_at_9pm = calculate_tax(100, 'Harpo', 21) print(f'You owe a total of: {tax_at_12noon}') print(f'You owe a total of: {tax_at_9pm}')
RATES = {
'Chico': 0.5,
'Groucho': 0.7,
'Harpo': 0.5,
'Zeppo': 0.4
}
def time_percentage(hour):
return hour / 24 ❶
def calculate_tax(amount, state, hour):
return amount + (amount * RATES[state] * time_percentage(hour))
print(calculate_tax(500, 'Harpo', 12))
❶ This means we’ll get 0% at midnight and just under 100% at 23:59.
You can work through a version of this code in the Python Tutor at http://mng.bz/ oP1j.
Note that the Python Tutor site doesn’t support modules, so this solution was placed in a single file, without the use of import
.
Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.
Now that you’ve written a simple function that masks more complex functionality, here are some other functions you can write as modules:
Income tax in many countries is not a flat percentage, but rather the combination of different “brackets.” So a country might not tax you on your first $1,000 of income, and then 10% on the next $10,000, and then 20% on the next $10,000, and then 50% on anything above that. Write a function that takes someone’s income and returns the amount of tax they will have to pay, totaling the percentages from various brackets.
Write a module providing a function that, given a string, returns a dict indicating how many characters provide a True
result to each of the following functions: str.isdigit
, str.isalpha
, and str.isspace
. The keys should be isdigit
, isalpha
, and isspace
.
The dict.fromkeys
method (http://mng.bz/1zrV) makes it easy to create a new dict. For example, dict.fromkeys('abc')
will create the dict {'a':None,
'b':None,
'c':None}
. You can also pass a value that will be assigned to each key, as in dict.fromkeys('abc',
5)
, resulting in the dict {'a':5,
'b':5,
'c':5}
. Implement a function that does the same thing as dict.keys
but whose second argument is a function. The value associated with the key will be the result of invoking f(key)
.
If you find yourself writing the same function multiple times across different programs or projects, you almost certainly want to turn that function into a module. In this exercise, you’re going to write a function that’s generic enough to be used in a wide variety of programs.
Specifically, write a new module called “menu” (in the file menu.py
). The module should define a function, also called menu
. The function takes any number of key-value pairs as arguments. Each value should be a callable, a fancy name for a function or class in Python.
When the function is invoked, the user is asked to enter some input. If the user enters a string that matches one of the keyword arguments, the function associated with that keyword will be invoked, and its return value will be returned to menu
’s caller. If the user enters a string that’s not one of the keyword arguments, they’ll be given an error message and asked to try again.
The idea is that you’ll be able to define several functions, and then indicate what user input will trigger each function:
from menu import menu def func_a(): return "A" def func_b(): return "B" return_value = menu(a=func_a, b=func_b) print(f'Result is {return_value}')
In this example, return_value
will contain A
if the user chooses a
, or B
if the user chooses b
. If the user enters any other string, they’re told to try again. And then we’ll print the user’s choice, just to confirm things.
The solution presented here is another example of a dispatch table, which we saw earlier in the book, in the “prefix calculator” exercise. This time, we’re using the **kwargs
parameter to create that dispatch table dynamically, rather than with a hard-coded dict.
In this case, whoever invokes the menu
function will provide the keywords--which function as menu options--and the functions that will be invoked. Note that these functions all take zero arguments, although you can imagine a scenario in which the user could provide more inputs.
We use **
here, which we previously saw in the XML-creation exercise. We could have instead received a dict as a single argument, but this seems like an easier way for us to create the dict, using Python’s built-in API for turning **kwargs
into a dict.
While I didn’t ask you to do so, my solution presents the user with a list of the valid menu items. I do this by invoking str.join
on the dict, which has the effect of creating a string from the keys, with /
characters between them. I also decided to use sorted
to present them in alphabetical order.
With this in place, we can now ask the user for input from any zero-argument function.
def menu(**options): ❶ while True: ❷ option_string = '/'.join(sorted(options)) ❸ choice = input( f'Enter an option ({option_string}): ') ❹ if choice in options: ❺ return options[choice]() ❻ print('Not a valid option') ❼ def func_a(): return "A" def func_b(): return "B" return_value = menu(a=func_a, b=func_b) print(f'Result is {return_value}')
❶ “options” is a dict populated by the keyword arguments.
❷ An infinite loop, which we’ll break out of when the user gives valid input
❸ Creates a string of sorted options, separated by “/”
❹ Asks the user to enter an option
❺ Has the user entered a key from “**options”?
❻ If so, then return the result of executing the function.
❼ Otherwise, scold the user and have them try again.
You can work through a version of this code in the Python Tutor at http://mng.bz/ nPW8.
Note that the Python Tutor site doesn’t support modules, so this solution was placed in a single file, without the use of import
.
Watch this short video walkthrough of the solution: https://livebook.manning.com/ video/python-workout.
Now that you’ve written and used two different Python modules, let’s go beyond that and experiment with some more advanced techniques and problems:
Write a version of menu.py
that can be imported (as in the exercise), but that when you invoke the file as a stand-alone program from the command line, tests the function. If you aren’t familiar with testing software such as pytest
, you can just run the program and check the output.
Turn menu.py
into a Python package and upload it to PyPI. (I suggest using your name or initials, followed by “menu,” to avoid name collisions.) See the sidebar on the difference between modules and packages, and how you can participate in the PyPI ecosystem with your own open-source projects.
Define a module stuff
with three variables--a
, b
, and c
--and two functions--foo
and bar
. Define __all__
such that from
stuff
import
*
will cause a
, c
, and bar
to be imported, but not b
and foo
.
Modules and packages are easy to write and use, and help us to DRY up our code--making it shorter and more easily maintainable. This benefit is even greater when you take advantage of the many modules and packages in the Python standard library, and on PyPI. It’s thus no wonder that so many Python programs begin with several lines of import
statements. As you become more fluent in Python, your familiarity with third-party modules will grow, allowing you to take even greater advantage of them in your code.