Hour 12. Expanding Classes to Add Functionality


What You’ll Learn in This Hour:

Image How to use built-in extras with classes

Image How to use class inheritance

Image When to expand classes in the real world


So far, you’ve learned the basics of how to create a class. We’ve only just scratched the surface, though. There’s so much more we can use to make our classes act more like the data types we’ve been using so far, and how to build one class off of another class.

Built-in Extras

Now that we know how to make our own data types, how do we get them to act more like the data types that already exist in Python? The data types we’ve used have worked really well with various operators (such as ==) and statements (such as print). Our classes don’t work that well with these right now. Let’s create a test class and try to print it and compare two objects of that class type:

>>> class Test(object):
...   def __init__(self):
...      self.one = 1
...
>>> t = Test()
>>> print
t <__main__.Test object at 0x10e450a10>
>>> s = Test()
>>> t == s
False

What’s going on here? We didn’t get any errors, but we certainly didn’t get what we expected. Printing t gave us some strange text that’s of no help, and comparing s and t gave us False, even though they both should be identical.

Python is being so strange because it doesn’t know yet how to compare Test objects, nor does it know what you want when you ask to print one out. We need to explicitly tell Python how to do those things.

Equality

So far, you’ve learned a couple ways to compare two values: are they the same, and is one bigger than the other one? What happens if we try this with a small custom object, as in the following example?

>>> class Test(object):
...   def __init__(self):
...     self.num = 5
...
>>> a = Test()
>>> b = Test()
>>> a == b
False

We know that a and b should be equal—there’s only one attribute, and they both should be the same! Python doesn’t know how to compare them yet, though, so we need to write a new function: __eq__().

__eq__() takes two parameters: self and another object. When we test for equality, self is the item before the == operator, and the item after the operator is passed in as a parameter.

Let’s add __eq__() to our Test class:

>>> class Test(object):
...   def __init__(self, num):
...     self.num = num
...   def __eq__(self, other):
...     if self.num == other.num:
...        return True
...     else:
...        return False
...
>>> a = Test(5)
>>> b = Test(5)
>>> c = Test(7)
>>> a == b
True
>>> a == c
False

Now, Python knows how to test for equality: It checks to see if the num attributes are the same. Does this mean that Python now knows how to test for inequality? Sadly, no. Python is not going to make any assumptions. Our class is simple, but a more complex class might have a more nuanced idea of what “equal” means.

In this case, we need to write another function: __ne__(). This one should return True if the objects are not the same, as in the following example:

>>> class Test(object):
...   def __init__(self, num):
...     self.num = num
...   def __eq__(self, other):
...     if self.num == other.num:
...        return True
...     else:
...        return False
...   def __ne__(self, other):
...     if self.num != other.num:
...        return True
...     else:
...        return False
...
>>> a = Test(5)
>>> b = Test(5)
>>> c = Test(7)
>>> a != b
False
>>> a != c
True

Now, both == and != work like we’d expect them to!

Greater Than and Less Than

Now that we can test for equality, we might want to consider testing to see if one object is greater than another. This isn’t always necessary. Think about a class that represents two books: How can one book be more than the other? Price? Page count? Number in inventory? Stars on Amazon? Contribution to humanity?

With our Test class, however, it’s pretty easy to say if one instance is bigger than another. There’s just one attribute, after all.

The four functions that test for size are shown in Table 12.1. If you decide that you want to allow users to test for size, make sure to write all of these functions, not just one or two. That could be confusing to people down the road.

Image

TABLE 12.1 Functions for Testing Size

These work in much the same way that the __eq__() function works: They accept two values, one before the operator and one after, and return True or False. The value before the operator is assigned to self, and the value after the operator is assigned to the other parameter you’ve defined.

Let’s add a “greater than or equal to” operator to our Test class:

