A class is a way of structuring a related collection of data about some object in a single place. You can think of it as a template for creating new instances of a type, where the class itself defines certain attributes that each of those instances must have. For example, we can think of the Person class as a template for describing people, where each Person must have a name, a birthdate, and some other relevant data about them. It's as though we've defined a new type. In the same way that all dictionaries have keys and all lists have a length, imagine that all Person objects have a name.
Classes consist of two main forms of information. The value of the data inside the class is stored in attributes, and the accessors for that information is through the class functions (also called methods). The attributes are variables that are often defined for each instance of the class, like a person's name. The person instance that represented me would have a name attribute set to "Alexander Coder". The class functions provide a convenient abstraction for accessing data stored in the attributes.
In the previous chapter, we talked about the datetime object inside of the datetime namespace. The datetime object is an instance of a class that manages a date. Dates are actually reasonably complicated data structures, consisting of a year, a month, a day, hours, minutes, seconds, possibly a time zone, and other possible values. Storing all of this data in a string or in a dictionary could get a little unwieldy, so the authors of the datetime module decided to create a new object to store and manage datetime objects. When you call the now function on the datetime object, you get a new datetime instance that holds all of the data about the instant in time when now was called.
The keyword for setting up a new class definition in Python is class. You use the class keyword along with a name for the class template. Any class functions or attributes are indented as if you'd written a function declaration, and these functions exist in the context of the class template. For example, to define a contact class template for our address book, we can write the following code:
class Contact:
def setName(self, name):
self.name = name
def getName(self):
return self.name
The class statement says that we're defining a new class template, which is essentially a new data type called Contact. There are two class functions that we've defined that are responsible for getting and setting one of the class attributes. The self.name variable is a class attribute, suggesting that each instance of the Contact data type will have a name attribute. Each contact will probably have a different name (although they are not required to), and we can either find out what that name is or set the name using the functions defined for the class.
You'll note that each of the functions is declared with a variable called self in the first position of the function parameter list. In a class, each function needs to be aware of the object that it belongs to so that it can make changes or retrieve data for the correct instance. An example of this is that if you and I are both contacts, we both have a getName function. If I ask you your name, you probably have a different answer than I do. I wouldn't want you to be giving my name as the answer, and you probably don't want that yourself. When someone asks you what your name is, you're aware of yourself, just as these functions must be aware of the parent object who they're being called for. By providing the self variable, the function can be aware of the current instance of the object, and can retrieve the name variable or set name appropriately.
To create new instances of Contact, set up a variable where you'd like to store the new reference, and call the class constructor. The constructor is used to set up a brand new instance of the class. By default, it just gives you an empty Contact object. You access it by calling a function matching the name of the class itself.
c = Contact()
c.setName("Alexander")
print(c.getName())
Alexander
The Contact() constructor is automatically created when you define a new class, and right off the bat will give you a completely empty data object. You can call the setName function on the c object, just like you'd call find on a string or append on a list. These are functions that exist for the class, and are available for you to use on each instance of the new class. When you print the name of the c instance, it gives you the expected result, since we've just set the c instance's name variable to "Alexander".
Each instance of the class exists in its own space. If you create multiple contacts, you can set the name of one by calling the class function. Even though this function has been defined in a single spot, the reference to self in the function parameter list ensures that one contact has no implicit knowledge of another, and will only make changes to itself, as you'd expect.
c1 = Contact()
c2 = Contact()
c1.setName("Alexander")
c2.setName("Jennifer")
print("c1's name: {0}".format(c1.getName()))
print("c2's name: {0}".format(c2.getName()))
c1's name: Alexander
c2's name: Jennifer
Everything looks the same for the c1 and c2 instances of Contact, except for the name value. Changing the name in c2 didn't modify the name in c1, and each contact retains its own set of information.
A similar analogy using dictionaries is the following piece of code:
c1 = {}
c2 = {}
c1["name"] = "Alexander"
c2["name"] = "Jennifer"
print("c1's name: {0}".format(c1["name"]))
print("c2's name: {0}".format(c2["name"]))
c1's name: Alexander
c2's name: Jennifer
The empty dictionary acts as a constructor for the dictionary class. Assigning a key and referencing that key by index are the equivalent getter and setter class functions. The data types work in a similar way, storing values and allowing other program code to retrieve and set data as necessary.
What if the name hasn't been set yet? When a Contact instance is created, the self.name parameter hasn't been defined. Contacts don't have names until we call setName. Let's look at how both dictionaries and classes handle this problem.
c1 = {}
print("c1's name: {0}".format(c1["name"]))
Traceback (most recent call last):
File "C:\Python33\sandbox.py", line 23, in <module>
print("c1's name: {0}".format(c1["name"]))
KeyError: 'name'
c2 = Contact()
print("c2's name: {0}".format(c2.getName()))
Traceback (most recent call last):
File "C:\Python33\sandbox.py", line 26, in <module>
print("c2's name: {0}".format(c2.getName()))
File "C:\Python33\sandbox.py", line 6, in getName
return self.name
AttributeError: 'Contact' object has no attribute 'name'
In each of the cases, an error occurs due to the absence of a name attribute (also known as a key in the dictionary). One way to handle this situation is to force a default value for a contact's name. We can use the empty string to prevent a large set of "John Doe" or "Jane Doe" contacts from hanging around in our address book. If the contact doesn't have a name, they'll still have a name attribute, but it will be empty. In a dictionary, we might use the following code to take care of this situation:
c1 = {"name": ""}
print("c1's name: {0}".format(c1["name"]))
c1's name:
For a dictionary, this might be better for your code than throwing an exception. We have some additional help with classes. The getName function actually allows us to check whether or not the name is empty, and to run additional code if it is. To allow this to happen, we'll need to set the self.name attribute to the empty string, just like the dictionary did in its constructor. But where is the constructor defined in a class?
Previously, we'd said that the constructor was implicitly defined when you wrote your class definition. If you don't provide an actual constructor, Python inherits a very simple one that just returns an empty instance of your new data type. If you want some additional functionality like the explicit definition of class attributes, you can define the __init__ method.
class Contact:
def __init__(self):
self.name = ""
def setName(self, name):
self.name = name
def getName(self):
return self.name
c1 = Contact()
print("c1's name: {0}".format(c1.getName()))
c1's name:
The __init__ function is the constructor method, and is actually executed when you call Contact() in the creation of c1. In the __init__ method, we also have the reference to self. This can be used to give self.name an initial value. Once that's done, any calls to getName will be fine, just like in the dictionary case where the name key received the empty string value.
With the self.name value defined, we can extend the setName function to check whether or not name has actually been set. If it hasn't, throw up a warning message to the user to let them know something strange is going on. Remember that exceptions are a valuable way to identify a severe problem, but if you just want your program to handle less dangerous cases, like forgetting someone's name in your contact list, you can take an approach like this to gracefully handling an absence of values.
def getName(self):
if self.name == "":
print("WARNING: This contact doesn't have a name yet!")
return self.name
WARNING: This contact doesn't have a name yet!
c1's name:
It is generally a good idea to explicitly define the constructor, if only to ensure that your default attributes are initialized properly. Let's use this idea to build a random number generator for a certain range, based on the earlier code we developed in previous chapters.
import random
class RandomGenerator:
def __init__(self):
self.min_number = 0
self.max_number = 100
def getMinNumber(self):
return self.min_number
def getMaxNumber(self):
return self.max_number
def setMinNumber(self, min_number):
self.min_number = min_number
def setMaxNumber(self, max_number):
self.max_number = max_number
def getRandom(self):
return random.randint(self.min_number, self.max_number)
random_generator = RandomGenerator()
print("Default range for random generator:")
for i in range(5):
print(random_generator.getRandom())
print("Constrained range for random generator:")
random_generator.setMinNumber(25)
random_generator.setMaxNumber(30)
for i in range(5):
print(random_generator.getRandom())
Default range for random generator:
13
95
95
17
3
Constrained range for random generator:
26
26
28
28
27
You might ask why it would be necessary to set up a random number generator in this way, when we could just reference the random module and make direct calls to randint. The paradigm of a class is a template for instances with similar purposes but different parameters. Instead of calling this a random generator, we could have just called it DiceClass, and set up each instance of the class as a dice roll. For example,
import random
class DiceClass:
def __init__(self, min_number=0, max_number=100):
self.min_number = min_number
self.max_number = max_number
def getMinNumber(self):
return self.min_number
def getMaxNumber(self):
return self.max_number
def setMinNumber(self, min_number):
self.min_number = min_number
def setMaxNumber(self, max_number):
self.max_number = max_number
def getRoll(self):
return random.randint(self.min_number, self.max_number)
d6 = DiceClass(1, 6)
d20 = DiceClass(1, 20)
print("Rolling five 6-sided dice:")
for i in range(5):
print(d6.getRoll())
print("Rolling five 20-sided dice:")
for i in range(5):
print(d20.getRoll())
Rolling five 6-sided dice:
5
2
4
2
1
Rolling five 20-sided dice:
4
19
2
17
7
Now the idea of a class representing the concept of rolling dice is clear. Each instance of the class represents a particular type of roll.
First, let's extend the Contact class to encompass the type of data we're going to try to store for each contact. It would be nice to have a get and a set method for each of the attributes and a constructor for the class to set up the defaults. In addition to this, we can actually build a method into the class to tell it how to handle getting called by a print statement. We can actually configure the class to print out values in a way that we specify. Let's look at some code.
class Contact:
def __init__(self, first_name="", last_name="", phone=None, cell_phone=None, town=""):
self.first_name = first_name
self.last_name = last_name
self.phone = phone
self.cell_phone = cell_phone
self.town = town
def getFirstName(self):
return self.first_name
def setFirstName(self, first_name):
self.first_name = first_name
def getLastName(self):
return self.last_name
def setLastName(self, last_name):
self.last_name = last_name
def getPhone(self):
return self.phone
def setPhone(self, phone):
self.phone = phone
def getCellPhone(self):
return self.cell_phone
def setCellPhone(self, cell_phone):
self.cell_phone = cell_phone
def getTown(self):
return self.town
def setTown(self, town):
self.town = town
def __str__(self):
st = "{0} {1}".format(self.first_name, self.last_name)
if self.phone is not None:
st = "{0}\nPhone: {1}".format(st, self.phone)
if self.cell_phone is not None:
st = "{0}\nCell: {1}".format(st, self.cell_phone)
if self.town != "":
st = "{0}\nTown: {1}".format(st, self.town)
return st
c1 = Contact(first_name="Alexander", last_name="Coder")
c1.setPhone("5551763")
c1.setTown("Kingston")
print("c1: {0}".format(c1))
c2 = Contact(first_name="Michael", cell_phone=5559955)
print("c2: {0}".format(c2))
c1: Alexander Coder
Phone: 5551763
Town: Kingston
c2: Michael
Cell: 5559955
Instead of using dictionaries with keys that may or may not exist, we can be relatively certain that all the attributes we're looking for are at least defined for each instance of the Contact class. The constructor accepts all of the keys we were concerned about, and if they aren't provided when the class is instantiated, they're set to a safe default parameter. If they are given, the class instance sets its own attribute to the appropriate value. For each of the attributes, a get and a set method are provided. If we wanted to add some error checking, or some additional formatting, we could modify the get and set methods to handle that.
A new method is __str__, which accepts only self as a parameter and returns a string. The __str__ method is called whenever you try to get a string representation of the class, like you'd do when printing, or when using the str function to do a type conversion. You can test this at the interpreter prompt, like so.
>>> str(c1)
'Alexander Coder\nPhone: 5551763\nTown: Kingston'
>>> c1.__str__()
'Alexander Coder\nPhone: 5551763\nTown: Kingston'
We need to make sure that __str__ returns a string instead of printing values to the screen. In the Contact example, we start by building a string variable called st using the first and last names stored in the contact. Each of the other attributes are added to the string if and only if they are not set to the default values. This saves us from having to build the string in any other application that uses Contact. We tell the class how to present itself, and save other programmers (including ourself!) from having to rewrite this at a later time.
It is also possible to write __str__ and other internal functions by using the get methods instead of calling the attributes directly. For example, we could have built __str__ like this:
def __str__(self):
st = "{0} {1}".format(self.getFirstName(), self.getLastName())
if self.getPhone() is not None:
st = "{0}\nPhone: {1}".format(st, self.getPhone())
if self.getCellPhone() is not None:
st = "{0}\nCell: {1}".format(st, self.getCellPhone())
if self.getTown() != "":
st = "{0}\nTown: {1}".format(st, self.getTown())
return st
One reason that we might want to take this approach instead is that if the attributes are held in an intermediate format, such as storing phone numbers as integers, we might want the get function to automatically format the result for us. Let's naively assume that the phone number is always going to be a seven digit number for the moment. A rewrite of getPhone could insert a hyphen for a typical North American phone number in the xxx-xxxx format.
def getPhone(self):
if self.phone is None:
return None
st = str(self.phone)
return '{0}-{1}'.format(st[0:3], st[3:])
c1: Alexander Coder
Phone: 555-1763
Town: Kingston
Now that the __str__ function has been updated, it automatically gains the added formatting from the getPhone class function. If we'd just referenced self.phone on its own, it would have printed as a number.
How does the rewrite from contacts as dictionaries to classes affect contacts_obj? For starters, we can keep contacts_obj as a dictionary. We use the key lookup of a dictionary as a convenient way to retrieve individual contacts, so for the moment, there's no need to change that. Each value in the dictionary is going to be an instance of the Contact class instead of a dictionary, so we'll call the constructor for each of the contacts we want to keep track of.
contacts_obj = {
"alexander": {"first_name": "Alexander",
"last_name": "Coder",
"phone": 5551763,
"town": "Kingston"},
"mike": {"first_name": "Michael",
"cell_phone": 5559955},
"laney": {"first_name": "Elaine",
"last_name": "Benes"},
"doc": {"first_name": "Tobias",
"town": "Newport Beach"},
}
contacts_obj = {
"alexander": Contact(first_name = "Alexander",
last_name = "Coder",
phone = 5551763,
town = "Kingston"),
"mike": Contact(first_name = "Michael",
cell_phone = 5559955),
"laney": Contact(first_name = "Elaine",
last_name = "Benes"),
"doc": Contact(first_name = "Tobias",
town = "Newport Beach"),
}
This should look very similar. In fact, each of the contacts actually has all of the attributes in the contact implicitly now, even though they are set to empty strings or None values. The print statement has been defined for the Contact class, allowing you to print any of the contacts in contacts_obj easily, as opposed to dealing with a dictionary print.
for key in contacts_obj:
print("Key: {0}, printing contacts_obj[{0}]".format(key))
print(contacts_obj[key])
Key: doc, printing contacts_obj[doc]
Tobias
Town: Newport Beach
Key: mike, printing contacts_obj[mike]
Michael
Cell: 555-9955
Key: alexander, printing contacts_obj[alexander]
Alexander Coder
Phone: 555-1763
Town: Kingston
Key: laney, printing contacts_obj[laney]
Elaine Benes
Now that we have the new data structure, let's rebuild the old sample application to work with the Contact class instead of dictionaries. It's a simple change, but it will allow you to observe the shifting of responsibility from the program to the class, allowing you to let the class handle how it should be displayed just like how lists and dictionaries already do it. Instead of printing each element of a list one-by-one, you just print the list. With the Contact class, you now have this same power.
nickname = input("Enter the contact you would like to see: ")
if nickname in contacts_obj:
print("Contact: {0}".format(nickname))
contact = contacts_obj[nickname]
for key in contact:
print(" {0}: {1}".format(key, contact[key]))
else:
print("Sorry, I don't know {0}!".format(nickname))
nickname = input("Enter the contact you would like to see: ")
if nickname in contacts_obj:
print("Contact: {0}".format(nickname))
print(contacts_obj[nickname])
else:
print("Sorry, I don't know {0}!".format(nickname))
Enter the contact you would like to see: alexander
Contact: alexander
Alexander Coder
Phone: 555-1763
Town: Kingston
It's easy to take a pair of integers and to compare them against one another. We even wrote a new max function to simulate the behaviour of the built-in max function. Even strings are comparable using the alpha-numeric ordering of characters. The letter "a" is less than the letter "b" because it occurs earlier in the alphabet.
This idea doesn't naturally carry over to data structures like lists or dictionaries. How can you authoritatively state that one list is less than another? Do you use the length of the list, or the value of the first element? What if that first element is a list? Are all lists of the same length equal to one another? The same rules apply to dictionaries; how can you state with confidence that one dictionary is less than another? Well, in Python, lists and dictionaries are unorderable types. All you can do is define an equality test for them, stating that a dictionary with the same keys and values as another dictionary is equal to the second one. It is unclear how you would use less than or greater then.
With classes, if you believe you can provide an ordering for the instances of the class, you can define some built-in comparison functions that are used when you use operators like less than or greater than. This provides you with a way to sort your instances, to order them in a meaningful way, and to otherwise treat your class instances as if they were a native type in Python like strings or numbers.
class.__lt__(self, other) Less-than <
class.__le__(self, other) Less-than-or-equal <=
class.__eq__(self, other) Equal ==
class.__ne__(self, other) Not-equal !=
class.__gt__(self, other) Greater-than >
class.__ge__(self, other) Greater-than-or-equal >=
When you evaluate an expression like c1 > c2, where c1 and c2 are instances of the Contact class, what you are doing is calling c1.__gt__(c2). Python provides the operator as shorthand for this action so that you don't have to write everything in terms of class operations. To gain access to this shorthand, you have to fill in these methods manually when writing your class. Without these methods, you get an error like the following:
>>> c1 = Contact()
>>> c2 = Contact()
>>> c1 > c2
Traceback (most recent call last):
File "<pyshell#25>", line 1, in <module>
c1 > c2
TypeError: unorderable types: Contact() > Contact()
Let's start with the greater-than method though. For the Contact class, let's assume that we can just use the contact's name as a sortable attribute. It would be just as easy to use the phone number, or some other attribute, but the name seems like the best way to identify a single contact. Head over to the Contact code and add the following two functions.
def getFullName(self):
return "{0} {1}".format(self.first_name, self.last_name)
def __gt__(self, other):
return self.getFullName() > other.getFullName()
The getFullName function acts as a helper for us by concatenating the first and last names together. We could do that in each of the comparison functions, but a helper function in this way makes it easier to read the code, and allows us to write less code overall. The __gt__ function is invoked whenever the greater-than operator is used. We can test with the objects inside contacts_obj from earlier.
>>> contacts_obj["alexander"] > contacts_obj["mike"]
True
>>> contacts_obj["laney"] > contacts_obj["mike"]
False
Now we have a way to directly compare instances of the class against one another as if they were a native Python data type. This can be extended to the other comparison methods with code similar to the following:
def __lt__(self, other):
return self.getFullName() < other.getFullName()
def __le__(self, other):
return self.getFullName() <= other.getFullName()
def __eq__(self, other):
return self.getFullName() == other.getFullName()
def __ne__(self, other):
return self.getFullName() != other.getFullName()
def __gt__(self, other):
return self.getFullName() > other.getFullName()
def __ge__(self, other):
return self.getFullName() >= other.getFullName()
In this way, the class has defined itself as a sortable data structure. We can now take advantage of this by using the built-in sorting methods. For example, consider the dictionary of contacts stored in contacts_obj. If this was a list, and we disregarded the nickname (or better yet, stored it as an attribute in the class itself), we could obtain sorted lists of contacts by defining the comparison functions and just relying on the ability of sorted to handle the data natively.
contacts_obj = [
Contact(first_name = "Alexander",
last_name = "Coder",
phone = 5551763,
town = "Kingston"),
Contact(first_name = "Michael",
cell_phone = 5559955),
Contact(first_name = "Elaine",
last_name = "Benes"),
Contact(first_name = "Tobias",
town = "Newport Beach"),
]
for contact in contacts_obj:
print(contact)
Alexander Coder
Phone: 555-1763
Town: Kingston
Michael
Cell: 555-9955
Elaine Benes
Tobias
Town: Newport Beach
contacts_obj.sort()
for contact in contacts_obj:
print(contact)
Elaine Benes
Michael
Cell: 555-9955
Alexander Coder
Phone: 555-1763
Town: Kingston
Tobias
Town: Newport Beach
Classes exist as part of a programming model called Object Oriented Programming. In this model, everything is considered to be an object. Integers are just objects that happen to also be numbers, strings are objects that are collections of letters, and Contact instances are a few strings and a number tied together in a single object. This gives a hierarchy of structure, and allows for Python to say that variable assignment is the act of assigning an object to a variable.
This hierarchy means that all the data in a Python program is represented by objects, or by the relation between objects. Each of these objects has an identity, a type, and a value. In the case of an instance of the Contact class, let's examine what each of these three features of the actual object refers to. The instance itself is the identity of the object; calling the constructor returns a reference to a new object, and that reference points to the new object that's been created. The type of the object is Contact, and relates to the class definition that we created. The value of the object consists of the internal state of the attributes, and will include a first name, last name, phone number, cell phone number, and a town.
Let's start with a blank object.
>>> x = object()
>>> x
<object object at 0x029E94B8>
>>> print(x)
<object object at 0x029E94B8>
>>> type(x)
<class 'object'>
So what use is this to us? We get a meaningless memory address (that's what 0x029E94B8 means), and we get told that the x variable has type object. There aren't any attributes to add data to. However, this is perfect, because it can be used as a building block. If all data is an object, variables don't need to have explicit types. For example, we don't need to say that x is an int at the time when x is defined. We just say that it's a variable, and since a variable holds an object and int values are stored as objects, x can hold an int.
When we say that int classes inherit from object classes, we mean that the int class takes all of the properties of the object class, and in addition to that information, adds its own specialized data such as the integer value it stores. There is a special function that can be used to examine the parent of a class. If we look at the int class and the object class using the issubclass function, we can observe whether or not int classes derive from object classes.
>>> x = int()
>>> x
0
>>> type(x)
<class 'int'>
>>> issubclass(type(x), object)
True
>>> issubclass(int, object)
True
The last lines explain that int has a base-class (parent) called object. We know that object is at the top of the Python class hierarchy, and can verify this by also using the __bases__ keyword.
>>> int.__bases__
(<class 'object'>,)
>>> object.__bases__
()
An empty tuple indicates that the object class inherits from nobody, and must therefore be at the top of the food chain. It is the upper-most class in the type hierarchy. All classes in Python inherit from it in some way.
If a class inherited from something other than object, what would happen? The new class would still be a child of the object class, although a distant one. Primarily, it would be a child of the class it inherited from directly. Let's build a new class to extend int but add no new functionality aside from a new dialog in __str__.
class NewInt(int):
def __str__(self):
return "NewInt: {0}".format(super().__str__())
x = NewInt(5)
print(x)
NewInt: 5
To understand this, we need to know a few pieces of information. First, the only thing we're going to change in our new class is the __str__ method. Every other aspect of a NewInt variable should act just like an int. We want to maintain addition, division, equality, and all that other great stuff that we already get for free. We just want to have a new class that prints out NewInt before writing it to the screen. Next, we need to understand that the int class already has a perfectly good __str__ method, except for the fact that it doesn't print our custom message. We can access the parent's methods using the super() method. It returns an instance of the parent class, which in this case is int, and allows us to call int's __str__ method with our instance's data. We haven't made any changes to the way that int operates. Finally, when the class is defined using class NewInt(int), having int inside the parentheses suggests that we would like to inherit existing properties from int. By default, do everything that int does, unless we say otherwise.
How does this change the type inheritance? Are NewInt instances still objects? Are they descendants of the int class, and not object? Or both? Let's use issubclass to answer these questions.
>>> issubclass(int, object)
True
>>> issubclass(NewInt, int)
True
>>> issubclass(NewInt, object)
True
>>> issubclass(int, NewInt)
False
These results tell us that int derives from object, that NewInt derives from int, that NewInt also derives from object (through its relation to int), and that int does not derive from NewInt. These inheritances are not backwards compatible, so nothing about int has to change for NewInt to inherit qualities from it. With that in mind, NewInt instances can act just like int instances, except for the way they convert to a string. Every call to a NewInt function is passed right through to the int class transparently thanks to the object oriented structure of the language.
When we looked at exceptions in the error handling portion of the code, we saw a number of error messages. They had names like ValueError, TypeError, or ZeroDivisionError, and they all had a very similar format. In Python, exceptions are also objects. More importantly, there is an Exception class in Python. While every individual exception is an object, every exception derives from the Exception class.
Consider the __str__ function that we wrote for the Contact class. Python knows that every object has a __str__ function, regardless of its type. If we don't explicitly write a __str__ function, Python uses the object's default __str__. By virtue of creating a class that implicitly inherits information from the basic object, we get a __str__ function for free because the parent class has one. It's like saying that all objects can be printed to the screen, and even if you don't tell me specifically how you'd like to look when you're printed, I can still make an attempt.
Exceptions provide a great example of this. When Python hits an error in the code like a division by zero, it wants to raise an exception so that you can catch it. Without specific information about the type of error that occurs, we might not know if it was a division by zero, or if we were trying to divide by a string instead of a number. With the diverse set of exceptions that can be returned, the programmer gets more information about the error case. This is an important point: even though we might see a TypeError or a ZeroDivisionError, these errors are specialized instances of the Exception class.
>>> issubclass(Exception, object)
True
>>> issubclass(ValueError, Exception)
True
>>> issubclass(ValueError, object)
True
Python knows that when an error occurs, it has to send an instance of the Exception class upwards through the code to be caught. For this reason, ValueError and TypeError are classes that inherit the properties of the Exception class.
This knowledge can be used to build unique exceptions that fit the type of programming error we'd like to represent. Let's go back and revisit the getNumber function from the modules chapter.
import math
class NumberTooLowError(Exception):
pass
def getNumber(minimum_value=False, input_text="Enter a number: "):
done = False
while not done:
try:
user_number = int(input(input_text))
except ValueError:
print("You didn't enter a number! Shame on you.")
continue
if minimum_value == False or user_number >= minimum_value:
done = True
else:
raise NumberTooLowError("Your number was too low! The minimum value is {0}.".format(minimum_value))
return user_number
getNumber(minimum_value=5)
Enter a number: 3
Traceback (most recent call last):
File "C:\Python33\sandbox.py", line 20, in <module>
getNumber(minimum_value=5)
File "C:\Python33\sandbox.py", line 17, in getNumber
raise NumberTooLowError("Your number was too low! The minimum value is {0}.".format(minimum_value))
NumberTooLowError: Your number was too low! The minimum value is 5.
Instead of hitting a TypeError or ValueError, we've now hit a custom error type with a custom error message. If we didn't want to loop inside the getNumber method but would rather throw an exception relevant to the situation, we can define a class like NumberTooLowError that inherits from Exception and changes nothing. Once the particular error case arises, we raise a NumberTooLowError with an appropriate message, and the warning gets sent through the code. As we'd expect, this new exception inherits from both the object and the Exception classes.
>>> issubclass(NumberTooLowError, object)
True
>>> issubclass(NumberTooLowError, Exception)
True
If we'd omitted the inheritance specifier in the class definition, Python would have complained.
class NumberTooLowError:
pass
Enter a number: 3
Traceback (most recent call last):
File "C:\Python33\sandbox.py", line 20, in <module>
getNumber(minimum_value=5)
File "C:\Python33\sandbox.py", line 17, in getNumber
raise NumberTooLowError("Your number was too low! The minimum value is {0}.".format(minimum_value))
TypeError: object.__new__() takes no parameters
There's an easy mistake to make when using the __str__ function in a class. Take a look at this code and see if you can catch it:
class StrExample:
def __init__(self, x):
self.x = x
def __str__(self):
print(self.x)
s = StrExample(5)
print("__str__: {0}".format(s.__str__()))
print("s: {0}".format(s))
The class is simple enough. All it aims to do is to store the value that it's initialized with, and to print out the value when the __str__ function is invoked. The code that follows creates an instance of the class with the value 5, then tests the difference between calling __str__ directly and then explicitly. So what output do we get in this case?
5
__str__: None
5
Traceback (most recent call last):
File "sample.py", line 9, in <module>
print("s: {0}".format(s))
TypeError: __str__ returned non-string (type NoneType)
Did you catch that? I'm still guilty of messing that up every once in a while.
The __str__ function, although used to print values most of the time, is designed to return a string, not to print one. In our class, the __str__ function calls print directly and returns nothing. That explains the output. Let's step through the first part.
The first line of the output comes from the call to s.__str__() in the first formatted string. Before our formatted string is sent to the screen, the print statement in s.__str__() is called, and the value 5 is printed to the screen. Next, our formatted string is built, and since our function returns nothing, the None value is used.
How do we solve this? Instead of calling print(self.x) in __str__, use return self.x.
Every piece of data in Python is an object. Generally this is a specialized type of object, but at the core, each and every piece of data inherits from the same parent type. This generalization is the reason that variable assignment is so flexible, why each and every object can be converted into a string to be printed, and why equality tests can be made even across types.
Classes offer a stronger way of describing your values with some accompanying behaviour. Contacts can be sorted by their entire set of data, and all of the information about a single person can be stored in a single data structure that has knowledge about itself and how it should be treated in very general conditions.
Understanding the hierarchy of types in Python can allow a programmer to customize classes for very specific purposes. If an existing data type performs a very relevant task, but only requires a very slight change to fit the job perfectly, it may be a good idea to inherit from an existing class and to modify the subtle differences needed to complete the task.
1) Build a wrapper around the string class called MyString. Construct the comparison methods to automatically check whether or not two MyString instances are equal based strictly on the lower-case representation of the strings. You don't need to inherit from the string class for this task, just complete the following template:
class MyString:
def __eq__(self, other):
# your code here
2) Make a new class called FamilyContact that inherits from the Contact class we wrote earlier. Add a new string class parameter called relationship that can store a value like "Mother" or "Uncle". See how Contact and FamilyContact instances compare against one another using the existing equality tests defined in the Contact class.