类型检查与接口

Beginner

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

简介

在这个实验中,你将加深对 Python 中类型检查和接口的理解。通过扩展一个表格格式化模块,你将实现抽象基类和接口验证等概念,以创建更健壮、更易维护的代码。

本实验基于之前练习中的概念,重点关注类型安全和接口设计模式。你的目标包括为函数参数实现类型检查、使用抽象基类创建和使用接口,以及应用模板方法模式来减少代码重复。你将修改 tableformat.py(一个用于将数据格式化为表格的模块)和 reader.py(一个用于读取 CSV 文件的模块)。

这是一个实验(Guided Lab),提供逐步指导来帮助你学习和实践。请仔细按照说明完成每个步骤,获得实际操作经验。根据历史数据,这是一个 初级 级别的实验,完成率为 92%。获得了学习者 90% 的好评率。

print_table() 函数添加类型检查

在这一步中,我们将改进 tableformat.py 文件中的 print_table() 函数。我们会添加一个检查,以确保 formatter 参数是一个有效的 TableFormatter 实例。为什么需要这样做呢?类型检查就像是代码的安全网,它能确保你处理的数据类型正确,从而避免许多难以发现的错误。

理解 Python 中的类型检查

类型检查是编程中非常有用的技术,它能让你在开发过程的早期捕获错误。在 Python 中,我们经常处理不同类型的对象,有时我们期望将特定类型的对象传递给函数。要检查一个对象是否是特定类型或其子类的实例,可以使用 isinstance() 函数。例如,如果你有一个期望传入列表的函数,就可以使用 isinstance() 来确保输入确实是一个列表。

修改 print_table() 函数

首先,在代码编辑器中打开 tableformat.py 文件。滚动到文件底部,你会找到 print_table() 函数。它最初的样子如下:

def print_table(data, columns, formatter):
    '''
    Print a table showing selected columns from a data source
    using the given formatter.
    '''
    formatter.headings(columns)
    for item in data:
        rowdata = [str(getattr(item, col)) for col in columns]
        formatter.row(rowdata)

这个函数接受一些数据、列名列表和一个格式化器,然后使用该格式化器打印表格。但目前,它没有检查格式化器的类型是否正确。

让我们修改它以添加类型检查。我们将使用 isinstance() 函数来检查 formatter 参数是否是 TableFormatter 的实例。如果不是,我们将抛出一个带有明确信息的 TypeError。修改后的代码如下:

def print_table(data, columns, formatter):
    '''
    Print a table showing selected columns from a data source
    using the given formatter.
    '''
    if not isinstance(formatter, TableFormatter):
        raise TypeError("Expected a TableFormatter")

    formatter.headings(columns)
    for item in data:
        rowdata = [str(getattr(item, col)) for col in columns]
        formatter.row(rowdata)

测试类型检查的实现

现在我们已经添加了类型检查,需要确保它能正常工作。让我们创建一个名为 test_tableformat.py 的新 Python 文件。你应该在其中放入以下代码:

import stock
import reader
import tableformat

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

## Define a formatter that doesn't inherit from TableFormatter
class MyFormatter:
    def headings(self, headers):
        pass
    def row(self, rowdata):
        pass

## Try to use the non-compliant formatter
try:
    tableformat.print_table(portfolio, ['name', 'shares', 'price'], MyFormatter())
    print("Test failed - type checking not implemented")
except TypeError as e:
    print(f"Test passed - caught error: {e}")

在这段代码中,我们首先读取一些投资组合数据。然后定义一个名为 MyFormatter 的新格式化器类,它不继承自 TableFormatter。我们尝试在 print_table() 函数中使用这个不符合要求的格式化器。如果我们的类型检查正常工作,它应该抛出一个 TypeError

要运行测试,打开终端并导航到 test_tableformat.py 文件所在的目录。然后运行以下命令:

python test_tableformat.py

如果一切正常,你应该会看到如下输出:

Test passed - caught error: Expected a TableFormatter

这个输出确认了我们的类型检查按预期工作。现在,print_table() 函数将只接受 TableFormatter 实例或其子类的格式化器。

实现抽象基类

在这一步中,我们将使用 Python 的 abc 模块把 TableFormatter 类转换为一个合适的抽象基类(Abstract Base Class,ABC)。不过,首先让我们了解一下什么是抽象基类以及为什么需要它。

理解抽象基类

