Class Intermediate: Inheritance

A place where you can post Python-related tutorials you made yourself, or links to tutorials made by others.

Class Intermediate: Inheritance

Postby ichabod801 » Sat Mar 23, 2013 2:46 pm

Be sure you understand the concepts in Class Basics, before reading this tutorial. I will also be using a few ideas from Class Intermediate: Operator Overloading, so it can't hurt to be familiar with those as well.

We've seen how we can use classes to create our own specialized objects, with whatever attributes and methods we want. But often you don't just want to create one type of object, you want to create a group of objects that share some similarities. Think of the various containers in Python: lists, dicts, tuples, sets. They all share certain things (they can all be used with len()), but they have differences as well.

For this tutorial, we'll consider a situation where you want to create a program for handling empoyees at a particular business. They have three kinds of employees:

* Managers, who have an office, a phone number, and a salary.
* Technical Staff, who have a cubicle, a phone number, and an hourly wage.
* Field Investigators, who have a phone number and an hourly wage.

Now, we could do it all in one class:

Code: Select all
class AnyEmployee(object):
   
   def __init__(self, grade, title, first, last, location, phone, pay):
      self.grade = grade
      self.title = title
      self.name = (last, first)
      self.location = location
      self.phone = phone
      self.pay = pay
      
   def contact_info(self):
      text = ', '.join(self.name) + ' ('
      if self.location:
         text += self.location + ', '
      text += self.phone + ')'
      return text
   
   def weekly_pay(self, hours = 0):
      if self.grade == 'Manager':
         return round(self.pay / 52, 2)
      else:
         week = self.pay * hours
         if hours > 40:
            week += self.pay * 0.5 * (hours - 40)
         return week


You might wonder what's wrong with that. It only takes a couple if statements to handle the different cases. Indeed, we could define a few employees:

Code: Select all
bob = AnyEmployee('Manager', 'Director of Analysis', "Bob", 'Dobbs', '801', 'x0888', 130130)
craig = AnyEmployee('Staff', 'Statistician', 'Craig', "O'Brien", '802-18', 'x7666', 25)
ronsarde = AnyEmployee('Investigator', 'Inspector', 'Sebastian', 'Ronsarde', None, '123-4567', 18)


and get the output we're looking for:

Code: Select all
>>> bob.contact_info()
'Dobbs, Bob (801, x0888)'
>>> ronsarde.contact_info()
'Ronsarde, Sebastian (123-4567)'
>>> bob.weekly_pay()
2502.5
>>> craig.weekly_pay(45)
1187.5


Indeed, in this case it really isn't a problem. But this is just a simple case made up for a tutorial. In real life the situation gets more complicated. You may have a dozen different characteristics of your classes that need to interact in a dozen different ways. Instead of a confusing mass of conditional statements and empty variables, it works better to break the parts out into different classes that combine through inheritance.

So what is inheritance, and how does it work? To start with, let's simplify our employee class to cover just what is covered by the typical employees:

Code: Select all
class StandardEmployee(object):
   """
   The typical employee at the ACME company.
   
   The employee's name is stored in last, first format.
   
   Attributes:
   grade: The type of employee. (str)
   location: Where the employee's desk is. (str)
   name: The employee's name. (tuple of str)
   pay: The employee's salary or hourly wage. (float)
   phone: The employee's business phone number. (str)
   title: The employee's position. (str)
   
   Methods:
   contact_info: Details on how to get in touch with the employee. (str)
   weekly_pay: Determine the employee's pay for a given week. (float)
   
   Overridden Methods:
   __init__
   """
   
   def __init__(self, grade, title, first, last, location, phone, pay):
      """
      Set up the employee's basic attributes.
      
      Parameters:
      first: The employee's first name. (str)
      grade: The type of employee. (str)
      last: The employee's last name. (str)
      location: Where the employee's desk is. (str)
      pay: The employee's hourly wage. (float)
      phone: The employee's business phone number. (str)
      title: The employee's position. (str)
      """
      self.grade = grade
      self.title = title
      self.name = (last, first)
      self.location = location
      self.phone = phone
      self.pay = pay
      
   def contact_info(self):
      """
      Details on how to get in touch with the employee. (str)
      """
      text = '{}, {} ({}, {})'.format(self.name[0], self.name[1], self.location, self.phone)
      return text
   
   def weekly_pay(self, hours):
      """
      Determine the employee's pay for a given week. (float)
      
      Parameters:
      hours: How many hours the employee worked. (float)
      """
      week = self.pay * hours
      if hours > 40:
         week += self.pay * 0.5 * (hours - 40)
      return week