>>> class Test(object):
...    def __init__(self, num):
...       self.num = num
...    def __gte__(self, other):
...       if self.num >= other.num:
...          return True
...       else:
...          return False
...
>>> alpha = Test(5)
>>> beta = Test(5)
>>> gamma = Test(6)
>>> alpha >= beta
True
>>> alpha >= gamma
False
>>> gamma >= alpha
True

The rest of the functions would be written in much the same way: Accept two values, figure out how you want to compare them, and then return True or False.

Working with print

What happens if we try to print a Test object? We get some rather strange output:

>>> class Test(object):
...   def __init__(self, text, num):
...     self.text = text
...     self.num = num
...
>>> a = Test(text="Hello", num=10)
>>> print a
<__main__.Test object at 0x109c09950>

It’s not an error, but it’s certainly not what we wanted. The problem is Python doesn’t know exactly what we want. Should it print text? Should it print num? Or should it print something completely different?

The __str__() function is what we need to write to set Python straight. __str__() takes one parameter (self) and should return a string. Let’s add __str__() to our Test class now:

>>> class Test(object):
...   def __init__(self, text, num):
...     self.text = text
...     self.num = num
...   def __str__(self):
...     return self.text
...
>>> a = Test(text="Hello", num=10)
>>> print a
Hello

This is a rather simple example: When we print a, Python checks with the Test class to see what it should print out. Because we’ve overwritten __str__(), Python now knows to print out the text in text.

You don’t have to limit yourself to simply returning one of the attributes of the variable, though. You can choose to print out something fancier. For example, let’s print out all the attributes in our object:

>>> class Test(object):
...   def __init__(self, word, num):
...     self.word = word
...     self.num = num
...   def __str__(self):
...     return "Values in this object:
... word = {word}, num = {num}".format(word=self.word, num=self.num)
...
>>> a = Test(word = "Hello", num=5)
>>> print a
Values in this object: word = Hello, num = 5

Just remember: At the end of your __str__() function, you have to return something! If you don’t, Python will get confused, throw an error, and your program will stop running.

Here, we have a __str__() function that doesn’t return anything. Note the error we get when we try to print our Test2 object.

>>> class Test2(object):
...   def __init__(self, text, num):
...     self.text = text
...     self.num = num
...   def __str__(self):
...     print "NO"
...
>>> t = Test2(text="Hi", num=5)
>>> print t
NO
Traceback (most recent call last):
   File "<stdin>", line 1, in <module>
TypeError: __str__ returned non-string (type NoneType)

Class Inheritance

In Hour 10, “Making Objects,” we touched on the idea of creating objects that inherit properties and functions from other objects. We created a generic inventory item class and then some more specific objects that would inherit from it. Book had an ISBN and an author, whereas Magazine had a month and year. Both Book and Magazine had attributes called “description” and “title” because the basic bookstore item had those.

This is called either polymorphism or class inheritance. It seems complicated at first blush, but really, it can cut down on the amount of programming you have to do and, more importantly, reduce maintenance.

Saving a Class in a File

For this section, let’s write our basic inventory item class in a file. First, let’s check our notes from Hour 10 (shown in Figure 12.1).

Image

FIGURE 12.1 Our bookstore plan from Hour 10.

First, we have our base item, which we’re going to call InventoryItem. All of our other objects are going to be based off of this object, so it’s going to be pretty generic.

InventoryItem is rather basic: It comes with a title, a description, a price, and a store ID. If you print it, it returns the item’s title. If you want to see if two items are equal, it compares the store IDs. It also has three functions for changing the price, description, and title.

class InventoryItem(object):
      def __init__(self, title, description, price, store_id):
         self.title = title
         self.description = description
         self.price = price
         self.store_id = store_id

      def __str__(self):
         return self.title

      def __eq__(self, other):
         if self.store_id == other.title:
             return True
         else:
             return False

      def change_description(self, description=""):
         if not description:
             description = raw_input("Please give me a description: ")
         self.description = description

      def change_price(self, price=-1):
         while price < 0:
            price = raw_input("Please give me the new price [X.XX]: ")
            try:
              price = float(price)
              break
            except:
              print "I'm sorry, but {} isn't valid.".format(price)
         self.price = price

      def change_title(self, title=""):
         if not title:
             title = raw_input("Please give me a new title: ")
         self.title = title

