Practical Use of Inheritance

PythonPythonBeginner
Practice Now

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

Introduction

In this lab, you will learn how to use inheritance to write extensible code and create a practical application that outputs data in multiple formats. You'll also understand how to use abstract base classes and their concrete implementations.

Moreover, you will implement a factory pattern to simplify class selection. The file you'll modify is tableformat.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/inheritance("Inheritance") python/ObjectOrientedProgrammingGroup -.-> python/polymorphism("Polymorphism") subgraph Lab Skills python/function_definition -.-> lab-132495{{"Practical Use of Inheritance"}} python/classes_objects -.-> lab-132495{{"Practical Use of Inheritance"}} python/inheritance -.-> lab-132495{{"Practical Use of Inheritance"}} python/polymorphism -.-> lab-132495{{"Practical Use of Inheritance"}} end

Understanding the Problem

In this lab, we're going to learn about inheritance in Python and how it can help us create code that is both extensible and adaptable. Inheritance is a powerful concept in object - oriented programming where a class can inherit attributes and methods from another class. This allows us to reuse code and build more complex functionality on top of existing code.

Let's start by looking at the existing print_table() function. This function is what we'll be improving to make it more flexible in terms of output formats.

First, you need to open the tableformat.py file in the WebIDE editor. The path to this file is as follows:

/home/labex/project/tableformat.py

Once you open the file, you'll see the current implementation of the print_table() function. This function is designed to format and print tabular data. It takes two main inputs: a list of records (which are objects) and a list of field names. Based on these inputs, it prints a nicely formatted table.

Now, let's test this function to see how it works. Open a terminal in the WebIDE and run the following Python commands. These commands import the necessary modules, read data from a CSV file, and then use the print_table() function to display the data.

import stock
import reader
import tableformat

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
tableformat.print_table(portfolio, ['name', 'shares', 'price'])

After running these commands, you should see the following output:

      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

The output looks good, but there's a limitation to this function. Currently, it only supports one output format, which is plain text. In real - world scenarios, you might want to output your data in different formats like CSV, HTML, or others.

Instead of making changes to the print_table() function every time we want to support a new output format, we can use inheritance to create a more flexible solution. Here's how we'll do it:

  1. We'll define a base TableFormatter class. This class will have methods that are used for formatting data. The base class provides a common structure and functionality that all the subclasses can build upon.
  2. We'll create various subclasses. Each subclass will be designed for a different output format. For example, one subclass might be for CSV output, another for HTML output, and so on. These subclasses will inherit the methods from the base class and can also add their own specific functionality.
  3. We'll modify the print_table() function so that it can work with any formatter. This means that we can pass different subclasses of the TableFormatter class to the print_table() function, and it will be able to use the appropriate formatting methods.

This approach has a big advantage. It allows us to add new output formats without changing the core functionality of the print_table() function. So, as your requirements change and you need to support more output formats, you can easily do so by creating new subclasses.

Creating a Base Class and Modifying the Print Function

In programming, inheritance is a powerful concept that allows us to create a hierarchy of classes. To start using inheritance for outputting data in different formats, we first need to create a base class. A base class serves as a blueprint for other classes, defining a common set of methods that its subclasses can inherit and override.

Now, let's create a base class that will define the interface for all table formatters. Open the tableformat.py file in the WebIDE and add the following code at the top of the file:

class TableFormatter:
    """
    Base class for all table formatters.
    This class defines the interface that all formatters must implement.
    """
    def headings(self, headers):
        """
        Generate the table headings.
        """
        raise NotImplementedError()

    def row(self, rowdata):
        """
        Generate a single row of table data.
        """
        raise NotImplementedError()

The TableFormatter class is an abstract base class. An abstract base class is a class that defines methods but doesn't provide implementations for them. Instead, it expects its subclasses to provide these implementations. The NotImplementedError exceptions are used to indicate that these methods must be overridden by subclasses. If a subclass doesn't override these methods and we try to use them, an error will be raised.