抽象基类是 Python 中的一种特殊类。它是一种不能直接创建对象的类,也就是说你不能对其进行实例化。抽象基类的主要目的是为其子类定义一个通用接口。它设定了一组所有子类都必须遵循的规则,具体来说,它要求子类实现某些特定的方法。

以下是关于抽象基类的一些关键概念:

  • 我们使用 Python 中的 abc 模块来创建抽象基类。
  • @abstractmethod 装饰器标记的方法就像是规则。任何继承自抽象基类的子类都必须实现这些方法。
  • 如果你尝试创建一个继承自抽象基类但没有实现所有必需方法的类的对象,Python 将会抛出一个错误。

现在你已经了解了抽象基类的基础知识,让我们看看如何修改 TableFormatter 类使其成为一个抽象基类。

修改 TableFormatter

打开 tableformat.py 文件。我们将对 TableFormatter 类进行一些修改,使其使用 abc 模块并成为一个抽象基类。

  1. 首先,我们需要从 abc 模块导入必要的内容。在文件顶部添加以下导入语句:
## tableformat.py
from abc import ABC, abstractmethod

这个导入语句引入了两个重要的内容:ABC,它是 Python 中所有抽象基类的基类;以及 abstractmethod,这是一个装饰器,我们将用它来标记抽象方法。

  1. 接下来,我们将修改 TableFormatter 类。它应该继承自 ABC 以成为一个抽象基类,并且我们将使用 @abstractmethod 装饰器将其方法标记为抽象方法。修改后的类应该如下所示:
class TableFormatter(ABC):
    @abstractmethod
    def headings(self, headers):
        '''
        Emit the table headings.
        '''
        pass

    @abstractmethod
    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        pass

注意这个修改后的类的几点变化:

  • 该类现在继承自 ABC,这意味着它正式成为了一个抽象基类。
  • headingsrow 方法都用 @abstractmethod 进行了装饰。这告诉 Python,TableFormatter 的任何子类都必须实现这些方法。
  • 我们用 pass 替换了 NotImplementedError@abstractmethod 装饰器会确保子类实现这些方法,所以我们不再需要 NotImplementedError 了。

测试你的抽象基类

现在我们已经将 TableFormatter 类变成了一个抽象基类,让我们测试一下它是否能正常工作。我们将创建一个名为 test_abc.py 的文件,并包含以下代码:

from tableformat import TableFormatter

## Test case 1: Define a class with a misspelled method
try:
    class NewFormatter(TableFormatter):
        def headers(self, headings):  ## Misspelled 'headings'
            pass
        def row(self, rowdata):
            pass

    f = NewFormatter()
    print("Test 1 failed - abstract method enforcement not working")
except TypeError as e:
    print(f"Test 1 passed - caught error: {e}")

## Test case 2: Define a class that properly implements all methods
try:
    class ProperFormatter(TableFormatter):
        def headings(self, headers):
            pass
        def row(self, rowdata):
            pass

    f = ProperFormatter()
    print("Test 2 passed - proper implementation works")
except TypeError as e:
    print(f"Test 2 failed - error: {e}")

在这段代码中,我们有两个测试用例。第一个测试用例定义了一个 NewFormatter 类,它试图继承自 TableFormatter,但方法名拼写错误。第二个测试用例定义了一个 ProperFormatter 类,它正确地实现了所有必需的方法。

要运行测试,打开终端并运行以下命令:

python test_abc.py

你应该会看到类似于以下的输出:

Test 1 passed - caught error: Can't instantiate abstract class NewFormatter with abstract methods headings
Test 2 passed - proper implementation works

这个输出确认了我们的抽象基类按预期工作。第一个测试用例失败是因为 NewFormatter 类没有正确实现 headings 方法。第二个测试用例通过是因为 ProperFormatter 类实现了所有必需的方法。

创建算法模板类

在这一步中,我们将使用抽象基类来实现模板方法模式。目标是减少 CSV 解析功能中的代码重复。代码重复会使你的代码更难维护和更新。通过使用模板方法模式,我们可以为 CSV 解析代码创建一个通用结构,并让子类处理具体细节。

理解模板方法模式

模板方法模式是一种行为设计模式。它就像是算法的蓝图。在一个方法中,它定义了算法的整体结构或“骨架”。然而,它并不完全实现所有步骤。相反,它将一些步骤推迟到子类中实现。这意味着子类可以重新定义算法的某些部分,而不改变其整体结构。