The StandardEmployee class has hourly pay and a location, both of which two out of three types of employees have. Of course, this means StandardEmployee is basically the same as a technical staff employee. Note that it is missing a few things: the grade of the employee, and the if clauses. That's fine, because we won't need them if we know we are creating a technical staff employee object. But what do we do about managers and field investigators? We inherit from the StandardEmployee class:

Code: Select all
class Manager(StandardEmployee):
   """
   A management employee at the ACME Company.
   
   Attributes;
   pay: The employee's salary. (float)
   
   Overridden Methods:
   weekly_pay
   """
      
   def weekly_pay(self):
      """
      Determine the employee's pay for a given week. (float)
      """
      return round(self.pay / 52, 2)
   
class Investigator(StandardEmployee):
   """
   A field investigator for the ACME Company.
   
   Overridden Methods:
   contact_info
   """
      
   def contact_info(self):
      """
      Details on how to get in touch with the employee. (str)
      """
      text = '{}, {} ({})'.format(self.name[0], self.name[1], self.phone)
      return text


Note the class definitions:

Code: Select all
class Manager(StandardEmployee):
...
class Investigator(StandardEmployee):


Every class we've done so far in these tutorials has had "(object)" after the class name, but now we have "(StandardEmployee)". The thing in parentheses after the class name is the base class. The base class is the class the new class inherits from. Base classes are also sometimes called parent classes or superclasses, usually depending on who you learned object oriented programming from.

But what does that mean? Note that the Manager and Investigator classes only define one method each, and neither of them has an __init__ method. They inherit the methods they don't define from their base class. You don't define __init__ and contact_info for Manager, it gets them from StandardEmployee. Likewise Investigator gets __init__ and weekly_pay from StandardEmployee.

But they both have "Overridden Methods" listed in their doc strings. If you call the contact_info method of an Investigator instance, it will use the one defined for that class. If you call the weekly_pay method of an Investigator instance, Python won't find one in the Investigator class, so it will look at the base class (StandardEmployee) and use the one it finds there. If you call the __repr__ method of an Investigator instance, Python won't find one in either the Investigator or StandardEmployee classes, so it will use the one it find's in the base class of StandardEmployee: the object class.

If you make some new instances of our new classes:

Code: Select all
# example employees
bob2 = Manager('Director of Analysis', "Bob", 'Dobbs', '801', 'x0888', 130130)
craig2 = StandardEmployee('Statistician', 'Craig', "O'Brien", '802-18', 'x7666', 25)
ronsarde2 = Investigator('Inspector', 'Sebastian', 'Ronsarde', None, '123-4567', 18)


You can see this in action:

Code: Select all
>>> ronsarde2.contact_info()
'Ronsarde, Sebastian (123-4567)'
>>> ronsarde2.weekly_pay(40)
720
>>> repr(ronsarde2)
'<__main__.Investigator object at 0x02446990>'


It may seem kind of screwey that we have classes for managers and field investigators, but that the technical staff uses the StandardEmployee class. You could just rename it Staff, or:

Code: Select all
class Staff(StandardEmployee):
   pass


would make another class that would just inherit everything from StanardEmployee. There is a lot of theory about how powerful object oriented programming is, but in many ways it is just a good way to organize your code so that it makes sense. It keeps the code for the employees all in one place. When you need to change it for a specific kind of employee, you do so. So your solution to issues like these are going to depend on you and your program, and what helps your program make sense. We're also going to get to some more complicated solutions, but I want to explore what we have a little first.
Craig "Ichabod" O'Brien
Minimalist, buddhist, theist, and programmer
Current languages: Python, SAS, and C++
Previous serious languages: R, Java, VBA, Lisp, HyperTalk, BASIC
ichabod801
 
Posts: 93
Joined: Sat Feb 09, 2013 12:54 pm
Location: Outside Washington DC

Re: Class Intermediate: Inheritance

Postby ichabod801 » Sat Mar 23, 2013 2:48 pm

