Behavior of Inheritance

Beginner

This tutorial is from open-source community. Access the source code

Introduction

In this lab, you will learn about the behavior of inheritance in Python. Specifically, you'll focus on how the super() function operates and how cooperative inheritance is implemented. Inheritance is a fundamental concept in object - oriented programming, enabling classes to inherit attributes and methods from parent classes for code reuse and hierarchical class structures.

In this hands - on experience, you'll understand different types of inheritance in Python, including single and multiple inheritance. You'll also learn to use the super() function to navigate inheritance hierarchies, implement a practical example of cooperative multiple inheritance, and apply these concepts to build a validation system. The main file created during this lab is validate.py.

This is a Guided Lab, which provides step-by-step instructions to help you learn and practice. Follow the instructions carefully to complete each step and gain hands-on experience. Historical data shows that this is a intermediate level lab with a 69% completion rate. It has received a 100% positive review rate from learners.

Understanding Single and Multiple Inheritance

In this step, we'll learn about the two main types of inheritance in Python: single inheritance and multiple inheritance. Inheritance is a fundamental concept in object - oriented programming that allows a class to inherit attributes and methods from other classes. We'll also look at how Python determines which method to call when there are multiple candidates, a process known as method resolution.

Single Inheritance

Single inheritance is when classes form a single line of ancestry. Think of it like a family tree where each class has only one direct parent. Let's create an example to understand how it works.

First, open a new terminal in the WebIDE. Once the terminal is open, start the Python interpreter by typing the following command and then pressing Enter:

python3

Now that you're in the Python interpreter, we'll create three classes that form a single inheritance chain. Enter the following code:

class A:
    def spam(self):
        print('A.spam')

class B(A):
    def spam(self):
        print('B.spam')
        super().spam()

class C(B):
    def spam(self):
        print('C.spam')
        super().spam()

In this code, class B inherits from class A, and class C inherits from class B. The super() function is used to call the method of the parent class.

After defining these classes, we can find out the order in which Python searches for methods. This order is called the Method Resolution Order (MRO). To see the MRO of class C, type the following code:

C.__mro__

You should see output similar to this:

