What You’ll Learn in This Hour:
How to use built-in extras with classes
How to use class inheritance
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.
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.
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!
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.
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
.
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)
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.
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).
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.
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.
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!
>>> 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
).
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.
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. 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
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.
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?
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.
Now that we have the Book
class written, write a class for any software the bookstore might sell. Here’s the list of requirements:
An operating system
An ERSB rating (E, T, M, and so on)
A function to change the operating system
A function to change the rating
Don’t forget to override the comparison and string functions, too!
3.15.220.16