InventoryItem is so generic, we’d probably never use it directly. That’s okay. Its real job is to give us a good foundation to work from.

Subclassing a Class

Now that we have a base class, let’s create a new class that inherits from it. Book is a good place to start.

We tell Python that we want to inherit from another class by putting that class’s name in the parentheses where we normally put object. So, when we declare Book, we’d use class Book(InventoryItem): rather than class Book(object):.

According to our notes, Book needs an author and a format. We also need a way to change the author and the format, and we probably want to print out more than the title (after all, there are quite a few books that have the same title).

To the same file containing InventoryItem, we add the following:

class Book(InventoryItem):
      def __init__(self, title, description, price, format, author, store_id):
         super(Book, self).__init__(title=title,
             description=description,
             price=price,
             store_id=store_id)
         self.format = format
         self.author = author

      def __str__(self):
         book_line = "{title} by {author}".format(
            title=self.title,
            author=self.author)
         return book_line

      def __eq__(self, other):
         if self.title == other.title and self.author = other.author:
             return True
         else:
             return False

      def change_format(self, format):
         if not format:
             format = raw_input("Please give me the new format: ")
         self.format = format

     def change_author(self, author):
         if not author:
             author = raw_input("Please give me the new author: ")
         self.author = author

__init__() looks a bit strange. What’s super()? super() is a special function in Python. It tells Python to call a function in the parent class. This way, we don’t have to keep writing the same code over and over again.

You’ll also note that we’ve written __eq__() and __str__(), even though we’d already written them in InventoryItem. This is called “overriding a method,” and it’s incredibly common when you work with inheritance.

Why would we want to override the class that compares two books? Think about the books in a bookstore: Titles aren’t always unique. Although many authors try their hardest to come up with a unique title for their books, collisions will still occur. To be safe, it’s best to check the author name as well to make certain that two books aren’t the same.

The same logic applies to __str__(). With books, you’re often interested in the title and author, so if you want to print out a book object, you should probably return both attributes. Formatting makes it easier to understand who the author is, because many book titles contain a person’s name.

Using the Classes

Now that we have the classes set up, let’s use them! They’re in a file, so we’ll have to import them into our shell to play with them (we’ll talk about importing more in the next two hours). Let’s say they’re in a file called bookstore.py. In IDLE, open that file and then select Run Module under the Run menu. A shell will pop up with your classes already imported!

Let’s make some books:

>>> hamlet = Book(title="Hamlet",
...  description="A Dane has a bad time.",
...  price=5.99, format="paperback",
...   store_id="29382918",
...   author="William Shakespeare")
>>> hamlet_hardback = Book(title="Hamlet",
...  description="A Dane has a bad time.",
...  price=10.99,
...  format="hardback",
...  store_id="3894083920",
...  author="William Shakespeare")
>>> macbeth = Book(title="Macbeth",
...  description="Don't listen to strange ladies on the side of the road.",
...  price=4.99, format="paperback",
...  store_id="23928932",
...  author="William Shakespeare")

Once we have the book set up, we can play with all of the functions we created:

>>> hamlet == hamlet_hardback
True
>>> hamlet == macbeth
False
>>> print hamlet
Hamlet by William Shakespeare
>>> hamlet.change_description()
Please give me a description: The trouble with remarriage.
>>> print hamlet.description
The trouble with remarriage.
>>> macbeth.change_format(format="audiobook")
>>> macbeth.format
'audiobook'

As you can see, we can change any of the book attributes, create new book objects, compare them, and print them out. We inherited change_description() from InventoryItem, so there was no need to rewrite that. Our overwritten functions (__str__() and __eq__()) work just fine, as does one of our new functions (change_format).

