Week 5: Prep to Build an Inclinometer (Namespaces, Modules, Objects)#
Laboratory 4a
Last updated September 21st, 2023
00. Content #
Mathematics
N/A
Programming Skills
Namespaces
Modules
Objects
Classes
Inheritance
Embedded Systems
N/A
0. Required Hardware #
N/A
Copy the file `student_info.py` into the folder you are working in, then run this cell to put your name and email into this document.
Last week you ran a script to download student_info.py and updated it with your own information. You'll be using that same Python file with your information for all the labs going forward. Copy the student_info.py file into the directory this lab is in.# this makes it so that changes to the imported module are automatically updated in this document.
# usually not necessary, but if we are making changes, this is helpful.
%load_ext autoreload
%autoreload 2
from student_info import show_info
show_info()
Additional lab materials#
In addition to the student_info.py file, you’ll need a couple more specific Python files to complete this lab. Run the following cell to download those files to your current directory.
%%sh
wget -q -N https://raw.githubusercontent.com/TheDataScienceLabs/DSLab_Calculus/main/book/labs/4_measure_tall_things/a_prepare_for_building_inclinometer/lab4a_check.py
wget -q -N https://raw.githubusercontent.com/TheDataScienceLabs/DSLab_Calculus/main/book/labs/4_measure_tall_things/a_prepare_for_building_inclinometer/example_module.py
1. Namespaces #
We are going to start dealing with more complicated systems in the coming weeks. Next week, you will make a piece of measurement equipment, including a display, buttons, and sensor. If we kept track of everything using their own variable names, we would have to make long names, like screen_data_pin_number_1
, for example. There are a few problems with that approach:
long variable names are hard to remember
long variable names are hard to type (lots of typos can happen!)
long variable names are hard to read:
it’s hard to see if they are typed incorrectly
they break up the flow of the sentences
Of course, if we just used short variable names for everything, we would run out of names very fast. It would not be much better to replace screen_data_pin_number_1
with sdpn1
, for example.
Namespaces are one of several ways that we deal with this in Python. Namespaces are so common that you have already been using them, you just didn’t know it. Let’s look at an example using namespaces.
from types import SimpleNamespace
alden = SimpleNamespace(name="Alden", age=27, grade=22)
print(alden)
print(alden.age)
print(alden.grade)
We can see that there are three variables living inside the variable alden
, and they each have a relatively short name. We have a special name for these variables: they are called “attributes”. We would say age
is an attribute of alden
. We access the attributes using the .
which probably looks familiar to you; we have been using attributes every time we load up a package like numpy
.
import numpy
numpy.cos(numpy.pi)
Both the function cos
and the number pi
are attributes in the numpy
namespace, so we refer to them using a .
.
More advantages of namespaces#
Another way namespaces are better than just having long function names is that we can pass them around in their own variables. We could write, for example:
np = numpy
np.cos(np.pi)
That’s what Python is doing “under the hood” when we use the as
keyword during imports. When we write import numpy as np
it’s doing two steps in one, import numpy
and np = numpy
.
Here is a practical way we can use this. Say we had another instructor:
kindyl = SimpleNamespace(name="Kindyl", age=23, grade=20)
kindyl
Now we can write a function which takes in just one argument and have it work on either instructor.
def describe(instructor):
print("My instructor's name is {}. They are {} years old and in grade {}.".format(instructor.name, instructor.age, instructor.grade))
for instructor in [kindyl, alden]:
describe(instructor)
This helps us to make more flexible, modular code.
Exercise 1 (10 pts)#
The following code is a mess! Use namespaces to make it more readable.
Write Answers for Exercise 1 Below
eiffel_tower_name = "Eiffel Tower"
eiffel_tower_height = 324
eiffel_tower_weight = 7_300_000
eiffel_tower_bmi = 7_300_000/324**2
turbine_name = "GE 1.5 Megawatt Wind Turbine"
turbine_height = 100
turbine_weight = 150_000
turbine_bmi = 150_000/100**2
burj_name = "Burj Khalifa"
burj_height = 830
burj_weight = 450_000_000
burj_bmi = 450_000_000/830**2
skytree_name = "Tokyo Skytree"
skytree_height = 634
skytree_weight = 32_600_000
skytree_bmi = 32_600_000/634**2
def describe_bmi(tower_name, tower_height, tower_weight, tower_bmi):
message = ("The {} is {} meters tall and weighs {} kilograms. "
.format(tower_name, tower_height, tower_weight)
)
if tower_bmi < 18.5:
message += "Its low BMI of {:.1f} indicates that it is underweight."
elif tower_bmi < 25:
message += "Its BMI is {:.1f} which is typical for a person, but strange for a building."
elif tower_bmi < 30:
message += "Its BMI is {:.1f}, so according to the World Health organization, it's overweight."
else:
message += "Oh no! Its BMI is {:.1f}, so the World Health Organization says it's obese!"
message = message.format(tower_bmi)
print(message)
describe_bmi(turbine_name, turbine_height, turbine_weight, turbine_bmi)
describe_bmi(eiffel_tower_name, eiffel_tower_height, eiffel_tower_weight, eiffel_tower_bmi)
describe_bmi(skytree_name, skytree_height, skytree_weight, skytree_bmi)
describe_bmi(burj_name, burj_height, burj_weight, burj_bmi)
A bit of poetry#
To give you an idea of just how important namespaces are to understanding Python, read the following poem which is included in every installation of Python. Pay particular attention to the last line.
import this
2. Modules #
Namespaces in Python are much more than a trick for writing clean code (though they do help with that). As we noted above, whenever you do an import you are assigning a namespace to a variable. In fact, whenever you use any variable in Python, it is being used in a namespace. Usually the code that you write yourself lives in the so-called “global” namespace – the idea is that everything else lives inside that space. We can access our current namespace using the built-in function dir
, which is meant to be short for “directory”.
dir()
In the list above, you probably recognize many of the variable names we have already used in this notebook. There are also several things which you probably don’t recognize, most of them beginning with an _
. It is a convention in Python that if you don’t want most people to look at a variable, you start it with an _
. All of the things in the list above are variables we can access. For example, look at the variable In
.
In
We can see that this is a list showing every input we have entered so far. This variable was created for us by Jupyter. I have not found a use for it. A few of the other automatically-made variables are quite handy, though. This one in particular is used in most Python programs which people publish online (we will see why in a minute):
__name__
What’s the point of this? Just to show that every variable in Python lives in some namespace. When we import a module, all the code inside it is run right away. Then, all its variables are made available in the namespace we loaded. Open up the file example_module.py and see what’s written inside. Then, run the cell below.
import example_module
You can tell that the code in the module was run because it printed out the message above. When we use the function dir
on the module, we see what is in its namespace.
dir(example_module)
Some things will look familiar, like numbers
:
example_module.numbers
and the function share_favorites
:
example_module.share_favorites()
Others were put in there for us automatically. We can see that it has its own __name__
:
example_module.__name__
And the variable __doc__
holds some potentially-useful information:
example_module.__doc__
Since that’s a string, and it lives in the variable __doc__
, it’s called a “docstring”. If you put a string as the first line of any module, or of any function, it gets stored as a docstring. Usually the docstring will contain helpful information about how to use a module or function.
Notice that every variable in example_module
is in that namespace, including the modules which were imported inside it. This is great, because it means you can call a module whatever you want when you import it (like when using an abbreviation), and it won’t affect other parts of a big program. We can confirm that we are really getting the same thing either way:
example_module.my_favorite_module is np
Here we are using the special keyword is
, which checks whether two variables point to the exact same thing.
One mystery still remains: what’s that block at the bottom of the module, inside the if
statement? We saw that when importing the module, the variable __name__
had the value "example_module"
, not the value "__main__"
. Surely it would never run. So why include it at all?
Exercise 2 (5 pts)#
To see why, open up a terminal inside Jupyter and navigate to this directory. Then, type the command python3 example_module.py
. Paste the output below to demonstrate that you have done this. (Hint: you can type cd my_folder/
to navigate into the folder named “my_folder”, and ls
to list everything in your current folder)
Write Answers for Exercise 2 Below
As you have seen, when we ran the code on its own (as the only program, not as a module in a larger program) its __name__
was "__main__"
. That meant it ran the block at the bottom of the module. You will see this all the time when reading other people’s code. People want to be able to import useful functions from other projects without running them, and people want to be able to run their project as its own program. By taking advantage of the variable __name__
, we get the best of both worlds.
Exercise 3 (20 pts)#
Now that you have seen some of how importing modules works, you should write your own module. It should conform to the following specifications:
name it
my_module.py
Have a docstring which describes what your module does
Include a variable
INCH
with the value2.54
.Include a function called
chebyshev_five
which takes one argumentx
and returns \(16x^{5}-20x^{3}+5x\).Make it so that when you run the module on its own it prints your name, but if imported does not print anything.
Once you have written your module, run the following cells to demonstrate this.
Write Answers for Exercise 3 Below
# these two lines are instructions to python that it should reload your module if you make changes.
%load_ext autoreload
%autoreload 2
import lab4a_check
import my_module
lab4a_check.exercise_3(my_module)
%%bash
# this is another way you can run a program on its own.
python3 my_module.py
Exercise 4 (20 pts)#
Now you know how to write a module. Why would you want to write a module? Come up with at least five reasons you might want to put code into a module and import it, rather than copying and pasting the code into a document to run it. Each reason should be in the form of 2-4 sentences.
Write Answers for Exercise 4 Below
Exercise 5 (20 pts)#
Make a module called datalab.py
and fill it with useful functions we have written, such as:
the
show_info
function you wrote in lab 3the functions you wrote in lab 3 which compute properties of a heartbeat
In this document, demonstrate loading the module and using the functions inside of it. You will need to come up with examples which demonstrate what each function does clearly. Then run the following cell which will print the contents of your file in this document to be graded. You will be graded on the following scale:
Category |
5 points |
3 points |
1 point |
---|---|---|---|
Documentation |
There is a docstring at the top, describing what is in the file. Every function has its own docstring as well. |
One or more docstring is missing. |
No docstrings were provided. |
Style |
Code is presented in a consistent style with appropriate uses of spacing and parentheses. |
Code is mostly well-formatted, but there are a few parts which are hard to read. |
It is hard to tell what the code does. |
Examples |
The examples demonstrate what the purpose of each function is, as well as how the function should be used. |
One or more examples are missing, or do not demonstrate what the function is good for. |
Several examples are poorly-chosen or missing. |
Write Answers for Exercise 5 Below
import inspect
import datalab
print(inspect.getsource(datalab))
3. Objects #
To review, we have introduced:
the concept of a namespace.
how Python groups code into namespaces when loading modules.
Most people who code in Python don’t think about namespaces, at least not by that name (although they should!). Most people don’t use the module types.Simplenamespace
to write their code. Most Python programmers instead think about “objects”. Why? Because everything in Python is some kind of object.
What is an “object”? An object is a namespace. Python is considered an “object-oriented” programming language because everything in Python is an object. Even things which you are familiar with, like integers, lists, and strings, are objects. We can inspect their attributes using the dir
function:
dir(1)
dir([1, 2, 3])
dir("hello")
Many of the attributes of these objects are functions. When an object’s attribute is a function, we call that function a method of the object. They usually have something to do with the object they are attached to. For example, the method upper
of the string hello
is a function which returns that string in upper case.
'hello'.upper()
Using object methods#
As you can see above, there are lots of attributes for even a simple object. Most of them start with __
, which is a convention which means that most programmers won’t have to use that attribute – it’s handled by Python internally. Since these are methods which start with a double underscore, these are called “dunder methods”. As an example, look at the method __len__
.
'hello'.__len__()
This does exactly the same thing as
len('hello')
In fact, that’s the first thing the function len
tries when you call it: the function checks if the object you are calling it on has a method named __len__
. The actual source code for len
is written in C so that it runs faster, but if we wrote it in Python it would look something like this.
def len(x):
if '__len__' in dir(x):
return x.__len__()
else:
# find the length another way
So you see, you almost never need to use the double-underscore methods. When we filter them out, things start to feel a lot more tractable:
for attribute in dir([1]):
if not attribute.startswith('__'):
print(attribute)
You can see there are not that many methods which you actually will be using. If you want to know how to use one of the methods, you can ask Python for help:
help([1].count)
Exercise 6 (10 pts)#
Pick one of the methods of "hello"
and use the built in help
function to figure out how to use it. Demonstrate using the method as well.
Write Answers for Exercise 6 Below
As you use Python more, you will become used to the methods of common types of objects. All strings, for example, have the same methods. If you want to know an object’s type, you can use the built in function type
,
type('hello'), type(1), type(1.0), type([])
Writing our own classes#
Most of the time it’s easiest to use objects that are already built in to Python. However, it is frequently helpful to be able to make up new types of objects. The type of an object is also called its class. Python gives us a short, clear way to come up with new classes. Let’s revisit our buildings from before. Instead of making them simple namespaces, let’s define a new class. Here is the way we would write that.
class Building:
def __init__(self, name, height, weight):
self.name = name
self.height = height
self.weight = weight
self.bmi = weight/height**2
eiffel = Building("Eiffel Tower", 324, 7_300_000)
print(eiffel)
print(eiffel.name)
We used the keyword class
to declare a new kind of object. Everything inside the class
block lives inside the Building
namespace. We defined a new function __init__
, but it’s not available outside the class:
Building.__init__
The name __init__
is a special name which is used to make objects of the Building
type.
type(eiffel)
You can see that the type of eiffel
is __main__.Building
, because it was created from the class Building
which lives in the __main__
namespace. Since the type of eiffel
is Building
, we would say that eiffel
is an instance of Building
.
We can already see one benefit of making our own class: we can specify only the information needed to come up with it. We didn’t need to compute the bmi
attribute ourselves; the function __init__
handled it. This helps to ensure that bmi
is computed the same way every time, and it saves us from having to remember the formula whenever we want to declare a new Building
.
Let’s get a bit more fancy. Instead of defining a function in the global namespace to describe a building, let’s make a method for the Building class which does the same description as before.
class Building:
def __init__(self, name, height, weight):
self.name = name
self.height = height
self.weight = weight
self.bmi = weight/height**2
def describe(self):
message = ("The {} is {} meters tall and weighs {} kilograms. "
.format(self.name, self.height, self.weight)
)
if self.bmi < 18.5:
message += "Its low BMI of {:.1f} indicates that it is underweight."
elif self.bmi < 25:
message += "Its BMI is {:.1f} which is typical for a person, but strange for a building."
elif self.bmi < 30:
message += "Its BMI is {:.1f}, so according to the World Health organization, it's overweight."
else:
message += "Oh no! Its BMI is {:.1f}, so the World Health Organization says it's obese!"
message = message.format(self.bmi)
print(message)
turbine = Building("GE 1.5 Megawatt Wind Turbine", 100, 150_000)
turbine.describe()
We should comment on the special word self
; actually it’s not special at all, it’s just the position of it that’s special. It’s traditional to use the parameter name self
there. The first parameter of any method in Python refers to the object that called it. In our example, we called turbine.describe()
. We could have just as easily done it like this:
Building.describe(turbine)
So you see, the class methods will always have a parameter self
which Python automatically fills in with the instance of the class.
There is one more thing I would like to change about our class: it doesn’t look good when printed.
print(turbine)
By telling Python how to make it into a string, we will get a much nicer print message.
class Building:
def __init__(self, name, height, weight):
self.name = name
self.height = height
self.weight = weight
self.bmi = weight/height**2
def describe(self):
message = ("The {} is {} meters tall and weighs {} kilograms. "
.format(self.name, self.height, self.weight)
)
if self.bmi < 18.5:
message += "Its low BMI of {:.1f} indicates that it is underweight."
elif self.bmi < 25:
message += "Its BMI is {:.1f} which is typical for a person, but strange for a building."
elif self.bmi < 30:
message += "Its BMI is {:.1f}, so according to the World Health organization, it's overweight."
else:
message += "Oh no! Its BMI is {:.1f}, so the World Health Organization says it's obese!"
message = message.format(self.bmi)
print(message)
def __str__(self):
return 'Building("{}", {}, {})'.format(self.name, self.height, self.weight)
turbine = Building("GE 1.5 Megawatt Wind Turbine", 100, 150_000)
print(turbine)
This is usually the only way programmers interact with the dunder methods; by defining them yourself, you can make your classes interact better with the rest of Python.
Exercise 7 (15 pts)#
Make a new class called Monomial
to represent the monomial \(a x^b\) which takes two parameters, coefficient
and exponent
. It should have a single method derivative
which returns a new Monomial
, representing the derivative of \(a x^b\). When printed it should display nicely, like this:
>>> print(Monomial(2, 3))
2*x**3
>>> print(Monomial(2, 3).derivative())
6*x**2
>>> print(Monomial(2, 3).derivative().derivative())
12*x**1
You can use the function lab4a_check.exercise_7
to check your work.
Write Answers for Exercise 7 Below
lab4a_check.exercise_7(Monomial)
4. Inheritance #
It is fairly common to have a class which does almost everything you need; you just wish it did one more thing, or did something differently. For those times there is a clever system called inheritance; you can make a new class which “inherits” most of its behavior from the old one. Suppose I really wish I could make my strings print out in a more sarcastic way. Here’s how I might do that:
class meme_str(str):
def sarcastically(self):
return ''.join(c.upper() if i%2 else c.lower() for i, c in enumerate(self))
def clapping(self):
return '👏'.join(word.capitalize() for word in self.split())
definition = meme_str("the derivative is the slope of the tangent line")
print(definition)
print(definition.clapping())
print(definition.sarcastically())
We won’t use inheritance very much. Twenty to thirty years ago, when object-oriented programming was still something new, many people were impressed with how easy it was to do inheritance and so they built big, elaborate systems of inheritance. Mostly, the code that came out of that style was a tangled mess, where you could never be entirely sure where all the attributes came from. As a result, inheritance has become much less popular lately. For small things inheritance can still be useful, and many people still use inheritance so it’s good to know about it.