Next, we need to modify the print_table() function to use the TableFormatter class. The print_table() function is used to print a table of data from a list of objects. By modifying it to use the TableFormatter class, we can make the function more flexible and able to work with different table formats.

Replace the existing print_table() function with the following code:

def print_table(records, fields, formatter):
    """
    Print a table of data from a list of objects using the specified formatter.

    Args:
        records: A list of objects
        fields: A list of field names
        formatter: A TableFormatter object
    """
    formatter.headings(fields)
    for r in records:
        rowdata = [getattr(r, fieldname) for fieldname in fields]
        formatter.row(rowdata)

The key change here is that print_table() now takes a formatter parameter, which should be an instance of TableFormatter or a subclass. This means that we can pass different table formatters to the print_table() function, and it will use the appropriate formatter to print the table. The function delegates the formatting responsibility to the formatter object by calling its headings() and row() methods.

Let's test our changes by trying to use the base TableFormatter class:

import stock
import reader
import tableformat

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
formatter = tableformat.TableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

When you run this code, you should see an error:

Traceback (most recent call last):
...
NotImplementedError

This error occurs because we're trying to use the abstract base class directly, but it doesn't provide implementations for its methods. Since the headings() and row() methods in the TableFormatter class raise NotImplementedError, Python doesn't know what to do when these methods are called. In the next step, we'll create a concrete subclass that does provide these implementations.

โœจ Check Solution and Practice

Implementing a Concrete Formatter

Now that we have defined our abstract base class and updated the print_table() function, it's time to create a concrete formatter class. A concrete formatter class is one that provides actual implementations for the methods defined in the abstract base class. In our case, we'll create a class that can format data into a plain - text table.

Let's add the following class to your tableformat.py file. This class will inherit from the TableFormatter abstract base class and implement the headings() and row() methods.

class TextTableFormatter(TableFormatter):
    """
    Formatter that generates a plain - text table.
    """
    def headings(self, headers):
        """
        Generate plain - text table headings.
        """
        print(' '.join('%10s' % h for h in headers))
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        """
        Generate a plain - text table row.
        """
        print(' '.join('%10s' % d for d in rowdata))

The TextTableFormatter class inherits from TableFormatter. This means it gets all the properties and methods from the TableFormatter class, but it also provides its own implementations for the headings() and row() methods. These methods are responsible for formatting the table headers and rows respectively. The headings() method prints the headers in a nicely formatted way, followed by a line of dashes to separate the headers from the data. The row() method formats each row of data in a similar way.

Now, let's test our new formatter. We'll use the stock, reader, and tableformat modules to read data from a CSV file and print it using our new formatter.

import stock
import reader
import tableformat

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
formatter = tableformat.TextTableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

When you run this code, you should see the same output as before. This is because our new formatter is designed to produce the same plain - text table as the original print_table() function.

      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

This output confirms that our TextTableFormatter is working correctly. The advantage of using this approach is that we've made our code more modular and extensible. By separating the formatting logic into a separate class hierarchy, we can easily add new output formats. All we need to do is create new subclasses of TableFormatter without modifying the print_table() function. This way, we can support different output formats like CSV or HTML in the future.

โœจ Check Solution and Practice

Creating Additional Formatters

In programming, inheritance is a powerful concept that allows us to create new classes based on existing ones. This helps in reusing code and making our programs more extensible. In this part of the experiment, we'll use inheritance to create two new formatters for different output formats: CSV and HTML. These formatters will inherit from a base class, which means they'll share some common behavior while having their own unique ways of formatting data.

Now, let's add the following classes to your tableformat.py file. These classes will define how to format data in CSV and HTML formats respectively.

class CSVTableFormatter(TableFormatter):
    """
    Formatter that generates CSV formatted data.
    """
    def headings(self, headers):
        """
        Generate CSV headers.
        """
        print(','.join(headers))

    def row(self, rowdata):
        """
        Generate a CSV data row.
        """
        print(','.join(str(d) for d in rowdata))