One issue we have with all of this code is that we have to provide 'None' as a location for our investigators. This is a minor point, but let's say we want to change it just to make our code cleaner. To do that we would have to override __init__ for the Investigator class. Again, this is not a big deal, since __init__ just assigns parameters to attributes. But in some of your classes you may have more complicated initialization. Repeating it defeats the purpose of code reuse, introducing potential bugs and problems updating code. The way around that is the super function:

Code: Select all
class Investigator(StandardEmployee):
   """
   A field investigator for the ACME Company.
   
   Overridden Methods:
   contact_info
   """
   
   def __init__(self, title, first, last, phone, pay):
      super(Investigator, self).__init__(title, first, last, None, phone, pay)
...


Now we don't have to pass a None location to the Investigator class, it takes care of that itself. The super function finds the method in the base class even if it's overridden in the sub-class. You can also call that method directly, with something like:

Code: Select all
      StandardEmployee.__init__(self, title, first, last, None, phone, pay)


But this is generally frowned upon. One problem with it is that if you change the base class of Investigator, you'd have to change it with every call you made to a base class method. Note that in our call to super, we pass the current class, not the base class. The super function figures out what the base class is. This is good for situations we will see later, where it is not as clear where the method we want is.

Also note that we don't pass self as a parameter to the __init__ from super, but we do when calling the one directly from the base class. The super case makes the __init__ method "bound" to the current instance, but the direct call does not.

There is one disadvantage of using different classes for each type of employee. When we had one class we could just call that class and it took care of the differences. Now we have to remember which class goes with which employee. The standard response to this is what is known as a "factory function":

Code: Select all
def make_employee(grade, title, first, last, location, phone, pay):
   """
   Create an appropriate employee instance.
   
   Parameters:
   first: The employee's first name. (str)
   grade: The type of employee. (str)
   last: The employee's last name. (str)
   location: Where the employee's desk is. (str)
   pay: The employee's salary or hourly wage. (float)
   phone: The employee's business phone number. (str)
   title: The employee's position. (str)
   """
   if grade == 'Manager':
      employee = Manager(title, first, last, location, phone, pay)
   elif grade == 'Staff':
      employee = StandardEmployee(title, first, last, location, phone, pay)
   elif grade == 'Investigator':
      employee = Investigator(title, first, last, phone, pay)
   else:
      raise ValueError('Unknown grade parameter ({})'.format(grade))
   return employee


Now we can create the instances more consistently, like we did the first time:

Code: Select all
# example employees
bob3 = make_employee('Manager', 'Director of Analysis', "Bob", 'Dobbs', '801', 'x0888', 130130)
craig3 = make_employee('Staff', 'Statistician', 'Craig', "O'Brien", '802-18', 'x7666', 25)
ronsarde3 = make_employee('Investigator', 'Inspector', 'Sebastian', 'Ronsarde', None, '123-4567', 18)


We have to pass the grade parameter again, which we had gotten rid of, but it might not be necessary. If our managers always have titles including Director, Executive, or President, and our investigator's titles always include the word Investigator; we could use the title to determine which class to use instead.
Craig "Ichabod" O'Brien
Minimalist, buddhist, theist, and programmer
Current languages: Python, SAS, and C++
Previous serious languages: R, Java, VBA, Lisp, HyperTalk, BASIC
ichabod801
 
Posts: 93
Joined: Sat Feb 09, 2013 12:54 pm
Location: Outside Washington DC

Re: Class Intermediate: Inheritance

Postby ichabod801 » Sat Mar 23, 2013 2:51 pm

So all is well and good with our employee classes. But what if our business expands, and now we have so many field investigators we want to have some managers to make them less efficient? These new field managers would have salaries, but no location. If we were to make a new sub-class of StandardEmployee, we would have to duplicate the methods that Manager and Investigator overrode. Much of the point of inheritance is to allow us to reuse code.

To get around this, we're going to use multiple inheritance. Multiple inheritance is pretty much what it sounds like: one class inherits from more than one base classes. We already have the classes we need, we just need to make a new class for the field managers:

Code: Select all
class FieldManager(Investigator, Manager):
   """
   A manager of field investigators.
   """
   pass


Simple, eh? And we can create an instance:

Code: Select all
# exammple employee
tyler = FieldManager('Director of Invesigations', 'Tyler', 'Durden', '555-6732', 123456)


