继承的实际应用

PythonPythonBeginner
立即练习

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

💡 本教程由 AI 辅助翻译自英文原版。如需查看原文,您可以 切换至英文原版

简介

在这个实验中,你将学习如何使用继承来编写可扩展的代码,并创建一个能以多种格式输出数据的实用应用程序。你还将了解如何使用抽象基类及其具体实现。

此外,你将实现一个工厂模式来简化类的选择。你需要修改的文件是 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{{"继承的实际应用"}} python/classes_objects -.-> lab-132495{{"继承的实际应用"}} python/inheritance -.-> lab-132495{{"继承的实际应用"}} python/polymorphism -.-> lab-132495{{"继承的实际应用"}} end

理解问题

在这个实验中,你将学习 Python 中的继承,以及它如何帮助你创建可扩展且适应性强的代码。继承是面向对象编程中的一个强大概念,一个类可以从另一个类继承属性和方法。这使你能够复用代码,并在现有代码的基础上构建更复杂的功能。

让我们先看看现有的 print_table() 函数。你将改进这个函数,使其在输出格式方面更加灵活。

首先,你需要在 WebIDE 编辑器中打开 tableformat.py 文件。该文件的路径如下:

/home/labex/project/tableformat.py

打开文件后,你会看到 print_table() 函数的当前实现。这个函数用于格式化和打印表格数据。它主要接受两个输入:一个记录列表(记录为对象)和一个字段名列表。根据这些输入,它会打印出格式良好的表格。

现在,让我们测试这个函数,看看它是如何工作的。在 WebIDE 中打开一个终端,并运行以下 Python 命令。这些命令会导入必要的模块,从 CSV 文件中读取数据,然后使用 print_table() 函数显示数据。

import stock
import reader
import tableformat

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

运行这些命令后,你应该会看到以下输出:

      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、HTML 等)输出数据。

与其每次想要支持新的输出格式时都修改 print_table() 函数,你可以使用继承来创建一个更灵活的解决方案。具体做法如下:

  1. 定义一个基类 TableFormatter。这个类将包含用于格式化数据的方法。基类提供了一个通用的结构和功能,所有子类都可以在此基础上构建。
  2. 创建各种子类。每个子类将针对不同的输出格式进行设计。例如,一个子类可能用于 CSV 输出,另一个用于 HTML 输出,依此类推。这些子类将继承基类的方法,并且还可以添加自己的特定功能。
  3. 修改 print_table() 函数,使其可以与任何格式化器一起工作。这意味着你可以将 TableFormatter 类的不同子类传递给 print_table() 函数,它将能够使用相应的格式化方法。

这种方法有一个很大的优点。它允许你在不改变 print_table() 函数核心功能的情况下添加新的输出格式。因此,随着需求的变化,当你需要支持更多的输出格式时,你可以通过创建新的子类轻松实现。

创建基类并修改打印函数

在编程中,继承是一个强大的概念,它允许我们创建类的层次结构。为了开始使用继承以不同格式输出数据,我们首先需要创建一个基类。基类作为其他类的蓝图,定义了一组通用的方法,其子类可以继承和重写这些方法。

现在,让我们创建一个基类,为所有表格格式化器定义接口。在 WebIDE 中打开 tableformat.py 文件,并在文件顶部添加以下代码:

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()

TableFormatter 类是一个抽象基类。抽象基类是定义了方法但不提供其实现的类。相反,它期望其子类提供这些实现。NotImplementedError 异常用于表明这些方法必须由子类重写。如果子类没有重写这些方法,而我们尝试使用它们,就会引发错误。

接下来,我们需要修改 print_table() 函数以使用 TableFormatter 类。print_table() 函数用于从对象列表中打印数据表格。通过修改它以使用 TableFormatter 类,我们可以使该函数更加灵活,能够处理不同的表格格式。

将现有的 print_table() 函数替换为以下代码:

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)

这里的关键变化是 print_table() 现在接受一个 formatter 参数,该参数应该是 TableFormatter 类或其子类的实例。这意味着我们可以将不同的表格格式化器传递给 print_table() 函数,它将使用适当的格式化器来打印表格。该函数通过调用格式化器对象的 headings()row() 方法,将格式化的职责委托给了该对象。