class HTMLTableFormatter(TableFormatter):
    """
    Formatter that generates HTML table code.
    """
    def headings(self, headers):
        """
        Generate HTML table headers.
        """
        print('<tr>', end=' ')
        for header in headers:
            print(f'<th>{header}</th>', end=' ')
        print('</tr>')

    def row(self, rowdata):
        """
        Generate an HTML table row.
        """
        print('<tr>', end=' ')
        for data in rowdata:
            print(f'<td>{data}</td>', end=' ')
        print('</tr>')

The CSVTableFormatter class is designed to format data in the CSV (Comma-Separated Values) format. The headings method takes a list of headers and prints them separated by commas. The row method takes a list of data for a single row and also prints them separated by commas.

The HTMLTableFormatter class, on the other hand, is used to generate HTML table code. The headings method creates the table headers using HTML <th> tags, and the row method creates a table row using HTML <td> tags.

Let's test these new formatters to see how they work.

  1. First, let's test the CSV formatter:
import stock
import reader
import tableformat

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
formatter = tableformat.CSVTableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

In this code, we first import the necessary modules. Then we read data from a CSV file named portfolio.csv and create instances of the Stock class. Next, we create an instance of the CSVTableFormatter class. Finally, we use the print_table function to print the portfolio data in CSV format.

You should see the following CSV-formatted output:

name,shares,price
AA,100,32.2
IBM,50,91.1
CAT,150,83.44
MSFT,200,51.23
GE,95,40.37
MSFT,50,65.1
IBM,100,70.44
  1. Now let's test the HTML formatter:
formatter = tableformat.HTMLTableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

Here, we create an instance of the HTMLTableFormatter class and use the print_table function again to print the portfolio data in HTML format.

You should see the following HTML-formatted output:

<tr> <th>name</th> <th>shares</th> <th>price</th> </tr>
<tr> <td>AA</td> <td>100</td> <td>32.2</td> </tr>
<tr> <td>IBM</td> <td>50</td> <td>91.1</td> </tr>
<tr> <td>CAT</td> <td>150</td> <td>83.44</td> </tr>
<tr> <td>MSFT</td> <td>200</td> <td>51.23</td> </tr>
<tr> <td>GE</td> <td>95</td> <td>40.37</td> </tr>
<tr> <td>MSFT</td> <td>50</td> <td>65.1</td> </tr>
<tr> <td>IBM</td> <td>100</td> <td>70.44</td> </tr>

As you can see, each formatter produces output in a different format, but they all share the same interface defined by the TableFormatter base class. This is a great example of the power of inheritance and polymorphism. We can write code that works with the base class, and it will automatically work with any subclass.

The print_table() function doesn't need to know anything about the specific formatter being used. It just calls the methods defined in the base class, and the appropriate implementation is selected based on the type of formatter provided. This makes our code more flexible and easier to maintain.

โœจ Check Solution and Practice

Creating a Factory Function

When using inheritance, one common challenge is that users have to remember the names of specific formatter classes. This can be quite a hassle, especially as the number of classes grows. To simplify this process, we can create a factory function. A factory function is a special type of function that creates and returns objects. In our case, it will return the appropriate formatter based on a simple format name.

Let's add the following function to your tableformat.py file. This function will take a format name as an argument and return the corresponding formatter object.

def create_formatter(format_name):
    """
    Create a formatter of the specified type.

    Args:
        format_name: Name of the formatter ('text', 'csv', 'html')

    Returns:
        A TableFormatter object

    Raises:
        ValueError: If format_name is not recognized
    """
    if format_name == 'text':
        return TextTableFormatter()
    elif format_name == 'csv':
        return CSVTableFormatter()
    elif format_name == 'html':
        return HTMLTableFormatter()
    else:
        raise ValueError(f'Unknown format {format_name}')

