29. Classes: Inheritance#

With inheritance, we can create a new class by extending an existing class. The new class inherits the existing class’s attributes and methods (the members). We can then add attributes and/or methods. We can also modify existing attributes and methods to change functionality.

As we look around the world, we can find many instances of hierarchy in which specialized classes exist. For example, the animal taxonomy serves to classify animals into different classes (taxonomic ranks based upon the different attributes that animals possess. Planes, cars, and trains are specialized cases of vehicles. Circles, triangles, and rectangles are specialized cases of shapes. You can also view these examples using the phrase “is-a”. A train is a vehicle. A circle is a shape. A dog is a mammal.

In object-oriented programming, inheritance creates this “is-a” relationship among classes. We build a new class from an existing class. The existing (original) class is called a parent, superclass, or base class. The superclass is the more general class. The new class is called a child, subclass, or derived class. This new class is the more specialized class. The subclass becomes specialized by adding attributes and/or methods. The subclass can also become more specialized by modifying the existing state or behavior.

In the previous notebook, we defined a BankAccount. However, many different types of bank accounts exist: checking, savings, money market, brokerage, etc. We also have transactions that can belong to those accounts. Again, though, we have different transactions types: deposit, withdrawal, transfer, purchase, etc. Then under deposit transactions, we have additional specialties: check, cash, ACH, etc. Similar specialties will also exist for the other transaction types.

As another example, consider different types of employees within a corporation. Corporations can have hourly employees, paid a fixed amount per hour and a specific multiplier for specialty shifts (holiday, weekend, nights). Corporations also can have salaried employees who earn a particular amount per pay period and commission employees who make a base salary per pay period plus a percentage of the gross sales they generate.

The below diagram is a Unified Modeling Language(UML) class diagram. These diagrams document classes, their relationships to each other, and each class’s members (attribute and methods). Each box represents a class. The top line contains that class name. The second area in the box contains the attributes, and the third area contains the methods.

Objects of the Employee class have three attributes - an ID, a name, and a job title - and no methods are defined. The HourlyEmployee class extends the Employee class by adding attributes for the employee’s hourly rate and the hours that the employee worked during the current pay period. The HourlyEmployee also adds behavior - a method to compute their pay. The SalariedEmployee class extends the Employee class by adding an attribute for the employee’s periodic salary amount. For example, if an employee earns \(\$\)120,000 annually and is paid twice a month, the periodic pay would be \(\$\)5,000. The SalariedEmployee class also defines a method to compute pay, but this differs from the HourlyEmployee’s behavior. CommissionedEmployee inherits the behavior and attributes of SalariedEmployee but adds an attribute to track their sales over the past pay period. Their pay calculation will “override” the SalariedEmployee class’s pay calculation as they receive a salary plus a percentage of their gross sales.

The following code cell defines the Employee class, the superclass for the three other classes. This code should be familiar based on the previous notebook. This code does create a calculate_pay() method, but any call to that will raise an exception. By defining a method here, we establish that any subclass must implement a calculate_pay() method. Similarly, the employee_type property produces “unknown” as we should not have any objects created as Employee.

 1class Employee:
 2    """Employee"""
 3    __id = 100  # must change name otherwise a recursive overflow error occurs
 4    
 5    def __init__(self, name, job_title):
 6        self.__id = Employee.__id
 7        Employee.__id += 1
 8        self.__name = name
 9        self.__job_title = job_title
10        
11    def __str__(self):
12        return "ID #{:d}: {:s}({:s}, {:s})".format(self.id,self.name, self.job_title, self.employee_type)
13    
14    def __repr__(self):
15        return str({ "id": self.id, "name": self.name, "job_title":self.job_title })
16    
17    @property
18    def id(self):
19        return self.__id
20
21    @property
22    def name(self):
23        return self.__name
24    
25    @property
26    def job_title(self):
27        return self.__job_title
28    
29    
30    @job_title.setter
31    def job_title(self, new_title):
32        self.__job_title = new_title
33    
34    @property
35    def employee_type(self):
36        return "unknown"
37        
38    
39    def calculate_pay(self):
40        raise  NotImplementedError("Employee subclasses must implement calculate_pay()")

Some sample code to perform ad-hoc testing that the class works.

1a = Employee("Steve","Programmer")
2b = Employee("Christine","Project Manager")
3print(a)
4repr(b)
ID #100: Steve(Programmer, unknown)
"{'id': 101, 'name': 'Christine', 'job_title': 'Project Manager'}"

Now, define a more robust set of unit tests for Employee.

 1import unittest
 2
 3class TestEmployee(unittest.TestCase):
 4    def setUp(self):
 5        Employee._Employee__id = 100
 6        
 7    def test_create(self):
 8        import ast
 9        
10        a = Employee("Steve","Programmer")
11        b = Employee("Christine","Project Manager")
12        self.assertNotEqual(a.id,b.id,"IDs are not unique")
13        self.assertEqual(a.name,"Steve")
14        self.assertEqual(b.name,"Christine")
15        self.assertEqual(a.job_title,"Programmer")
16        self.assertEqual(b.job_title,"Project Manager")
17        self.assertEqual(str(a),"ID #100: Steve(Programmer, unknown)")
18        self.assertEqual(ast.literal_eval(repr(b)),{'id': 101, 'name': 'Christine', 'job_title': 'Project Manager'})
19        
20    def test_calculate_pay_not_implemented(self):
21        a = Employee("Steve","Programmer")
22        with self.assertRaises(Exception) as context:
23            a.calculate_pay()
24        
25        self.assertTrue(type(context.exception) == NotImplementedError)
26        self.assertTrue("must implement" in context.exception.args[0])
27        
28    def test_change_job_title(self):
29        a = Employee("Steve","Programmer")
30        a.job_title = 'Senior Programmer'
31        self.assertEqual(a.job_title,'Senior Programmer')
32        self.assertEqual(str(a),"ID #100: Steve(Senior Programmer, unknown)")
33        
34unittest.main(argv=['unittest','TestEmployee'], verbosity=2, exit=False)
test_calculate_pay_not_implemented (__main__.TestEmployee.test_calculate_pay_not_implemented) ... 
ok
test_change_job_title (__main__.TestEmployee.test_change_job_title) ... 
ok
test_create (__main__.TestEmployee.test_create) ... 
ok

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK
<unittest.main.TestProgram at 0x106f167b0>

At this point, we have implemented our base Employee class and have a robust set of test cases for it.

Next, let’s look at defining the HourlyEmployee class. The following code block adds a few new details:

  1. We define HourlyEmployee as a subclass of Employee. Subclasses are defined similarly to other classes, except we add the parent class name inside of parenthesis at the end. Syntax -

    class ClassName(ParentClassName):
    
  2. In the initializer, we make a call to the parent class with super(). As we have defined __init__ in the child class, the interpreter does not automatically call the corresponding method in the parent class. Therefore, we explicitly call the initializer with reference to the superclass (super()). This call ensures our code performs the steps to initialize the base Employee type properly. The __init__ method then continues with setting the attributes specific to instances of HourlyEmployee.

  3. Note that in the initializer, we explicitly set __hours_worked to None. This statement defines that attribute. In calculate_pay(), we add a sanity check to our code to ensure hours_worked has a valid value with the assert statement. Programmers can place a conditional expression within an assert statement. If the expression evaluates to True, processing continues normally. If the expression evaluates to False, the interpreter raises an exception.

  4. The HourlyEmployee class adds additional methods to support the hourly_rate and hours_worked attributes.

  5. In addition to the __init__ method, the HourlyEmployee class also overrides the methods for employee_type and calculate_pay. By overriding methods, objects of type HourlyEmployee will use the behavior for the methods defined within HourlyEmployee itself. Any behavior defined in the parent class will not be performed unless explicitly called through the super() reference.

  6. Notice that the HourlyEmployee class did not change the __str__ or __repr__ methods. In the test code in the following block, notice that when the __str__ method gets the employee type, it calls the method based on the actual class. i.e., an instance of Employee returns “unknown” while an instance of HourlyEmployee returns “hourly”.

 1from decimal import Decimal
 2
 3class HourlyEmployee(Employee):
 4    """Hourly Employee"""
 5    
 6    
 7    def __init__(self, name, job_title,hourly_rate):
 8        super().__init__(name,job_title)
 9        self.__hourly_rate = Decimal(hourly_rate)
10        self.__hours_worked = None        
11        
12    @property
13    def employee_type(self):
14        return "hourly"
15        
16    @property
17    def hourly_rate(self):
18        return self.__hourly_rate
19    
20    
21    @hourly_rate.setter
22    def hourly_rate(self, new_rate):
23        self.__hourly_rate= new_rate        
24        
25
26    @property
27    def hours_worked(self):
28        return self.__hours_worked
29    
30    
31    @hours_worked.setter
32    def hours_worked(self, new_hours):
33        self.__hours_worked = Decimal(new_hours)               
34        
35    def calculate_pay(self):
36        assert type(self.hours_worked) is Decimal, "Hours worked not established"
37        hours = self.hours_worked
38        overtime_hours = Decimal(0)
39        if hours > 40:
40            overtime_hours = hours - Decimal(40.0)
41            hours = Decimal(40.0)
42        return hours * self.hourly_rate + overtime_hours * self.hourly_rate* Decimal(1.5)

Run some code to see how the HourlyEmployee class works.

1c = HourlyEmployee("Max","System Administrator","54.76")
2print(c)
3print(c.name)
4print(c.hours_worked)
5print(c.calculate_pay())   # will cause an assertion error as hours_worked not set.
ID #102: Max(System Administrator, hourly)
Max
None
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[5], line 5
      3 print(c.name)
      4 print(c.hours_worked)
----> 5 print(c.calculate_pay())   # will cause an assertion error as hours_worked not set.

Cell In[4], line 36, in HourlyEmployee.calculate_pay(self)
     35 def calculate_pay(self):
---> 36     assert type(self.hours_worked) is Decimal, "Hours worked not established"
     37     hours = self.hours_worked
     38     overtime_hours = Decimal(0)

AssertionError: Hours worked not established

Now define some additional test cases. First, we check that the parent functionality still works. We also check that the calculate_pay() method checks that hours_worked has a valid numerical value. We also check several equivalence classes for hours_worked in compute_pay() to cover amounts < 40 hours, amounts equal to 40 hours, and amounts greater than 60 hours. Finally, we check that the employee type value is correct for the different types.

 1import unittest
 2
 3class TestHourlyEmployee(unittest.TestCase):
 4    def setUp(self):
 5        Employee._Employee__id = 100
 6        
 7    def test_create(self):
 8        a = HourlyEmployee("Max","System Administrator",65.0)
 9        self.assertEqual(a.name,"Max")
10        self.assertEqual(a.job_title,"System Administrator")
11        self.assertEqual(str(a),"ID #100: Max(System Administrator, hourly)")
12
13    def test_compute_pay_no_hours(self):
14        a = HourlyEmployee("Max","System Administrator",65.0)
15        with self.assertRaises(Exception) as context:
16            a.calculate_pay()
17        
18        self.assertTrue(type(context.exception) in [TypeError,AssertionError])
19
20    def test_compute_pay(self):
21        a = HourlyEmployee("Max","System Administrator",65.0)
22        a.hours_worked = 20
23        self.assertEqual(a.calculate_pay(), Decimal(1300.0), "Pay not correct")
24        a.hours_worked = 40
25        self.assertEqual(a.calculate_pay(), Decimal(2600.0), "Pay not correct")
26        
27    def test_compute_pay_with_overtime(self):
28        a = HourlyEmployee("Max","System Administrator",65.0)
29        a.hours_worked = 60
30        self.assertEqual(a.calculate_pay(), Decimal(4550.0), "Pay not correct")
31        
32    def test_employee_types(self):
33        a = HourlyEmployee("Max","System Administrator",65.0)
34        b = Employee("Cindy", "Sales Manager")
35        self.assertEqual(a.employee_type,"hourly")
36        self.assertNotEqual(a.employee_type,b.employee_type)
37        
38unittest.main(argv=['unittest','TestHourlyEmployee'], verbosity=2, exit=False)
test_compute_pay (__main__.TestHourlyEmployee.test_compute_pay) ... 
ok
test_compute_pay_no_hours (__main__.TestHourlyEmployee.test_compute_pay_no_hours) ... 
ok
test_compute_pay_with_overtime (__main__.TestHourlyEmployee.test_compute_pay_with_overtime) ... 
ok
test_create (__main__.TestHourlyEmployee.test_create) ... 
ok
test_employee_types (__main__.TestHourlyEmployee.test_employee_types) ... 
ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK
<unittest.main.TestProgram at 0x10741df40>

Many programming languages support the concept of an abstract class. Such a class cannot be instantiated on its own and is meant to serve as a base class for other classes. Within the base class, we can declare common behavior and properties for all its subclasses. We can also define behavior that should be implemented by the subclasses such as with calculate_pay(). To formally define an abstract base class in Python, use the abc module.

29.1. Multiple Inheritance#

Multiple inheritance is the ability of a class to inherit from two or more superclasses.

The primary drawback to multiple inheritance is the diamond problem. In the below diagram, suppose classes A, B, and C have all defined a particular method while D has not. Then, when that method is called on an object of class D, which version of the method is used? A’s? B’s? C’s?

Python solves this problem by defining a specific method resolution order. When looking for a method or attribute, Python performs the following search: the object itself, the object’s class, the first parent class, the second parent class, the nth parent class, and then those parent’s in order.

For example, consider the following set of classes:

 1class SkilledEmployee:
 2    def beverage(self): return "water"
 3    def skill(self):    return "Works hard"
 4
 5class Programmer(SkilledEmployee):
 6    def skill(self):    return "Writes Code"
 7
 8class Statistician(SkilledEmployee):
 9    def skill(self):    return "statistical analysis"
10    
11class StoryTeller(SkilledEmployee):
12    def beverage(self): return "beer"
13    def skill(self):    return "tells stories"
14    
15class ComputationalDataScientist(Programmer,Statistician):
16    def beverage(self): return "Mountain Dew"
17
18class StatisticalAnalyst(Statistician, Programmer):
19    def beverage(self): return "tea"
20
21class Presenter(StoryTeller, Programmer, Statistician):
22    pass
1print("Skills - ")
2print("ComputationalDataScientist:", ComputationalDataScientist().skill())
3print("StatisticalAnalyst:", StatisticalAnalyst().skill())
4print("Presenter:", Presenter().skill())
5print("\nBeverages - ")
6print("ComputationalDataScientist:", ComputationalDataScientist().beverage())
7print("StatisticalAnalyst:", StatisticalAnalyst().beverage())
8print("Presenter:", Presenter().beverage())
Skills - 
ComputationalDataScientist: Writes Code
StatisticalAnalyst: statistical analysis
Presenter: tells stories

Beverages - 
ComputationalDataScientist: Mountain Dew
StatisticalAnalyst: tea
Presenter: beer

Each Python class contains a method mro() that returns the list of classes to search to find a particular attribute or method for an object of that class.

1ComputationalDataScientist.mro()
[__main__.ComputationalDataScientist,
 __main__.Programmer,
 __main__.Statistician,
 __main__.SkilledEmployee,
 object]

As we look for a skill for a ComputationalDataScientist, the interpreter checks these class definitions

  1. The object itself

  2. The object’s class (class and static methods)

  3. The class’s first parent class - Programmer

  4. The class’s second parent class - Statistician

  5. The interpreter then continues to check the parent’s superclasses in a similar order.

Finding the skill() implementation for StatisticalAnalyst follows the same logic, but its first parent class is Statistician and then Programmer.

Presenter shows that we could inherit from 3 parent classes.

1Presenter.mro()
[__main__.Presenter,
 __main__.StoryTeller,
 __main__.Programmer,
 __main__.Statistician,
 __main__.SkilledEmployee,
 object]

29.2. Polymorphism and Duck Typing#

Polymorphism is the ability to call a specific method (send a message) to an object without knowing the receiving object’s actual type. If the receiving object implements that method, then it can respond appropriately. A runtime exception is generated if the receiving object does not implement that method.

The Employee class demonstrates polymorphism within the __str__ method. That method calls the employee_type() method without knowing the exact underlying type of self. The multiple inheritance example demonstrates polymorphism as well.

With Polymorphism, Python programmers can apply the same operation(method call) to different types as long as the method’s name and the number of arguments exist with the receiving type’s definition.

Polymorphism allows methods of the same name to have predictable behavior but allows the underlying class to define the specific behavior independently.

Duck typing is a programming concept used primarily in dynamically typed languages, where the type or class of an object is determined by its behavior (methods) and properties rather than its inheritance or class definition. Duck typing allows for more flexible and extensible code by focusing on what an object can do.

The following code demonstrates polymorphism and duck typing. speak() is called on the animal parameter in the function animal_sound(). We do not know from looking at the code what the Dog.speak() or Duck.speak() is called - that determination is made at runtime based upon the actual type of animal (polymorphism). With duck typing, if an object behaves like a certain type (meaning it has the necessary methods or attributes), it’s treated as an instance of that type, regardless of its actual class or type definition. In this example, the presence of the method speak() effectively becomes a type.

 1class Dog:
 2    def speak(self):
 3        return "Woof!"
 4
 5class Duck:
 6    def speak(self):
 7        return "Quack!"
 8
 9class Fox:
10    def run(self):
11        return "Running!"
12
13def animal_sound(animal):
14    return animal.speak()
15
16d = Dog()
17duck = Duck()
18f = Fox()
19
20print(animal_sound(d))    # Output: Woof!
21print(animal_sound(duck)) # Output: Quack!
22print(animal_sound(f))    # AttributeError 
Woof!
Quack!
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[11], line 22
     20 print(animal_sound(d))    # Output: Woof!
     21 print(animal_sound(duck)) # Output: Quack!
---> 22 print(animal_sound(f))    # AttributeError 

Cell In[11], line 14, in animal_sound(animal)
     13 def animal_sound(animal):
---> 14     return animal.speak()

AttributeError: 'Fox' object has no attribute 'speak'

For a fun diversion, What Does the Fox Say? - YouTube

29.3. Mixins#

A popular use case for multiple inheritance is to inherit from a particular class (a “mixin”) that defines well-established methods and attributes (features). Usually, only one feature exists in a “mixin” class. This class does not share methods with any other parent class - this avoids the diamond problem. The inheritance from “mixins” is not an “is a” relationship, but rather “has behavior”.

The methods in “mixin” classes are typically “side” tasks - sometimes generic in nature, such as logging or type conversions. However, the methods can also be specific to the problem domain, adding shared functionality to different classes. For example, a charting library could have mixins to deal with colors and legends.

The below example creates DumpAttributeMixin to print an object’s attributes.

 1class DumpAttributeMixin:
 2    def dump(self):
 3        import pprint
 4        pprint.pprint(vars(self))
 5        
 6class HourlyDumpEmployee(HourlyEmployee, DumpAttributeMixin):
 7    pass
 8
 9c = HourlyDumpEmployee("Max","System Administrator","54.76")
10c.dump()
{'_Employee__id': 102,
 '_Employee__job_title': 'System Administrator',
 '_Employee__name': 'Max',
 '_HourlyEmployee__hourly_rate': Decimal('54.76'),
 '_HourlyEmployee__hours_worked': None}
1help(object)
Help on class object in module builtins:

class object
 |  The base class of the class hierarchy.
 |
 |  When called, it accepts no arguments and returns a new featureless
 |  instance that has no instance attributes and cannot be given any.
 |
 |  Built-in subclasses:
 |      anext_awaitable
 |      async_generator
 |      async_generator_asend
 |      async_generator_athrow
 |      ... and 90 other subclasses
 |
 |  Methods defined here:
 |
 |  __delattr__(self, name, /)
 |      Implement delattr(self, name).
 |
 |  __dir__(self, /)
 |      Default dir() implementation.
 |
 |  __eq__(self, value, /)
 |      Return self==value.
 |
 |  __format__(self, format_spec, /)
 |      Default object formatter.
 |
 |      Return str(self) if format_spec is empty. Raise TypeError otherwise.
 |
 |  __ge__(self, value, /)
 |      Return self>=value.
 |
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |
 |  __getstate__(self, /)
 |      Helper for pickle.
 |
 |  __gt__(self, value, /)
 |      Return self>value.
 |
 |  __hash__(self, /)
 |      Return hash(self).
 |
 |  __init__(self, /, *args, **kwargs)
 |      Initialize self.  See help(type(self)) for accurate signature.
 |
 |  __le__(self, value, /)
 |      Return self<=value.
 |
 |  __lt__(self, value, /)
 |      Return self<value.
 |
 |  __ne__(self, value, /)
 |      Return self!=value.
 |
 |  __reduce__(self, /)
 |      Helper for pickle.
 |
 |  __reduce_ex__(self, protocol, /)
 |      Helper for pickle.
 |
 |  __repr__(self, /)
 |      Return repr(self).
 |
 |  __setattr__(self, name, value, /)
 |      Implement setattr(self, name, value).
 |
 |  __sizeof__(self, /)
 |      Size of object in memory, in bytes.
 |
 |  __str__(self, /)
 |      Return str(self).
 |
 |  ----------------------------------------------------------------------
 |  Class methods defined here:
 |
 |  __init_subclass__(...) from builtins.type
 |      This method is called when a class is subclassed.
 |
 |      The default implementation does nothing. It may be
 |      overridden to extend subclasses.
 |
 |  __subclasshook__(...) from builtins.type
 |      Abstract classes can override this to customize issubclass().
 |
 |      This is invoked early on by abc.ABCMeta.__subclasscheck__().
 |      It should return True, False or NotImplemented.  If it returns
 |      NotImplemented, the normal algorithm is used.  Otherwise, it
 |      overrides the normal algorithm (and the outcome is cached).
 |
 |  ----------------------------------------------------------------------
 |  Static methods defined here:
 |
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |
 |  ----------------------------------------------------------------------
 |  Data and other attributes defined here:
 |
 |  __class__ = <class 'type'>
 |      type(object) -> the object's type
 |      type(name, bases, dict, **kwds) -> a new type

29.4. Determining Object Types#

Python has several different ways to test an object’s type:

The buit-in functionisinstance() tests if an object is an instance of a particular type. Notice that since StoryTeller inherits from SkilledEmployee, that e is also a SkilledEmployee. We are maintaining the is a relationship.

1e = StoryTeller()
2print("isinstance(e,StoryTeller):", isinstance(e,StoryTeller))
3print("isinstance(e,Programmer):", isinstance(e,Programmer))
4print("isinstance(e,SkilledEmployee):", isinstance(e,SkilledEmployee))
isinstance(e,StoryTeller): True
isinstance(e,Programmer): False
isinstance(e,SkilledEmployee): True

We can also use the built-in function type(), which only checks if the object is that exact type (i.e., it does not consider inheritance).

1e = StoryTeller()
2print(type(e))
3print("Equality to StoryTeller:", type(e) == StoryTeller)
4print("Equality to Programmer:",type(e) == Programmer)
5print("Equality to SkilledEmployee:",type(e) == SkilledEmployee)
<class '__main__.StoryTeller'>
Equality to StoryTeller: True
Equality to Programmer: False
Equality to SkilledEmployee: False

The built-in function issubclass tests if the class reference is derived from another class or is the same class. The first argument must be a class

1print("issubclass(Statistician,Statistician):",       issubclass(Statistician,Statistician))
2print("issubclass(Statistician,Programmer):",         issubclass(Statistician,Programmer))
3print("issubclass(Statistician,SkilledEmployee):",    issubclass(Statistician,SkilledEmployee))
4print("issubclass(Statistician,StatisticalAnalyst):", issubclass(Statistician,StatisticalAnalyst))
issubclass(Statistician,Statistician): True
issubclass(Statistician,Programmer): False
issubclass(Statistician,SkilledEmployee): True
issubclass(Statistician,StatisticalAnalyst): False

Ideally, we do not want to explicitly check an object’s type. We should rely upon Python to use duck typing and polymorphism to take the appropriate behavior. If necessary, we can handle an exception if we call an object that does not implement a particular method.

29.5. Note#

In Python, all objects implicitly inherit from the class object if a parent class is not explicitly defined. This implicit inheritance is object appears as the last item in the mro() calls above. This inheritance hierarchy allows for shared behavior defined among all created objects in Python - such as the ability to get the string representation.

29.6. Suggested LLM Prompts#

  • Explain the concept of inheritance in object orient programming, where a subclass inherits attributes and methods from a parent or base class. Demonstrate how to create a subclass in Python and how to override or extend methods from the parent class. Include examples of single and multiple inheritance.

  • Describe the different types of inheritance in Python (single, multiple, and multilevel inheritance) with code examples for each type. Discuss the advantages and potential pitfalls of using multiple inheritance.

  • Explain the concept of method overriding in Python inheritance. Provide an example where a child class overrides a method from its parent class, and discuss the reasons for overriding methods.

  • Explain polymorphism, which is the ability of objects to take on many forms. Demonstrate how to achieve polymorphism in Python through method overriding in subclasses. Show examples of how the same method can behave differently based on the object’s class.

  • Discuss the use of the super() function in Python inheritance. Provide an example where super() is used to call a method in the parent class from a child class. Explain the benefits of using super().

  • Explain the concept of abstract classes and abstract methods in Python. Discuss the purpose of using abstract classes and provide an example of how to create an abstract class and abstract methods.

  • Discuss the tradeoffs between inheritance and composition in Python. Provide examples of scenarios where inheritance would be preferable and scenarios where composition (using objects as attributes) would be a better design choice.

  • Explain the concept of multiple inheritance in Python and discuss the potential issues that can arise, such as the diamond problem. Provide an example of the diamond problem and discuss possible solutions.

  • Discuss the use of mixins in Python, which are classes designed to provide specific functionality to other classes through inheritance. Provide an example of creating and using a mixin class.

29.7. Review Questions#

  1. What is inheritance in the context of object-oriented programming, why is it useful, and how is it implemented in Python?

  2. How do you create a subclass that inherits from a parent class in Python? Explain the difference from a regular class.

  3. What is the purpose of the super() function when working with inheritance in Python?

  4. What is method overriding, and how is it achieved in Python?

  5. What is the difference between single inheritance, multiple inheritance, and multilevel inheritance?

  6. What is polymorphism in object-oriented programming, and how is it implemented in Python?

  7. What is an abstract class, and how do you define one in Python?

  8. What is the purpose of abstract methods in abstract classes?

  9. What is the diamond problem in multiple inheritance, and how is it resolved in Python?

  10. What are mixins in Python, and how are they used in inheritance?

  11. How can you check if an instance is an instance of a particular class or its subclasses?

  12. Which of these is a common tool that software engineers use to describe the design of their classes?

    1. XML inheritance trees.
    2. HTML class refinements.
    3. CDML hierarchies.
    4. UML diagrams.
  13. What will be the output of the following Python code?

    class Test:
        def __init__(self):
            self.x = 0
    
    class Derived_Test(Test):
        def __init__(self):
            Test.__init__(self)
            self.y = 1
    
    b = Derived_Test()
    print(b.x,b.y)
    
    1. Syntax error in the code
    2. The program runs fine but nothing is printed
    3. 1 0
    4. 0 1
  14. What will be the output of the following Python code?

    class A:
        def one(self):
            return self.two()
    
        def two(self):
            return 'A'
    
    class B(A):
        def two(self):
            return 'B'
    
    obj1=A()
    obj2=B()
    print(obj1.two(),obj2.two())
    
    1. A A
    2. A B
    3. B B
    4. An exception occurs
  15. Within an initializer methods (__init__), is it necessary to call the parent’s initializer method?

  16. Which of the following statements is not true about inheritance?

    1. Inheritance represents an "is a" relationship
    2. Classes can inherit from multiple parent classes in Python.
    3. Inheritance allows us to inherit attributes from a child class.
    4. Inheritance provides another mechanism for code reuse.

answers

29.8. Exercises#

  1. Complete the Salaried and Commissioned Employee classes. Commissioned employees earn 5% of their sales. Salary should be set when constructing the classes.