PROJECT 9
In this project you create a basic address book in which you can store your friend’s name, email address, and birthday. Or you can store your friends’ names, email addresses, and birthdays. You know. Some people like to keep a tight circle. You could extend the address book to include anything else about them. Did they borrow your favorite book or a dollar for lunch?
To put the address book together, you make your own objects, called classes. These custom (made-by-you) objects are the spine of Python work. In this project, you also see how to store your data in files so you can load your info later.
Address books have a lot of individual entries. The structure of each entry is pretty much the same, although the names are changed (to protect the innocent). You’re going to create your own personalized Python objects (called classes) to reuse for each address book entry by using Python’s class keyword.
You need to know about custom objects (objects you make yourself) to understand the projects at dummies.com/go/pythonforkids
.
Earlier when you created a string, Python packed up a heap of string methods for you. When you wrote a function, Python got the docstring for the help facility for you. You're going to have to get your hands dirty now, by making your own classes and squishing around their internals. (Eeew!)
Python objects have two parts:
The address book project uses two classes:
Creating a class is a little like creating a function:
Name the class.
Use an initial capital (per PEP8). If the name has two or more words, use CapsWords to show the new word. (Don’t insert spaces between words, but do capitalize the first letter in each new word.)
Type this:
class <the class name you've chosen>(object):
The '(object)' bit means that this class is a kind of object. It is said to inherit from object. Class inheritance is important, but you don’t need to know the details right now. Unless I say otherwise, include the '(object)' bit in all your class statements.
Add a docstring explaining the purpose of the class.
The docstring starts a new code block, so indent it.
Go down a line, indent, and put the stuff that makes up the class in a new code block on the next line.
For now, put a pass keyword (technically not necessary since you have a docstring).
You’ll use this a stub for the AddressEntry class later. Work in the IDLE Shell window for the time being so you can interact directly with the instances:
>>> class AddressEntry(object):
"""
AddressEntry instances hold and manage details of a person
"""
pass
>>> AddressEntry # without parentheses
<class '__main__.AddressEntry'>
>>>
When you press Return twice after the pass statement, the class is created. If you type the name of the class without parentheses, Python says (in the readout) that there’s a class, which is part of __main__ with the name AddressEntry.
You create instances of a class by adding parentheses after the name of the class (like the class was a function). This creates an instance of the AddressEntry class, but doesn’t store it:
>>> AddressEntry() # parentheses create an instance
<__main__.AddressEntry object at 0x7f9309751590>
Normally though, you’d choose a variable name to hold the class instance and assign the instance to the variable like this:
>>> address_entry = AddressEntry()
Notice the different naming convention for the instance:
Notice also that Python's description of the object is different:
AddressEntry: <class '__main__.AddressEntry'>
address_entry: <__main__.AddressEntry object at 0x7f9309751590>
Do a dir listing for each object to see its base attributes.
In other projects, I ask you to find out about objects’ attributes. Your custom classes should have attributes, too.
You can create attributes of class definitions or instances by using the dot syntax you've been using for object methods. For example, in Project 6 you use my_message.upper() to access the upper method of the my_message object. You use this dot syntax to assign a value to the attribute with your desired name.
Here’s an example where attributes called class_attribute and instance_attribute are created and a string assigned to each one, in order:
>>> AddressEntry.class_attribute = "This is a class attribute"
>>> address_entry.instance_attribute = "This is an instance attribute"
>>> AddressEntry.class_attribute
'This is a class attribute'
>>> address_entry.instance_attribute
'This is an instance attribute'
Now here’s a surprise for you:
>>> address_entry.class_attribute
'This is a class attribute'
The instance has inherited the class’s attribute — even though the instance already existed (you created it in the previous section) when the attribute was created. However, the class didn’t inherit the instance attribute. (Use dir to check if you don’t believe me.)
When Python accesses attributes, it looks them up in a specific order at the time you need them. This is why the instance inherits new attributes of the class, even after the instance was created, but the class doesn’t inherit new attributes of the instance. I don’t expect this to make sense right now, but it will later.
You’ll often want to create a class with default values for the attributes that instances will need. However, those values are just defaults. You usually want to change them later (less often for methods, but you’ll get an example of a method to be changed later).
Create a new instance (address_entry2) and change the attribute named class_attribute.
When an instance assigns a new value to an attribute that the class has, the instance is said to override that attribute of the class:
>>> address_entry2 = AddressEntry()
>>> address_entry2.class_attribute = "An overridden class attribute"
>>> address_entry2.class_attribute
'An overridden class attribute'
>>> AddressEntry.class_attribute
'This is a class attribute'
It only overrides the attribute in that instance. The class and other instances don’t have that attribute changed.
Your address book will store these tidbits:
You can also add other entries like private notes about them, kik, Skype, and Twitter accounts, who they’re friends with, and who they’re related to.
Things to do at this step are:
Create a new file!
Ah, yeah. Sorry. Gotta tell you every time. How about you call it address_book.py?
In the Classes section, create and name a class stub AddressBook.
The stub will store the address book. Make sure you remember a docstring.
You create a stub for a class pretty much the same way you do for a function. Use the AddressEntry code, but change the name and docstring.
In the Main section, create a main block and instantiate one instance of each class.
Choose a name for a variable to store each.
Did you get something like this?
"""
Addressbook.py
An address book program to store details of people I know.
Stuff I'm storing is:
first name
family name
email address
date of birth
[other stuff]
Brendan Scott
Feb 2015
"""
##### Classes Section
class AddressBook(object):
"""
AddressBook instances hold and manage a list of people
"""
pass
class AddressEntry(object):
"""
AddressEntry instances hold and manage details of a person
"""
##### Main Section
if __name__ == "__main__":
address_book = AddressBook()
person1 = AddressEntry()
When run, you shouldn’t see any output (no news is good news), but it will create the classes and create an instance of each class.
You can create attributes and values for an instance by assigning them as you would for any object.
For example, you could create an entry for a person like this. This continues from where you left off in the IDLE Shell window:
>>> person1 = AddressEntry()# creates the entry
>>> person1.first_name = "Eric" # sets the first name etc.
>>> person1.family_name = "Idle"
>>> person1.date_of_birth = "March 29, 1943"
>>> person1.email_address = None
Python gives you a way of streamlining the initialization of an instance’s values at the time of the creation of the instance. Create a method in the class named __init__. This method is called whenever an instance is created, so it can initialize values for the instance.
As you know, a method is a function which is part of an object. To make an __init__ method for one of your custom classes, you define a function called __init__ within the code block of the class.
Here’s what you need to know:
Here’s example code for the AddressEntry class. The main things to initialize when you’re creating an instance are the person’s first and last name, email address, and birthdate. So you add a dummy argument for each one in the definition statement:
class AddressEntry(object):
"""
AddressEntry instances hold and manage details of a person
"""
def _ _init_ _(self, first_name=None, family_name=None,
email_address=None, date_of_birth=None):
"""Initialize attributes first_name,
family_name and date_of_birth.
Each argument should be a string.
date_of_birth should be of the form "MM DD, YYYY"
"""
self.first_name = first_name
self.family_name = family_name
self.email_address = email_address
self.date_of_birth = date_of_birth
Define the __init__ method by using a def statement. It has an argument list just like you’d expect for a function. The arguments here are taking default values (except for self — you and I talk about your sense of self in a moment).
I added a docstring here because you’re supposed to. Sometimes I’m naughty and leave out the docstring for __init__.
The method doesn’t have a return statement at the end. You don’t need one. In this case, lots of assignments are made to attributes of the object self (that is, the first argument that was passed to the method). Think back for a moment to the previous section. There you created an attribute called first_name by doing this: person1.first_name = "Eric". When you’re writing the constructor ( __init__ ), it’s part of the class definition. That’s written before any instances are created.
When you make any method of a class, Python reserves the first argument to be a copy of itself. That way, you don’t need to know the name of the instance. All you need to know is the name of the dummy variable used to reference it. In this case, that name is self. At the time you instantiate an instance, a reference to that instance is passed to __init__.
Time to actually use this newfangled code to create an instance:
person1 = AddressEntry("Eric", "Idle", None, "March 29, 1943")
Your code for the Classes section and Main section should look like this:
##### Classes Section
class AddressBook(object):
"""
AddressBook instances hold and manage a list of people
"""
pass
class AddressEntry(object):
"""
AddressEntry instances hold and manage details of a person
"""
def _ _init_ _(self, first_name=None, family_name=None,
email_address=None, date_of_birth=None):
"""Initialize attributes first_name,
family_name and date_of_birth.
Each argument should be a string.
date_of_birth should be of the form "MM DD, YYYY"
"""
self.first_name = first_name
self.family_name = family_name
self.email_address = email_address
self.date_of_birth = date_of_birth
##### Main Section
if __name__ == "__main__":
address_book = AddressBook()
person1 = AddressEntry("Eric", "Idle", None, "March 29, 1943")
print(person1)
Running this code should get you output that looks something along the lines of <__main__.AddressEntry object at 0x7f6225c9cc10> or something equally unglamorous. That’s because Python has no idea how to print your custom object. It can’t tell what data in the object is important to print and what isn’t, whether you want everything printed or just a summary, and yadda. It takes the easy way out and prints the kind of object it is and where it’s stored in memory.
How can you tell whether the initialization worked? Right now you can’t. Printing out the details of an AddressEntry instance is the next problem you solve.
You need a function that prints out details of an AddressEntry instance. Then you’ll be able to check whether your initialization worked properly.
To do this, follow the steps:
Create a function, called __repr__, that takes a single argument.
That argument will be an instance of AddressEntry and will print out its attributes. Call that argument self for now. Can you guess why you’re calling it self? (Hint: It’s a method later.)
template = "AddressEntry(first_name='%s', "+
"family_name='%s',"+
" email_address='%s', "+
"date_of_birth='%s')"
Return the template filled with the corresponding attributes from the AddressEntry by making a tuple (self.first_name, self.family_name, self.email_address, self.date_of_birth).
Format it with the template.
The Classes section doesn’t change. For the new Functions and Main sections you should get this:
##### Functions Section
def _ _repr_ _(self):
"""
Given an AddressEntry object self return
a readable string representation
"""
template = "AddressEntry(first_name='%s', "+
"family_name='%s',"+
" email_address='%s', "+
"date_of_birth='%s')"
return template%(self.first_name, self.family_name,
self.email_address, self.date_of_birth)
##### Main Section
if __name__ == "__main__":
address_book = AddressBook()
person1 = AddressEntry("Eric", "Idle", None, "March 29, 1943")
print(person1)
print(__repr__(person1))
When you run this code, you will get a printout that looks like this:
<__main__.AddressEntry object at 0x2772d50>
AddressEntry(first_name='Eric', family_name='Idle',
email_address='None', date_of_birth='March 29, 1943')
The first line is the output of print(person1). The second line is from the new __repr__ function. Notice two things:
Did you figure out why you used the name self? Back in Project 4 (remember that long ago?) I told you that functions that start with __ have a special meaning in Python; __repr__ is one of them.
If you do a dir listing on one of your AddressEntry instances you will see it has a __repr__ method already. It’s that __repr__ method that is printing out <__main__.AddressEntry object at 0x2772d50>. If you override this method, the function you’ve just created will be called whenever you call print.
To do it quickly, add this line at the end of your Functions section, just before your Main section. Why aren’t there parentheses here?
AddressEntry.__repr__ = __repr__
Then run the code again. You get this:
AddressEntry(first_name='Eric', family_name='Idle',
email_address='None', date_of_birth='March 29, 1943')
AddressEntry(first_name='Eric', family_name='Idle',
email_address='None', date_of_birth='March 29, 1943')
Is that magic? It’s magic isn’t it? Go on, you know it is.
The code AddressEntry.__repr__ = __repr__ substitutes your newly written __repr__ function for the default __repr__ method that Python gave to your class when you created it. (It got that method from the object class it inherited from.) Now that you made this substitution to print an instance like person1, all you need to do is type print(person1).
The code AddressEntry.__repr__ = __repr__ was a patch job. The proper thing to do is move the __repr__ code into the class definition as a new method:
Delete the code AddressEntry.__repr__ = __repr__.
Your Functions section should be empty.
Delete the line print(__repr__(person1)).
You don’t need it, and it won’t work now that you’ve moved __repr__.
Here’s the new code for the AddressEntry class and the Functions (empty) and Main sections:
class AddressEntry(object):
"""
AddressEntry instances hold and manage details of a person
"""
def __init__(self, first_name=None, family_name=None,
email_address=None, date_of_birth=None):
"""initialize attributes first_name, family_name and date_of_birth
each argument should be a string
date_of_birth should be of the form "MM DD, YYYY"
"""
self.first_name = first_name
self.family_name = family_name
self.email_address = email_address
self.date_of_birth = date_of_birth
def __repr__(self):
"""
Given an AddressEntry object self return
a readable string representation
"""
template = "AddressEntry(first_name='%s', "+
"family_name='%s',"+
" email_address='%s', "+
"date_of_birth='%s')"
return template%(self.first_name, self.family_name,
self.email_address, self.date_of_birth)
##### Functions Section
##### Main Section
if __name__ == "__main__":
address_book = AddressBook()
person1 = AddressEntry("Eric", "Idle", None, "March 29, 1943")
print(person1)
At the moment, your AddressBook class is only a placeholder. It’s supposed to store a list of people. Time for AddressBook to pull its weight. You also include a way to add AddressEntry instances to that list:
Create a skeleton __init__ method.
It only needs self as an argument.
The AddressEntry code hasn’t changed. The code right now, including the new AddressBook code and the Main section, is this:
"""
Addressbook.py
An address book program to store details of people I know.
Stuff I'm storing is:
first name
family name
email address
date of birth
[other stuff]
Brendan Scott
Feb 2015
"""
##### Classes Section
class AddressBook(object):
"""
AddressBook instances hold and manage a list of people
"""
def __init__(self):
""" Set people attribute to an empty list"""
self.people = []
def add_entry(self, new_entry):
""" Add a new entry to the list of people in the
address book the new_entry should be an instance
of the AddressEntry class"""
self.people.append(new_entry)
class AddressEntry(object):
"""
AddressEntry instances hold and manage details of a person
"""
def __init__(self, first_name=None, family_name=None,
email_address=None, date_of_birth=None):
"""Initialize attributes first_name,
family_name and date_of_birth.
Each argument should be a string.
date_of_birth should be of the form "MM DD, YYYY"
"""
self.first_name = first_name
self.family_name = family_name
self.email_address = email_address
self.date_of_birth = date_of_birth
def __repr__(self):
"""
Given an AddressEntry object self return
a readable string representation
"""
template = "AddressEntry(first_name='%s', "+
"family_name='%s',"+
" email_address='%s', "+
"date_of_birth='%s')"
return template%(self.first_name, self.family_name,
self.email_address, self.date_of_birth)
##### Functions Section
##### Main Section
if __name__ == "__main__":
address_book = AddressBook()
person1 = AddressEntry("Eric", "Idle", None, "March 29, 1943")
print(person1)
address_book.add_entry(person1)
print(address_book.people)
When you run this, you get:
AddressEntry(first_name='Eric', family_name='Idle',
email_address='None', date_of_birth='March 29, 1943')
[AddressEntry(first_name='Eric', family_name='Idle',
email_address='None', date_of_birth='March 29, 1943')]
The second line is the list people (notice the []), which has one entry — person1. Python has been working magic again here. It called __repr__ when it printed out person1. You could, if you wanted to, create a __repr__ method for AddressBook, but it’s not necessary.
To save your address book, you need to write its contents out to a file. Here’s where the power of pickles comes in! (In the background, a hapless victim calls out, “Save me with the power of a pickle!”)
Import the pickle module and use its dump method. It needs an open file. (Go back to Project 7 if you forgot how to open files.)
Here’s an example:
>>> import pickle
>>> FILENAME = "p4k_test.pickle"
>>> dummy_list = [x*2 for x in range(10)]
>>> dummy_list # confirm what it looks like
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
>>> with open(FILENAME,'w') as file_object: #now dump it!
pickle.dump(dummy_list,file_object)
>>> # open the raw file to look at what was written
>>> with open(FILENAME,'r') as file_object: # change w to r!!!
print(file_object.read())
(lp0
I0
aI2
aI4
aI6
aI8
aI10
aI12
aI14
aI16
aI18
a.
You created a list object, called dummy_list, and then pickled it into the file p4k_test.pickle. You opened the file and read back its contents. It was mostly junk, but you can see an echo of the original list hidden in there.
The pickle module is to “Create portable serialized representations of Python objects” (from the docstring). Which is easy to say (try it), but rather harder to explain. Python stores its objects in your computer’s memory. The way it stores it is determined, in part, by the computer you’re running and the operating system that you are using. In order to transfer an object from one place (or one time) to another, it needs to be represented in a way which is independent of (apart from) those things.
Generally speaking, that’s called the process of serialization, preparing data so it can be saved or sent. Python’s pickle module allows you to save Python objects in a file in a way that any other Python program reading that file is able to re-create a copy of the object.
Python can’t pickle just anything. It can pickle only if the object is hashable. It’s hashable if it has __hash__() and either the __eq__() or __cmp__() method. When you try to pickle more complex objects, you’ll run into this problem. By then you’ll be able to research your own solution.
Time to prove that you can re-create the original object. Close IDLE completely. Then restart it:
Python 2.7.3 (default, Apr 14 2012, 08:58:41) [GCC] on linux2
Type "copyright", "credits" or "license()" for more information.
>>> import pickle
>>> FILENAME = "p4k_test.pickle"
>>> with open(FILENAME,'r') as file_object:
dummy_copy = pickle.load(file_object)
>>> dummy_copy
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
Hey, it’s the same as what you pickled. It survived in the file p4k_test.pickle while you turned IDLE off and on again.
To save an object called variable_name with the pickle module, follow these steps:
Name the file where you’ll store the data.
If it’s going to be the same name every time (rather than, for example, a file name chosen by the user) then store it as a constant.
Open that filename with the 'w' (write) attribute and store the file object that open returns.
The with keyword takes care of a lot of this for you: with open(FILENAME,'w') as file_object: I suggest you use the 'w' so that you always overwrite the save file with a new copy of the object. You can add more objects to an existing file with the append attribute ('a') but I’m not going there.
To load an object with the pickle module:
Open that file with the 'r' (read) attribute and store the file object that open returns.
The with keyword takes care of a lot of this for you: with open(FILENAME,'r') as file_object:
You can customize the pickle module. Another module, cPickle, isn’t as customizable, but is much faster than pickle for saving and loading.
Use cPickle in your programs in the future. It’s customary to refer to the module pickle in your code even when you’re using the cPickle module. What? Import the cPickle module with an alias — import cPickle as pickle means everywhere else in the code. Python sees pickle as a reference to cPickle. For example, if you did import random as crazy, then you could call the crazy.randint method.
You can add a save function to the file fairly easily, although testing it is a little difficult. You just dump the instances to the file.
When you work on a more complicated program, include code that lets the user choose the name of the file in which to save the address book.
For this project, the filename is hard coded:
SAVE_FILE_NAME = "address_book.pickle"
with open(SAVE_FILE_NAME,'w') as file_object:
pickle.dump(self, file_object)
The new Imports and Constants sections look like this:
#### Imports
import cPickle as pickle
#### Constants
SAVE_FILE_NAME = "address_book.pickle"
This is the save method you added to the AddressBook class:
def save(self):
with open(SAVE_FILE_NAME, 'w') as file_object:
pickle.dump(self, file_object)
You added this line to the Main section:
address_book.save()
When you wrote the code to save the AddressBook instance, you made it a method of the class. It literally saved itself, and that’s a problem. An object can’t load itself because it won’t exist until after it’s been loaded. (Riddle me that one, PythonCoder.) The problem then is where to put the code for loading the object.
I propose that you make another class to control how the program flows are going to work and manage communications between the user and the AddressBook instance. That will allow you to step outside the AddressBook and reference it more naturally.
Create a Controller class with these steps:
class Controller(object):
Add a load method to the class.
The load method you create should use the formula to load an object from the save file, and should replace the address_book attribute of the Controller. The load method relies on you having already pickled an AddressBook instance into that file. (Save a pickle there from the command line if you need to.)
def load(self):
"""
Load a pickled address book from the standard save file
"""
with open(SAVE_FILE_NAME, 'r') as file_object:
self.address_book = pickle.load(file_object)
I’m calling it a Controller because it’s controlling the data saved in the address book. The revised code in all its glory:
"""
Addressbook.py
An address book program to store details of people I know.
Stuff I'm storing is:
first name
family name
email address
date of birth
[other stuff]
Brendan Scott
Feb 2015
"""
#### Imports
import cPickle as pickle
#### Constants
SAVE_FILE_NAME = "address_book.pickle"
##### Classes Section
class AddressBook(object):
"""
AddressBook instances hold and manage a list of people
"""
def __init__(self):
""" Set people attribute to an empty list"""
self.people = []
def add_entry(self, new_entry):
""" Add a new entry to the list of people in the
address book the new_entry should be an instance
of the AddressEntry class"""
self.people.append(new_entry)
def save(self):
""" save a copy of self into a pickle file"""
with open(SAVE_FILE_NAME, 'w') as file_object:
pickle.dump(self, file_object)
class AddressEntry(object):
"""
AddressEntry instances hold and manage details of a person
"""
def __init__(self, first_name=None, family_name=None,
email_address=None, date_of_birth=None):
"""Initialize attributes first_name,
family_name and date_of_birth.
Each argument should be a string.
date_of_birth should be of the form "MM DD, YYYY"
"""
self.first_name = first_name
self.family_name = family_name
self.email_address = email_address
self.date_of_birth = date_of_birth
def __repr__(self):
"""
Given an AddressEntry object self return
a readable string representation
"""
template = "AddressEntry(first_name='%s', "+
"family_name='%s',"+
" email_address='%s', "+
"date_of_birth='%s')"
return template%(self.first_name, self.family_name,
self.email_address, self.date_of_birth)
class Controller(object):
"""
Controller acts as a way of managing the data stored in
an instance of AddressBook and the user, as well as managing
loading of stored data
"""
def __init__(self):
"""
Initialise controller. Look for a saved address book
If one is found,load it, otherwise create an empty
address book.
"""
self.address_book = AddressBook()
person1 = AddressEntry("Eric", "Idle", "March 29, 1943")
self.address_book.add_entry(person1)
def load(self):
"""
Load a pickled address book from the standard save file
"""
with open(SAVE_FILE_NAME, 'r') as file_object:
self.address_book = pickle.load(file_object)
##### Functions Section
##### Main Section
if __name__ == "__main__":
controller = Controller()
print(controller.address_book.people)
The main thing to pay attention to in this code is that I have changed the references from address_book to self.address_book. It makes it an attribute of the Controller instance. I also changed the last print function from address_book.people to controller.address_book.people. It shows that people is an attribute of address_book and that address_book is an attribute of controller.
When you run the code, you get this:
[AddressEntry(first_name='Eric', family_name='Idle', email_address='March 29, 1943', date_of_birth='None')]
How does that testing help? It makes sure all the references from the new Controller class are working. Remember that the load method is not being tested here, since control never passes to it.
To test the load method, you need an object already pickled into the file address_book.pickle. In the final version of your application, you want the load file to run automatically when you start it. To do that, the file should first test whether there’s a save file to load. If it’s there, it should load (and, if not, do nothing).
To test whether a file exists, use the exists method from the os.path module. If you pass os.path a file path, it tells you whether a file exists at that path. Test to see if you have already saved a file. You should have saved this when running the earlier code.
>>> import os.path
>>> SAVE_FILE_NAME = "address_book.pickle"
>>> os.path.exists(SAVE_FILE_NAME)
True
>>> os.path.exists("some other filename that doesn't exist")
False
If os.path.exists(SAVE_FILE_NAME) is False, you’re in trouble! Check that you’ve typed SAVE_FILE_NAME = "address_book.pickle" correctly here and in the Constants section of the preceding code. If it’s typed correctly, then run this code in the IDLE Shell window to save a new copy:
>>> from address_book import SAVE_FILE_NAME
>>> from address_book import AddressBook, AddressEntry
>>> person1 = AddressEntry("Eric", "Idle", None, "March 29, 1943")
>>> address_book = AddressBook()
>>> address_book.add_entry(person1)
>>> address_book.save()
>>> import os.path # confirm it's there
>>> os.path.exists(SAVE_FILE_NAME)
True
Now you’re going to upgrade the Controller so that when it’s created, it checks whether there’s a save file to load (and if there is, it loads that save file):
Make the first line of the Controller constructor self.address_book = self.load().
You’re going to change the load method so that if there’s a save file to load, then it loads address_book from the file and returns it. Otherwise it returns None.
If the file exists, change the existing code to load an object using pickle, then return the object loaded.
Otherwise, return None.
Now the Imports section looks like this:
#### Imports
import cPickle as pickle
import os.path
And the Controller section looks like this:
class Controller(object):
"""
Controller acts as a way of managing the data stored in
an instance of AddressBook and the user, as well as managing
loading of stored data
"""
def __init__(self):
"""
Initialize controller. Look for a saved address book
If one is found,load it, otherwise create an empty
address book.
"""
self.address_book = self.load()
if self.address_book is None:
self.address_book = AddressBook()
def load(self):
"""
Load a pickled address book from the standard save file
"""
#TODO: Test this method
if os.path.exists(SAVE_FILE_NAME):
with open(SAVE_FILE_NAME, 'r') as file_object:
return pickle.load(file_object)
else:
return None
I tested this both when the file was there and when it wasn’t. You should too. Be careful!
The final thing you need to do for this project is add an interface to add, delete, and show address book entries:
INSTRUCTIONS = """Address Book Application
(Python For Kids For Dummies Project 9)
Press:
a to add an entry
d to display a list of all entries in summary form.
i to print these instructions again
q to quit.
"""
CONFIRM_QUIT_MESSAGE = 'Are you sure you want to quit (Y/n)? '
Add a new method called run_interface in Controller: def run_interface(self):.
Don’t forget your self!
In run_interface, show the instructions print(INSTRUCTIONS). Then create an infinite while loop: while True:.
The while loop is the program’s context. In each iteration it should:
if command == "a":
self.add_entry()
Create a method stub for add_entry and display_summaries.
Each stub should include a docstring explaining the method and a print statement so you can make sure the correct function is being called for each possible command given. An example for add_entry:
def add_entry(self):
"""query user for values to add a new entry"""
print("In add_entry")
The AddressBook and AddressEntry classes are unchanged, as are the Imports and Main sections. The new Constants section looks like this:
#### Constants
SAVE_FILE_NAME = "address_book.pickle"
INSTRUCTIONS = """Address Book Application
(Python For Kids For Dummies Project 9)
Press:
a to add an entry
d to display a list of all entries in summary form.
i to print these instructions again
q to quit.
"""
CONFIRM_QUIT_MESSAGE = 'Are you sure you want to quit (Y/n)? '
In Controller, the constructor looks like this. Only the last line is new:
def __init__(self):
"""
Initialize controller. Look for a saved address book
If one is found,load it, otherwise create an empty
address book.
"""
self.address_book = self.load()
if self.address_book is None:
self.address_book = AddressBook()
self.run_interface()
I also added the run_interface methods to Controller (after the end of the load method) and copied across confirm_quit from Project 5 into the Functions section:
def run_interface(self):
""" Application's main loop.
Get user input and respond accordingly"""
print(INSTRUCTIONS)
while True:
command = raw_input("What would you like to do? ")
if command == "a":
self.add_entry()
elif command == "q":
if confirm_quit():
print("Saving")
self.address_book.save()
print("Exiting the application")
break
elif command == "i":
print(INSTRUCTIONS)
elif command == "d":
self.display_summaries()
else:
template = "I don’t recognise that instruction (%s)"
print(template%command)
def add_entry(self):
"""query user for values to add a new entry"""
print("In add_entry")
def display_summaries(self):
""" display summary information for each entry in
address book"""
print("In display_summaries")
##### Functions Section
def confirm_quit():
"""Ask user to confirm that they want to quit
default to yes
Return True (yes, quit) or False (no, don't quit) """
spam = raw_input(CONFIRM_QUIT_MESSAGE)
if spam == 'n':
return False
else:
return True
I ran the code and tested that each command called the correct method. You need to test your code as well to make sure that for these commands, it prints certain things:
To finish off the application, add code that makes the methods add_entry and display_summaries work.
The method add_entry is supposed to add an address book entry. To do that you need to get values for first_name, last_name, email_address, and date_of_birth.
print("Adding a new person to the address book")
print("What is the person's:")
Use a raw_input statement to get a value for each of the attributes.
This is an example for first_name:
first_name = raw_input("First Name? ")
For each of these, add a test to see if the user typed q.
If so, don’t add the entry (just return). The code for first_name (repeat this code, but change the name of the variable for the other attributes) would be this:
if first_name == "q":
print("Not Adding")
return
entry = AddressEntry(first_name, family_name, date_of_birth)
self.address_book.add_entry(entry)
For display_summaries, use enumerate to list all the people in AddressBook.
Each will be an AddressEntry instance. Make a template (in the Constants section) like:
SUMMARY_TEMPLATE = "%s %s DOB: %s email: %s"
In the for loop, use this template to format each of the attributes first_name, last_name, date_of_birth, and email_address into a dummy variable. Then print the index (add one) and the entry using a short formatting string like "%s: %s". This will print a summary of each entry with numbering. Some sample code:
for index, e in enumerate(self.address_book.people):
values = (e.first_name, e.family_name,
e.date_of_birth, e.email_address)
entry = SUMMARY_TEMPLATE%values
print("%s: %s"%(index+1, entry))
# start numbering at 1
Remove the code print(controller.address_book.people) at the end of the Main section
You don’t need it for debugging anymore. Now the code in Main section is very simple. It's mainly just controller = Controller(). The program flow is a little different in this example. After controller is instantiated, control passes to its constructor and, from there, to its run_interface method. It only leaves the run_interface method when it’s time to quit. You’ll see this structure more often as you work with Python.
Your final version of the code should look something like this:
"""
Addressbook.py
An address book program to store details of people I know.
Stuff I'm storing is:
first name
family name
email address
date of birth
[other stuff]
Brendan Scott
Feb 2015
"""
#### Imports
import cPickle as pickle
import os.path
#### Constants
SAVE_FILE_NAME = "address_book.pickle"
INSTRUCTIONS = """Address Book Application
(Python For Kids For Dummies Project 9)
Press:
a to add an entry
d to display a list of all entries in summary form.
i to print these instructions again
q to quit.
"""
CONFIRM_QUIT_MESSAGE = 'Are you sure you want to quit (Y/n)? '
SUMMARY_TEMPLATE = "%s %s DOB: %s email: %s"
##### Classes Section
class AddressBook(object):
"""
AddressBook instances hold and manage a list of people
"""
def __init__(self):
""" Set people attribute to an empty list"""
self.people = []
def add_entry(self, new_entry):
""" Add a new entry to the list of people in the
address book the new_entry should be an instance
of the AddressEntry class"""
self.people.append(new_entry)
def save(self):
""" save a copy of self into a pickle file"""
with open(SAVE_FILE_NAME, 'w') as file_object:
pickle.dump(self, file_object)
class AddressEntry(object):
"""
AddressEntry instances hold and manage details of a person
"""
def __init__(self, first_name=None, family_name=None,
email_address=None, date_of_birth=None):
"""Initialize attributes first_name,
family_name and date_of_birth.
Each argument should be a string.
date_of_birth should be of the form "MM DD, YYYY"
"""
self.first_name = first_name
self.family_name = family_name
self.email_address = email_address
self.date_of_birth = date_of_birth
def __repr__(self):
"""
Given an AddressEntry object self return
a readable string representation
"""
template = "AddressEntry(first_name='%s', "+
"family_name='%s',"+
" email_address='%s', "+
"date_of_birth='%s')"
return template%(self.first_name, self.family_name,
self.email_address, self.date_of_birth)
class Controller(object):
"""
Controller acts as a way of managing the data stored in
an instance of AddressBook and the user, as well as managing
loading of stored data
"""
def __init__(self):
"""
Initialize controller. Look for a saved address book
If one is found,load it, otherwise create an empty
address book.
"""
self.address_book = self.load()
if self.address_book is None:
self.address_book = AddressBook()
self.run_interface()
def load(self):
"""
Load a pickled address book from the standard save file
"""
if os.path.exists(SAVE_FILE_NAME):
with open(SAVE_FILE_NAME, 'r') as file_object:
address_book = pickle.load(file_object)
return address_book
else:
return None
def run_interface(self):
""" Application's main loop.
Get user input and respond accordingly"""
print(INSTRUCTIONS)
while True:
command = raw_input("What would you like to do? ")
if command == "a":
self.add_entry()
elif command == "q":
if confirm_quit():
print("Saving")
self.address_book.save()
print("Exiting the application")
break
elif command == "i":
print(INSTRUCTIONS)
elif command == "d":
self.display_summaries()
else:
template = "I don't recognise that instruction (%s)"
print(template%command)
def add_entry(self):
"""query user for values to add a new entry"""
print("Adding a new person to the address book")
print("What is the person's:")
first_name = raw_input("First Name? ")
if first_name == "q":
print("Not Adding")
return
family_name = raw_input("Family Name? ")
if first_name == "q":
print("Not Adding")
return
email_address = raw_input("Email Address? ")
if first_name == "q":
print("Not Adding")
return
DOB_PROMPT = "Date of Birth (Month day, year)? "
date_of_birth = raw_input(DOB_PROMPT)
if first_name == "q":
print("Not Adding ")
return
entry = AddressEntry(first_name, family_name,
email_address, date_of_birth)
self.address_book.add_entry(entry)
values = (first_name, family_name)
print("Added address entry for %s %s
"%values)
def display_summaries(self):
""" display summary information for each entry in
address book"""
print("Displaying Summaries")
for index, e in enumerate(self.address_book.people):
values = (e.first_name, e.family_name,
e.date_of_birth, e.email_address)
entry = SUMMARY_TEMPLATE%values
print("%s: %s"%(index+1, entry))
# start numbering at 1
##### Functions Section
def confirm_quit():
"""Ask user to confirm that they want to quit
default to yes
Return True (yes, quit) or False (no, don't quit) """
spam = raw_input(CONFIRM_QUIT_MESSAGE)
if spam == 'n':
return False
else:
return True
##### Main Section
if __name__ == "__main__":
controller = Controller()
You made it through a big fat project! This code gives you a solid core for an address book application that you can expand — for example, use the Cryptopy project to add secret notes, change the AddressBook so that when you add an entry it sorts the list in alphabetical order, add new fields (special skills?), calculate the person’s age from their date of birth (research the datetime module), and when you do the Hello GUI World project, wrap a graphical interface around the app. The possibilities are endless.
You’re introduced to tons in this project: