PROJECT 9

Address Book

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?

image

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.

Your First Class Objects

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:

  • A class declaration, which helps you create the objects themselves. (Each is called an instance of the class.) Creating an instance is called instantiation. Classes are like a factory for creating instances. The instances are nearly copies of their corresponding class, but they’re not clones because you don’t use them to make more copies.
  • Instances of the class declaration. You use classes like a mold to create the instances. When you use a class declaration, the object you create (instantiate is the computer term) is called an instance of the class.

Create a Class

The address book project uses two classes:

  • One for the address book. It will only have one instance.
  • One for the address entries. It will have one instance for each person whose details you store.

Creating a class is a little like creating a function:

  1. Name the class.

    remember 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.)

  2. 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.

  3. Add a docstring explaining the purpose of the class.

    The docstring starts a new code block, so indent it.

  4. 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.

Create an Instance

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:

  • On the left, lowercase_words are separated by an underscore.
  • On the right, CapsWords with no separations.

tip Using lowercase for instances and CapsWords for classes helps you tell the difference between classes (AddressEntry) and class instances (address_entry).

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.

Create Class and Instance 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.

Plan Your Address Book

Your address book will store these tidbits:

  • First name
  • Last name
  • Email address
  • Birthday

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.

Set Up Your File and Create a Class

Things to do at this step are:

  1. Create a new file!

    Ah, yeah. Sorry. Gotta tell you every time. How about you call it address_book.py?

  2. Write a module docstring telling the world that this is an address book application.
  3. Add a #### Classes section.
  4. 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.

  5. In the Classes section, copy in the code for AddressEntry.
  6. Create a ##### Main section.
  7. 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.

Add Your First Person

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:

  • You don’t need to think of a name. It’s __init__. You’ve got no choice.
  • Unlike normal functions, all methods need to be defined with at least one argument. That first argument is always called self. If you want to initialize attributes with values — that is, when you instantiate the class you want to configure some initial values — pass in the values as arguments and assign them to attributes.

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).

tip Please pay attention to the indentation. The __init__ method is part of the class’s code block, so everything in it is one indent to the right more than it would be if it were defined as a function.

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.

remember You can’t use the name of an instance to create an attribute in __init__. What’s more, you can’t choose the name of any particular instance because it’ll be wrong for every other instance. That’s where the magic of self comes in.

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__.

Instantiate an Instance Using __init__

Time to actually use this newfangled code to create an instance:

  1. Update your AddressEntry class to include the code for __init__.
  2. In the Main section, create a new instance like this:

    person1 = AddressEntry("Eric", "Idle", None, "March 29, 1943")

  3. Add a line to print out person1 to the Main section.

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.

Create a Function to Print the Instance

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:

  1. Add a ##### Functions section before the ##### Main section.
  2. 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.)

  3. In the function, use %s formatting specifiers to create a template that looks like this:

        template = "AddressEntry(first_name='%s', "+

                   "family_name='%s',"+

                   " email_address='%s', "+

                   "date_of_birth='%s')"

  4. 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.

  5. Add a line to print(__repr__(person1)) in the Main section.

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:

  • Each attribute is correct (technically, 'None' should be None without the quotes, but it was going to be a little too involved to do that properly). This means that your __init__ function is properly initializing the attributes.
  • The output (apart from None) is good enough to copy and paste into your code to create a new AddressEntry.

Use __repr__ Magic

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.

remember Whenever Python prints an object it calls the object’s __repr__ method.

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:

  1. Cut and paste the code of the __repr__ function into AddressEntry below the __init__ method. Then indent it one level.
  2. Delete the code AddressEntry.__repr__ = __repr__.

    Your Functions section should be empty.

  3. 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)

Initialize the AddressBook Instance

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:

  1. Delete the pass statement in the AddressBook class.
  2. Create a skeleton __init__ method.

    It only needs self as an argument.

  3. Write a docstring.
  4. Set the attribute people to be an empty list.
  5. Create a method called add_entry in the AddressBook class.
  6. Define it to have two arguments — self and new_entry.
  7. Write a docstring for it. It’s adding new_entry to the list people.
  8. Use self.people.append(new_entry) to add the entry.
  9. In the Main section, add address_book.add_entry (person1). Then add print(address_book.people).

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.

Find Pickle Power

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:

  1. 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.

  2. 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.

  3. Call pickle.dump(file_object, variable_name).