让我们通过尝试使用基类 TableFormatter 来测试我们的更改:

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)

当你运行这段代码时,你应该会看到一个错误:

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

这个错误的出现是因为我们试图直接使用抽象基类,而它没有为其方法提供实现。由于 TableFormatter 类中的 headings()row() 方法引发了 NotImplementedError,当调用这些方法时,Python 不知道该怎么做。在下一步中,我们将创建一个具体的子类来提供这些实现。

✨ 查看解决方案并练习

实现具体的格式化器

现在我们已经定义了抽象基类并更新了 print_table() 函数,接下来是时候创建一个具体的格式化器类了。具体的格式化器类会为抽象基类中定义的方法提供实际的实现。在我们的例子中,我们将创建一个可以将数据格式化为纯文本表格的类。

让我们在 tableformat.py 文件中添加以下类。这个类将继承自 TableFormatter 抽象基类,并实现 headings()row() 方法。

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))

TextTableFormatter 类继承自 TableFormatter。这意味着它继承了 TableFormatter 类的所有属性和方法,但同时也为 headings()row() 方法提供了自己的实现。这些方法分别负责格式化表格的表头和行。headings() 方法以良好的格式打印表头,随后打印一行短横线,用于将表头与数据分隔开。row() 方法以类似的方式格式化每一行数据。

现在,让我们测试一下新的格式化器。我们将使用 stockreadertableformat 模块从 CSV 文件中读取数据,并使用新的格式化器打印数据。

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)

当你运行这段代码时,你应该会看到与之前相同的输出。这是因为我们的新格式化器被设计为生成与原始 print_table() 函数相同的纯文本表格。

      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

这个输出证实了我们的 TextTableFormatter 工作正常。使用这种方法的优势在于,我们使代码更具模块化和可扩展性。通过将格式化逻辑分离到一个独立的类层次结构中,我们可以轻松添加新的输出格式。我们只需创建 TableFormatter 的新子类,而无需修改 print_table() 函数。这样,我们将来就可以支持不同的输出格式,如 CSV 或 HTML。

✨ 查看解决方案并练习

创建额外的格式化器

在编程中,继承是一个强大的概念,它允许我们基于现有的类创建新的类。这有助于代码复用,并使我们的程序更具可扩展性。在这个实验的这一部分,我们将使用继承来为不同的输出格式创建两个新的格式化器:CSV 和 HTML。这些格式化器将继承自一个基类,这意味着它们将共享一些通用的行为,同时拥有各自独特的数据格式化方式。

现在,让我们在 tableformat.py 文件中添加以下类。这些类将分别定义如何以 CSV 和 HTML 格式格式化数据。

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>')

CSVTableFormatter 类旨在以 CSV(逗号分隔值)格式格式化数据。headings 方法接受一个表头列表,并以逗号分隔的形式打印它们。row 方法接受一行数据列表,并同样以逗号分隔的形式打印它们。

另一方面,HTMLTableFormatter 类用于生成 HTML 表格代码。headings 方法使用 HTML <th> 标签创建表格表头,row 方法使用 HTML <td> 标签创建表格行。

让我们测试这些新的格式化器,看看它们是如何工作的。

  1. 首先,让我们测试 CSV 格式化器:
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)

在这段代码中,我们首先导入必要的模块。然后,我们从名为 portfolio.csv 的 CSV 文件中读取数据,并创建 Stock 类的实例。接下来,我们创建 CSVTableFormatter 类的一个实例。最后,我们使用 print_table 函数以 CSV 格式打印投资组合数据。

你应该会看到以下 CSV 格式的输出:

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. 现在让我们测试 HTML 格式化器:
formatter = tableformat.HTMLTableFormatter()
tableformat.print_table(portfolio, ['name', 'shares', 'price'], formatter)

在这里,我们创建 HTMLTableFormatter 类的一个实例,并再次使用 print_table 函数以 HTML 格式打印投资组合数据。

你应该会看到以下 HTML 格式的输出:

<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>