在我们的例子中,如果你查看 reader.py 文件,你会注意到 read_csv_as_dicts()read_csv_as_instances() 函数有很多相似的代码。它们之间的主要区别在于如何从 CSV 文件的行中创建记录。通过使用模板方法模式,我们可以避免多次编写相同的代码。

添加 CSVParser 基类

让我们从为 CSV 解析添加一个抽象基类开始。打开 reader.py 文件。我们将在文件顶部,紧接在导入语句之后添加 CSVParser 抽象基类。

## reader.py
import csv
from abc import ABC, abstractmethod

class CSVParser(ABC):
    def parse(self, filename):
        records = []
        with open(filename) as f:
            rows = csv.reader(f)
            headers = next(rows)
            for row in rows:
                record = self.make_record(headers, row)
                records.append(record)
        return records

    @abstractmethod
    def make_record(self, headers, row):
        pass

这个 CSVParser 类作为 CSV 解析的模板。parse 方法包含了读取 CSV 文件的常见步骤,如打开文件、获取表头以及遍历行。从行中创建记录的具体逻辑被抽象到 make_record() 方法中。由于它是一个抽象方法,任何继承自 CSVParser 的类都必须实现这个方法。

实现具体的解析器类

现在我们有了基类,我们需要创建具体的解析器类。这些类将实现具体的记录创建逻辑。

class DictCSVParser(CSVParser):
    def __init__(self, types):
        self.types = types

    def make_record(self, headers, row):
        return { name: func(val) for name, func, val in zip(headers, self.types, row) }

class InstanceCSVParser(CSVParser):
    def __init__(self, cls):
        self.cls = cls

    def make_record(self, headers, row):
        return self.cls.from_row(row)

DictCSVParser 类用于将记录创建为字典。它在构造函数中接受一个类型列表。make_record 方法使用这些类型来转换行中的值并创建一个字典。

InstanceCSVParser 类用于将记录创建为类的实例。它在构造函数中接受一个类。make_record 方法调用该类的 from_row 方法从行中创建一个实例。

重构原始函数

现在,让我们重构原始的 read_csv_as_dicts()read_csv_as_instances() 函数以使用这些新类。

def read_csv_as_dicts(filename, types):
    '''
    Read a CSV file into a list of dictionaries with appropriate type conversion.
    '''
    parser = DictCSVParser(types)
    return parser.parse(filename)

def read_csv_as_instances(filename, cls):
    '''
    Read a CSV file into a list of instances of a class.
    '''
    parser = InstanceCSVParser(cls)
    return parser.parse(filename)

这些重构后的函数与原始函数具有相同的接口。但在内部,它们使用了我们刚刚创建的新解析器类。这样,我们就将通用的 CSV 解析逻辑与具体的记录创建逻辑分离开来。

测试你的实现

让我们检查一下我们重构后的代码是否能正常工作。创建一个名为 test_reader.py 的文件,并在其中添加以下代码。

import reader
import stock

## Test the refactored read_csv_as_instances function
portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
print("First stock:", portfolio[0])

## Test the refactored read_csv_as_dicts function
portfolio_dicts = reader.read_csv_as_dicts('portfolio.csv', [str, int, float])
print("First stock as dict:", portfolio_dicts[0])

## Test direct use of a parser
parser = reader.DictCSVParser([str, int, float])
portfolio_dicts2 = parser.parse('portfolio.csv')
print("First stock from direct parser:", portfolio_dicts2[0])

要运行测试,打开终端并执行以下命令:

python test_reader.py

你应该会看到类似于以下的输出:

First stock: Stock('AA', 100, 32.2)
First stock as dict: {'name': 'AA', 'shares': 100, 'price': 32.2}
First stock from direct parser: {'name': 'AA', 'shares': 100, 'price': 32.2}

如果你看到这个输出,这意味着你重构后的代码能正常工作。原始函数和直接使用解析器都产生了预期的结果。

总结

在本次实验中,你学习了几个关键的面向对象编程概念,以增强 Python 代码的性能。首先,你在 print_table() 函数中实现了类型检查,确保只使用有效的格式化器,从而提高了代码的健壮性。其次,你将 TableFormatter 类转换为抽象基类,强制子类实现特定的方法。

此外,你通过创建 CSVParser 抽象基类及其具体实现应用了模板方法模式。这在保持一致的算法结构的同时减少了代码重复。这些技术对于创建更易于维护和健壮的 Python 代码至关重要,特别是在大规模应用程序中。为了进一步学习,你可以探索 Python 中的类型提示(PEP 484)、协议类和 Python 中的设计模式。