To load an object with the pickle module:

  1. Get the name of the file where the data’s been stored.
  2. 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:

  3. Call pickle’s load method and assign it to a variable: variable_name = pickle.load(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.

warning Pickle’s awesome pickle power is a security risk. If someone tricks you into running pickle.load() on a pickle they made, they can run code on your machine. Don’t unpickle any pickle unless you trust that it’s yours and nobody’s messed with it. When you move your skills up to The Next Level, this includes not accepting data in pickle files across a network.

Add a save Function

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:

  1. Add an Imports section and a Constants section.
  2. Type the words import cPickle as pickle.
  3. Type this in the Constants section:

    SAVE_FILE_NAME = "address_book.pickle"

  4. In the AddressBook class, add a method called save.
  5. In the save method, add this:

          with open(SAVE_FILE_NAME,'w') as file_object:

              pickle.dump(self, file_object)

  6. In the Testing section, type address_book.save().

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()

Loading a Saved Pickle in the Same Application

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

Create a Controller class with these steps:

  1. Create a new class definition called Controller:

    class Controller(object):

  2. Create a docstring for it. Freestyle it!
  3. Give it a constructor method: def __init__(self):
  4. In that method, create an instance of AddressBook as an attribute of Controller. Call it address_book. self.address_book = AddressBook().
  5. Transfer the other initialization code to this constructor method: person1 = AddressEntry("Eric", "Idle", "March 29, 1943").
  6. 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)

  7. In the Main section, create an instance of the Controller class: controller = Controller().

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.

warning This is two levels of attribute (attribute of attribute of object). You can do as many as you like, but don’t get too carried away. It’ll get hard to follow.

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.

Test the load Method

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):

  1. In your Imports section: import os.path.
  2. 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.

  3. Add a line to check if self.address_book is None:. If it is, create a new address_book: self.address_book = AddressBook().
  4. In the load method, test to see if os.path.exists(SAVE_FILE_NAME).
  5. 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!

Add an Interface

The final thing you need to do for this project is add an interface to add, delete, and show address book entries:

  1. Give the user some instructions and a prompt to confirm a quit. Add this in the Constants section:

    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)? '

  2. Read those instructions yourself, because that’s what you’re going to be coding to.
  3. Add a new method called run_interface in Controller: def run_interface(self):.

    Don’t forget your self!

  4. Add self.run_interface() at the end of the Controller constructor.
  5. 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:

    • Ask the user what they want to do: command = raw_input("What would you like to do? "). Then see what the user’s command is.
    • Respond to the user’s command. Use an if/elif clause to check for each option listed in the INSTRUCTIONS constant.
    • Create a method stub (in Controller) for add_entry and display_summaries. Add a call to that stub at the corresponding point in the if clause. For example, the code to deal with "a" (for add entry) is

      if command == "a":

          self.add_entry()

    • If the user chooses to quit elif command == "q": then call confirm_quit. Copy and paste the code for the confirm_quit function across from Project 5 (guess_game_fun.py) into the Functions section. If the quit is confirmed, print a message saying that the application is saving print("Saving"), call address_book’s save method, then break (out of the loop), and let the program end.
    • If the user has not typed a command listed in the instructions, say so. At the end of the if block include an else: which tells the user you don’t understand their instructions print("I don't recognise that instruction (%s)"%command)
  6. 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:

  • a = "In add_entry"
  • d = "In display_summaries"
  • i = instructions again
  • q = quit confirmation
  • You also need to check if you type something else (eg r) it prints "I don't recognize that instruction (r)".
  • Note that you now have two add_entry methods. The first is in the AddressBook class, the second is in the Controller class. They can live in harmony because the name of the class allows you to distinguish between the two of them. In fact, the add_entry method in the Controller class will even call the add_entry method in the AddressBook class.

Fill in the Methods

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.

  1. Give the user some information:

            print("Adding a new person to the address book")

            print("What is the person's:")

  2. 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? ")

  3. 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

  4. Use the values you collect to create an AddressEntry and add it to AddressBook. The code entry = AddressEntry(first_name, family_name, date_of_birth) is a single line of code.

            entry = AddressEntry(first_name, family_name, date_of_birth)

            self.address_book.add_entry(entry)

  5. 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

  6. 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.

The Complete Code

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()

Summary

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:

  • Defining your own custom objects with classes.
  • Creating objects from your definitions with instances.
  • That classes, like all objects, can have attributes.
  • The difference between class attributes and instance attributes.
  • How instances can inherit the attributes of their defining class, but not vice versa.
  • What a constructor method (__init__) is and how it runs each time you create an instance.
  • Passing parameters to __init__ to prefill instances with data.
  • What overriding is, and how overriding __repr__ lets you get a different printout for your object.
  • Adding methods to your classes.
  • How self is automatically passed as an argument when you call your class methods.
  • Saving and loading general Python objects with the power of pickle.
  • Using the os.path module to see if a file exists on your file system.
  • Reusing and applying some earlier concepts: if/elif/else blocks, opening files, enumerate, raw_input, and formatting strings.
..................Content has been hidden....................

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