如你所见,每个格式化器都以不同的格式生成输出,但它们都共享由 TableFormatter 基类定义的相同接口。这是继承和多态性强大功能的一个很好的例子。我们可以编写与基类一起工作的代码,它将自动适用于任何子类。

print_table() 函数不需要了解所使用的具体格式化器的任何信息。它只需调用基类中定义的方法,然后根据提供的格式化器类型选择合适的实现。这使得我们的代码更加灵活,更易于维护。

✨ 查看解决方案并练习

创建工厂函数

在使用继承时,一个常见的挑战是用户必须记住具体格式化器类的名称。这可能会很麻烦,尤其是当类的数量增加时。为了简化这个过程,我们可以创建一个工厂函数。工厂函数是一种特殊类型的函数,它可以创建并返回对象。在我们的例子中,它将根据一个简单的格式名称返回合适的格式化器。

让我们在 tableformat.py 文件中添加以下函数。这个函数将接受一个格式名称作为参数,并返回相应的格式化器对象。

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}')

create_formatter() 函数是一个工厂函数。它会检查你提供的 format_name 参数。如果是 'text',它会创建并返回一个 TextTableFormatter 对象;如果是 'csv',它会返回一个 CSVTableFormatter 对象;如果是 'html',它会返回一个 HTMLTableFormatter 对象。如果格式名称未被识别,它会抛出一个 ValueError。这样,用户只需提供一个简单的名称就能轻松选择格式化器,而无需了解具体的类名。

现在,让我们测试这个工厂函数。我们将使用一些现有的函数和类从 CSV 文件中读取数据,并以不同的格式打印出来。

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)

在这段代码中,我们首先导入必要的模块和函数。然后从 portfolio.csv 文件中读取数据并创建一个 portfolio 对象。之后,我们使用不同的格式名称('text'、'csv' 和 'html')测试 create_formatter() 函数。对于每种格式,我们创建一个格式化器对象,打印格式名称,然后使用 print_table() 函数以指定的格式打印 portfolio 数据。

当你运行这段代码时,你应该会看到三种格式的输出,每种格式由格式名称分隔:

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>

工厂函数使代码更具用户友好性,因为它隐藏了类实例化的细节。用户无需知道如何创建格式化器对象,只需指定他们想要的格式即可。

使用工厂函数创建对象的这种模式是面向对象编程中一种常见的设计模式,称为工厂模式(Factory Pattern)。它在客户端代码(使用格式化器的代码)和实际实现类(格式化器类)之间提供了一层抽象。这使得代码更具模块化,更易于使用。

关键概念回顾:

  1. 抽象基类TableFormatter 类充当一个接口。接口定义了一组所有实现它的类必须具备的方法。在我们的例子中,所有格式化器类都必须实现 TableFormatter 类中定义的方法。
  2. 继承:具体的格式化器类,如 TextTableFormatterCSVTableFormatterHTMLTableFormatter,继承自基类 TableFormatter。这意味着它们从基类获取基本结构和方法,并可以提供自己的特定实现。
  3. 多态性print_table() 函数可以与任何实现了所需接口的格式化器一起工作。这意味着你可以将不同的格式化器对象传递给 print_table() 函数,它都能正确处理。
  4. 工厂模式create_formatter() 函数简化了格式化器对象的创建过程。它根据格式名称处理创建正确对象的细节,因此用户无需为此操心。

通过运用这些面向对象的原则,我们创建了一个灵活且可扩展的系统,用于以各种输出格式格式化表格数据。

✨ 查看解决方案并练习

总结

在这个实验中,你学习了面向对象编程中的几个关键概念。你掌握了如何使用继承来创建可扩展的代码,如何将抽象基类定义为接口,以及如何实现从基类继承的具体子类。此外,你还学会了使用多态性编写适用于不同类型的代码,以及使用工厂模式(Factory Pattern)简化对象的创建过程。

这些概念是创建可维护和可扩展代码的强大工具。你构建的表格格式化系统展示了继承如何创建一个灵活的系统。你可以将这些原则应用到其他编程任务中,使你的代码模块化、可复用,并能适应不断变化的需求。