The create_formatter() function is a factory function. It checks the format_name argument you provide. If it's 'text', it creates and returns a TextTableFormatter object. If it's 'csv', it returns a CSVTableFormatter object, and if it's 'html', it returns an HTMLTableFormatter object. If the format name is not recognized, it raises a ValueError. This way, users can easily select a formatter just by providing a simple name, without having to know the specific class names.

Now, let's test the factory function. We'll use some existing functions and classes to read data from a CSV file and print it in different formats.

import stock
import reader
from tableformat import create_formatter, print_table

portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)

## Test with text formatter
formatter = create_formatter('text')
print("\nText Format:")
print_table(portfolio, ['name', 'shares', 'price'], formatter)

## Test with CSV formatter
formatter = create_formatter('csv')
print("\nCSV Format:")
print_table(portfolio, ['name', 'shares', 'price'], formatter)

## Test with HTML formatter
formatter = create_formatter('html')
print("\nHTML Format:")
print_table(portfolio, ['name', 'shares', 'price'], formatter)

In this code, we first import the necessary modules and functions. Then we read data from the portfolio.csv file and create a portfolio object. After that, we test the create_formatter() function with different format names: 'text', 'csv', and 'html'. For each format, we create a formatter object, print the format name, and then use the print_table() function to print the portfolio data in the specified format.

When you run this code, you should see output in all three formats, separated by the format name:

Text Format:
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44

CSV Format:
name,shares,price
AA,100,32.2
IBM,50,91.1
CAT,150,83.44
MSFT,200,51.23
GE,95,40.37
MSFT,50,65.1
IBM,100,70.44

HTML Format:
<tr> <th>name</th> <th>shares</th> <th>price</th> </tr>
<tr> <td>AA</td> <td>100</td> <td>32.2</td> </tr>
<tr> <td>IBM</td> <td>50</td> <td>91.1</td> </tr>
<tr> <td>CAT</td> <td>150</td> <td>83.44</td> </tr>
<tr> <td>MSFT</td> <td>200</td> <td>51.23</td> </tr>
<tr> <td>GE</td> <td>95</td> <td>40.37</td> </tr>
<tr> <td>MSFT</td> <td>50</td> <td>65.1</td> </tr>
<tr> <td>IBM</td> <td>100</td> <td>70.44</td> </tr>

The factory function makes the code more user-friendly because it hides the details of class instantiation. Users don't need to know how to create formatter objects; they just need to specify the format they want.

This pattern of using a factory function to create objects is a common design pattern in object-oriented programming, known as the Factory Pattern. It provides a layer of abstraction between the client code (the code that uses the formatter) and the actual implementation classes (the formatter classes). This makes the code more modular and easier to use.

Key Concepts Review:

  1. Abstract Base Class: The TableFormatter class serves as an interface. An interface defines a set of methods that all classes implementing it must have. In our case, all formatter classes must implement the methods defined in the TableFormatter class.

  2. Inheritance: The concrete formatter classes, like TextTableFormatter, CSVTableFormatter, and HTMLTableFormatter, inherit from the base TableFormatter class. This means they get the basic structure and methods from the base class and can provide their own specific implementations.

  3. Polymorphism: The print_table() function can work with any formatter that implements the required interface. This means you can pass different formatter objects to the print_table() function, and it will work correctly with each one.

  4. Factory Pattern: The create_formatter() function simplifies the creation of formatter objects. It takes care of the details of creating the right object based on the format name, so users don't have to worry about it.

By using these object-oriented principles, we've created a flexible and extensible system for formatting tabular data in various output formats.

โœจ Check Solution and Practice

Summary

In this lab, you have learned several key concepts in object-oriented programming. You've mastered how to use inheritance to create extensible code, define an abstract base class as an interface, and implement concrete subclasses that inherit from a base class. Additionally, you've learned to use polymorphism for writing code that works with different types and the Factory Pattern to simplify object creation.

These concepts are powerful tools for creating maintainable and extensible code. The table formatting system you built shows how inheritance can create a flexible system. You can apply these principles to other programming tasks, making your code modular, reusable, and adaptable to changing requirements.