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.
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:
- We start by importing the
PositiveIntegerandPositiveFloatvalidators from ourvalidatemodule. These validators will help us make sure that the number of shares is a positive integer and the price is a positive float. - Then we define a
Stockclass. Inside the class, we have an__init__method. This method is called when we create a newStockobject. It takes in three parameters:name,shares, andprice, and assigns them to the object's attributes. - We use properties and setters to validate the values of
sharesandprice. 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 thesharesattribute, thePositiveInteger.checkmethod is called to make sure the value is a positive integer. Similarly, when we set thepriceattribute, thePositiveFloat.checkmethod is called to make sure the value is a positive float. - Finally, we have a
costmethod. 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:
- Import the
NonEmptyStringvalidator from thevalidatemodule. This validator will help us check if the stock name is not an empty string. - Add a property setter for the
nameattribute in theStockclass. This setter will use theNonEmptyString.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.