When to Expand Classes in the Real World

The manager of our sample restaurant has had increasing interest in adding catering to his business. Some of his competitors have started offering it and appear to be having some success. He looks at the beginnings of his menu program, specifically his MenuItem object. Will he have to rewrite it?

class MenuItem(object):

  def __init__(self, title, cost, long_desc = '', short_desc = '', item_
  type='main'):
    self.title = title
    self.cost = cost
    self.long_desc = long_desc
    self.short_desc = short_desc
    self.item_type = item_type

  def change_item_type(self, item_type):
    self.item_type = item_type

  def change_cost(self, cost):
    self.cost = cost

  def change_description(self, long_desc='', short_desc=''):
    if long_desc:
      self.long_desc = long_desc
    if short_desc:
      self.short_desc = short_desc

  def change_title(self, title):
    self.title = title

  def print_item(self, desc_type='short'):
    print "{title} ... ${cost}".format(title=self.title, cost=self.cost)
    if desc_type == 'short':
      print self.short_desc
    else:
      print self.long_desc

He thinks about how a catering dish is different. It still needs a title and a description. It obviously needs to cost something. It’s going to have a type.

He realizes the main thing that’s different is that it’s going to feed more than one person. The dishes he serves in house only feed one person or, in the case of appetizers, one table. Also, a customer may need to care for the dish in a certain way. Maybe it requires customers to cook it themselves, or maybe it has to be kept cool.

Rather than rewrite his MenuItem object, he decides to create a new class, just for catering items:

class CateringItem(MenuItem):

  def __init__(self, title, cost, number_serves,
               special_instr='', long_desc='', short_desc='', item_type='main',):
    super(MenuItem, self).__init__()
    self.title = title
    self.cost = cost
    self.special_instr = special_instr
    self.long_desc = long_desc
    self.short_desc = short_desc
    self.item_type = item_type
    self.number_serves = number_serves
    self.special_instr = special_instr

  def print_item(self, desc_type='short'):
    print "{title} ... ${cost}".format(title=self.title, cost=self.cost)
    print "Serves: ", self.number_serves
    if desc_type == 'short':
      print self.short_desc
    elif desc_type == 'long':
      print self.long_desc
    if self.special_instr:
      print "Special instructions: ", self.special_instr

Now, rather than having to add values to his already pretty big MenuItem class that wouldn’t be used most of the time, he can write one with just the extra functionality and features he needs.

Summary

During this hour, you learned how to compare objects from custom classes, as well as how to get Python to print your custom objects. You used inheritance to make new classes, and you learned how to override existing functions from parent classes.

Q&A

Q. What other default methods can I override?

A. There are quite a few default methods you can choose to override. All mathematical operators can be overridden, so if you want someone to be able to add or subtract with your object, you can use one of these. You can also make it so a user can convert your custom data type into another one of Python’s data types using functions such as int() and str(). An exhaustive list can be found at http://docs.python.org/2/reference/datamodel.html.

Q. Can I make a class method without passing self in?

A. Technically, you can. You would need to put @staticmethod before the method definition, so Python knows that self doesn’t need to be passed to this function. It would look like this:

  class name(object):

@staticmethod
def some_method():
    code

Workshop

The Workshop contains quiz questions and exercises to help you solidify your understanding of the material covered. Try to answer all questions before looking at the answers that follow.

Quiz

1. How do you customize what is returned when you print an instance of a class?

2. When you override a comparison function, what is stored into self, and what is sent as an argument?

Answers

1. The __str__() function needs to be overridden. It must return a string.

2. The item on the left side of the operator is sent in as self, whereas the argument on the right side of the operator is sent as an argument.

Exercise

Now that we have the Book class written, write a class for any software the bookstore might sell. Here’s the list of requirements:

Image An operating system

Image An ERSB rating (E, T, M, and so on)

Image A function to change the operating system

Image A function to change the rating

Don’t forget to override the comparison and string functions, too!

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

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