And it will work as expected:

Code: Select all
>>> tyler.contact_info()
'Durden, Tyler (555-6732)'
>>> tyler.weekly_pay()
2374.0


Note that we didn't initialize Tyler with a location, so he must be taking __init__ from Investigator. He's also clearly taking the contact_info method from Investigator, but he's taking the weekly_pay method from Manager.

You might wonder if it matters what order we list the base classes in. If you create another version with the base classes reversed:

Code: Select all
class FieldManager2(Manager, Investigator):
   """
   A manager of field investigators.
   """
   pass
   
# exammple employee
tyler2 = FieldManager2('Director of Invesigations', 'Tyler', 'Durden', '555-6732', 123456)


If we try it out, we get the exact same results:

Code: Select all
>>> tyler2.contact_info()
'Durden, Tyler (555-6732)'
>>> tyler2.weekly_pay()
2374.0


That would seem to imply that the order doesn't matter. But this is a special case, because there is no conflict between Manager and Investigator. Neither one overrides a method that the other one does. If there's no conflict, the order doesn't matter, but if there is a conflict, the order does matter:

Code: Select all
class Spam(object):
      
   def eggs(self):
      return 'spam'
      
   def ham(self):
      return 'spam'
   
   def me(self):
      return 'spam'
      
   def spam(self):
      return 'spam'
      
class Ham(Spam):
   
   def ham(self):
      return 'ham'
   
   def me(self):
      return 'ham'
      
class Eggs(Spam):
   
   def eggs(self):
      return 'eggs'
   
   def me(self):
      return 'eggs'

class HamAndEggs(Ham, Eggs):
   
   pass

class EggsAndHam(Eggs, Ham):
   
   pass


Here there's a conflict: both Ham and Eggs define a 'me' method.

Code: Select all
>>> he = HamAndEggs()
>>> he.me()
'ham'
>>> eh = EggsAndHam()
>>> eh.me()
'eggs'


HamAndEggs is getting the me method from Ham (first in it's base class list), while EggsAndHam is getting it from Eggs (first in EggsAndHam's base class list). We can track down the whole order of how HamAndEggs searches when looking for a method (called the Method Resolution Order):

Code: Select all
>>> he.me()
'ham'
>>> he.ham()
'ham'
>>> he.eggs()
'eggs'
>>> he.spam()
'spam'
[code]

There's three place it could get me (Spam, Eggs, and Ham), and it gets it from Ham. There's two places it could get ham (Ham and Spam), and it gets it from Ham. Likewise it gets Eggs from Eggs instead of Spam. So Ham is before Eggs, and both are before Spam. Of course it will search itself first, so the order is HamAndEggs, Ham, Eggs, Spam. You can check this with the __mro__ attribute of a class:

[code]
>>> HamAndEggs.__mro__
(<class '__main__.HamAndEggs'>, <class '__main__.Ham'>, <class '__main__.Eggs'>,
 <class '__main__.Spam'>, <class 'object'>)
[/code]

I didn't account for object in my order, but it's in there at the end. Now this might just look like a breadth-first search of the tree of ancestors. In that case, the MRO of:

[code]
class Breakfast(HamAndEggs, EggsAndHam):
   pass
[/code]

Would be (Breakfast, HamAndEggs, EggsAndHam, Ham, Eggs, Spam, object). But what you actually get is:

[code]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Cannot create a consistent method resolution
order (MRO) for bases Ham, Eggs


Why you get that error and exactly how the MRO is calculated is really beyond the scope of this tutorial. If you're reading a tutorial on classes, you don't need to worry about it yet. It will be a while before you need multiple inheritance, and a while after that before these points become an issue. Just keep in mind that multiple inheritance is out there, the order of inheritance matters, and before you try anything more complicated than the FieldManager class above, read this article. It explains how the MRO works and why you get that error.
Craig "Ichabod" O'Brien
Minimalist, buddhist, theist, and programmer
Current languages: Python, SAS, and C++
Previous serious languages: R, Java, VBA, Lisp, HyperTalk, BASIC
ichabod801
 
Posts: 93
Joined: Sat Feb 09, 2013 12:54 pm
Location: Outside Washington DC


Return to Tutorials

Who is online

Users browsing this forum: No registered users and 0 guests

cron