(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

This output shows that Python first looks for a method in class C, then in class B, then in class A, and finally in the base object class.

Now, let's create an instance of class C and call its spam() method. Type the following code:

c = C()
c.spam()

You should see the following output:

C.spam
B.spam
A.spam

This output demonstrates how super() works in a single inheritance chain. When C.spam() calls super().spam(), it calls B.spam(). Then, when B.spam() calls super().spam(), it calls A.spam().

Multiple Inheritance

Multiple inheritance allows a class to inherit from more than one parent class. This gives a class access to the attributes and methods of all its parent classes. Let's see how method resolution works in this case.

Enter the following code in your Python interpreter:

class Base:
    def spam(self):
        print('Base.spam')

class X(Base):
    def spam(self):
        print('X.spam')
        super().spam()

class Y(Base):
    def spam(self):
        print('Y.spam')
        super().spam()

class Z(Base):
    def spam(self):
        print('Z.spam')
        super().spam()

Now, we'll create a class M that inherits from multiple parent classes X, Y, and Z. Enter the following code:

class M(X, Y, Z):
    pass

M.__mro__

You should see the following output:

(<class '__main__.M'>, <class '__main__.X'>, <class '__main__.Y'>, <class '__main__.Z'>, <class '__main__.Base'>, <class 'object'>)

This output shows the Method Resolution Order for class M. Python will search for methods in this order.

Let's create an instance of class M and call its spam() method:

m = M()
m.spam()

You should see the following output:

X.spam
Y.spam
Z.spam
Base.spam

Notice that super() doesn't just call the method of the immediate parent class. Instead, it follows the Method Resolution Order (MRO) defined by the child class.

Let's create another class N with the parent classes in a different order:

class N(Z, Y, X):
    pass

N.__mro__

You should see the following output:

(<class '__main__.N'>, <class '__main__.Z'>, <class '__main__.Y'>, <class '__main__.X'>, <class '__main__.Base'>, <class 'object'>)

Now, create an instance of class N and call its spam() method:

n = N()
n.spam()

You should see the following output:

Z.spam
Y.spam
X.spam
Base.spam

This shows an important concept: in Python's multiple inheritance, the order of parent classes in the class definition determines the Method Resolution Order. The super() function follows this order no matter which class it's called from.

When you're done exploring these concepts, you can exit the Python interpreter by typing the following code:

exit()

Building a Validation System with Inheritance

In this step, we're going to build a practical validation system using inheritance. Inheritance is a powerful concept in programming that allows you to create new classes based on existing ones. This way, you can reuse code and create more organized and modular programs. By building this validation system, you'll see how inheritance can be used to create reusable code components that can be combined in different ways.

Creating the Base Validator Class

First, we need to create a base class for our validators. To do this, we'll create a new file in the WebIDE. Here's how you can do it: click on "File" > "New File", or you can use the keyboard shortcut. Once the new file is open, name it validate.py.

Now, let's add some code to this file to create a base Validator class. This class will serve as the foundation for all our other validators.

## validate.py
class Validator:
    @classmethod
    def check(cls, value):
        return value

In this code, we've defined a Validator class with a check method. The check method takes a value as an argument and simply returns it unchanged. The @classmethod decorator is used to make this method a class method. This means we can call this method on the class itself, without having to create an instance of the class.

Adding Type Validators

Next, we'll add some validators that check the type of a value. These validators will inherit from the Validator class we just created. Go back to the validate.py file and add the following code:

class Typed(Validator):
    expected_type = object
    @classmethod
    def check(cls, value):
        if not isinstance(value, cls.expected_type):
            raise TypeError(f'Expected {cls.expected_type}')
        return super().check(value)

class Integer(Typed):
    expected_type = int

class Float(Typed):
    expected_type = float

class String(Typed):
    expected_type = str

The Typed class is a subclass of Validator. It has an expected_type attribute, which is initially set to object. The check method in the Typed class checks if the given value is of the expected type. If it's not, it raises a TypeError. If the type is correct, it calls the check method of the parent class using super().check(value).

The Integer, Float, and String classes inherit from Typed and specify the exact type they are supposed to check for. For example, the Integer class checks if a value is an integer.

Testing the Type Validators

Now that we've created our type validators, let's test them. Open a new terminal and start the Python interpreter by running the following command:

python3

Once the Python interpreter is running, we can import and test our validators. Here's some code to test them:

from validate import Integer, String

Integer.check(10)  ## Should return 10

try:
    Integer.check('10')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

String.check('10')  ## Should return '10'

When you run this code, you should see something like this:

10
Error: Expected <class 'int'>
'10'

We can also use these validators in a function. Let's try that:

def add(x, y):
    Integer.check(x)
    Integer.check(y)
    return x + y

add(2, 2)  ## Should return 4

try:
    add('2', '3')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

When you run this code, you should see:

4
Error: Expected <class 'int'>

Adding Value Validators

So far, we've created validators that check the type of a value. Now, let's add some validators that check the value itself rather than the type. Go back to the validate.py file and add the following code:

class Positive(Validator):
    @classmethod
    def check(cls, value):
        if value < 0:
            raise ValueError('Expected >= 0')
        return super().check(value)

class NonEmpty(Validator):
    @classmethod
    def check(cls, value):
        if len(value) == 0:
            raise ValueError('Must be non-empty')
        return super().check(value)

The Positive validator checks if a value is non-negative. If the value is less than 0, it raises a ValueError. The NonEmpty validator checks if a value has a non-zero length. If the length is 0, it raises a ValueError.

Composing Validators with Multiple Inheritance

Now, we're going to combine our validators using multiple inheritance. Multiple inheritance allows a class to inherit from more than one parent class. Go back to the validate.py file and add the following code:

class PositiveInteger(Integer, Positive):
    pass

class PositiveFloat(Float, Positive):
    pass

class NonEmptyString(String, NonEmpty):
    pass

These new classes combine type checking and value checking. For example, the PositiveInteger class checks that a value is both an integer and non-negative. The order of inheritance matters here. The validators are checked in the order specified in the class definition.

Testing the Composed Validators

Let's test our composed validators. In the Python interpreter, run the following code:

from validate import PositiveInteger, PositiveFloat, NonEmptyString

PositiveInteger.check(10)  ## Should return 10

try:
    PositiveInteger.check('10')  ## Should raise TypeError
except TypeError as e:
    print(f"Error: {e}")

try:
    PositiveInteger.check(-10)  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

NonEmptyString.check('hello')  ## Should return 'hello'

try:
    NonEmptyString.check('')  ## Should raise ValueError
except ValueError as e:
    print(f"Error: {e}")

When you run this code, you should see:

10
Error: Expected <class 'int'>
Error: Expected >= 0
'hello'
Error: Must be non-empty

This shows how we can combine validators to create more complex validation rules.

When you're done testing, you can exit the Python interpreter by running the following command:

exit()

Applying Validators to a Stock Class

In this step, we're going to see how our validators work in a real - world situation. Validators are like little checkers that make sure the data we use meets certain rules. We'll create a Stock class. A class is like a blueprint for creating objects. In this case, the Stock class will represent a stock in the stock market, and we'll use our validators to make sure the values of its attributes (like the number of shares and the price) are valid.

Creating the Stock Class

First, we need to create a new file. In the WebIDE, create a new file called stock.py. This file will hold the code for our Stock class. Now, add the following code to the stock.py file:

## stock.py
from validate import PositiveInteger, PositiveFloat

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        self._shares = PositiveInteger.check(value)

    @property
    def price(self):
        return self._price

    @price.setter
    def price(self, value):
        self._price = PositiveFloat.check(value)

    def cost(self):
        return self.shares * self.price

Let's break down what this code does:

  1. We start by importing the PositiveInteger and PositiveFloat validators from our validate module. These validators will help us make sure that the number of shares is a positive integer and the price is a positive float.
  2. Then we define a Stock class. Inside the class, we have an __init__ method. This method is called when we create a new Stock object. It takes in three parameters: name, shares, and price, and assigns them to the object's attributes.
  3. We use properties and setters to validate the values of shares and price. A property is a way to control access to an attribute, and a setter is a method that gets called when we try to set the value of that attribute. When we set the shares attribute, the PositiveInteger.check method is called to make sure the value is a positive integer. Similarly, when we set the price attribute, the PositiveFloat.check method is called to make sure the value is a positive float.
  4. Finally, we have a cost method. This method calculates the total cost of the stock by multiplying the number of shares by the price.

Testing the Stock Class

Now that we've created our Stock class, we need to test it to see if the validators are working correctly. Open a new terminal and start the Python interpreter. You can do this by running the following command:

python3

Once the Python interpreter is running, we can import and test our Stock class. Enter the following code into the Python interpreter:

from stock import Stock

## Create a valid stock
s = Stock('GOOG', 100, 490.10)
print(f"Name: {s.name}, Shares: {s.shares}, Price: {s.price}")
print(f"Cost: {s.cost()}")

## Try setting an invalid shares value
try:
    s.shares = -10
except ValueError as e:
    print(f"Error setting shares: {e}")

## Try setting an invalid price value
try:
    s.price = "not a price"
except TypeError as e:
    print(f"Error setting price: {e}")

When you run this code, you should see output similar to the following:

Name: GOOG, Shares: 100, Price: 490.1
Cost: 49010.0
Error setting shares: Expected >= 0
Error setting price: Expected <class 'float'>

This output shows that our validators are working as expected. The Stock class doesn't allow us to set invalid values for shares and price. When we try to set an invalid value, an error is raised, and we can catch and print that error.

Understanding How Inheritance Helps

One of the great things about using our validators is that we can easily combine different validation rules. Inheritance is a powerful concept in Python that allows us to create new classes based on existing ones. With multiple inheritance, we can use the super() function to call methods from multiple parent classes.

For example, if we want to make sure that the stock name is not empty, we can follow these steps:

  1. Import the NonEmptyString validator from the validate module. This validator will help us check if the stock name is not an empty string.
  2. Add a property setter for the name attribute in the Stock class. This setter will use the NonEmptyString.check() method to validate the stock name.

This shows how inheritance, especially multiple inheritance with the super() function, lets us build components that are flexible and can be reused in different combinations.

When you're done testing, you can exit the Python interpreter by running the following command:

exit()

Summary

In this lab, you have learned about the behavior of inheritance in Python and grasped several key concepts. You explored the difference between single and multiple inheritance, understood how the super() function navigates the Method Resolution Order (MRO), learned to implement cooperative multiple inheritance, and applied inheritance to build a practical validation system.

You also created a flexible validation framework using inheritance and applied it to a real - world example with the Stock class, which shows how inheritance can create reusable and composable components. The key takeaways include how super() works in single and multiple inheritance, the ability of multiple inheritance to compose functionality, and the use of property setters with validators. These concepts are fundamental to Python's object - oriented programming and widely used in real - world applications.