重新定义特殊方法

Intermediate

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

简介

在这个实验中,你将学习如何通过重新定义特殊方法来自定义对象的行为。你还将改变用户定义对象的打印方式,并使对象具有可比性。

此外,你将学习创建一个上下文管理器。本实验中需要修改的文件是 stock.py

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

使用 __repr__ 改进对象表示

在 Python 中,对象可以通过两种不同的方式表示为字符串。这两种表示方式有不同的用途,并且在各种场景中都很有用。

第一种类型是字符串表示。这是由 str() 函数创建的,当你使用 print() 函数时,它会自动被调用。字符串表示的设计目的是便于人类阅读。它以一种我们易于理解和解释的格式呈现对象。

第二种类型是代码表示。这是由 repr() 函数生成的。代码表示展示了你为重新创建该对象所需编写的代码。它更侧重于提供一种精确且明确的方式,在代码中表示对象。

让我们使用 Python 的内置 date 类来看一个具体的例子。这将帮助你直观地看到字符串表示和代码表示之间的区别。

>>> from datetime import date
>>> d = date(2008, 7, 5)
>>> print(d)              ## Uses str()
2008-07-05
>>> d                     ## Uses repr()
datetime.date(2008, 7, 5)

在这个例子中,当我们使用 print(d) 时,Python 会对 date 对象 d 调用 str() 函数,我们会得到一个格式为 YYYY-MM-DD 的易读日期。当我们在交互式 shell 中直接输入 d 时,Python 会调用 repr() 函数,我们会看到重新创建该 date 对象所需的代码。

你可以通过多种方式显式地获取 repr() 字符串。以下是一些示例:

>>> print('The date is', repr(d))
The date is datetime.date(2008, 7, 5)
>>> print(f'The date is {d!r}')
The date is datetime.date(2008, 7, 5)
>>> print('The date is %r' % d)
The date is datetime.date(2008, 7, 5)

现在,让我们将这个概念应用到我们的 Stock 类中。我们将通过实现 __repr__ 方法来改进这个类。当 Python 需要对象的代码表示时,会调用这个特殊方法。

为此,在你的编辑器中打开 stock.py 文件。然后,将 __repr__ 方法添加到 Stock 类中。__repr__ 方法应该返回一个字符串,该字符串展示了重新创建 Stock 对象所需的代码。

def __repr__(self):
    return f"Stock('{self.name}', {self.shares}, {self.price})"

添加 __repr__ 方法后,你完整的 Stock 类现在应该如下所示:

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

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

    def sell(self, shares):
        self.shares -= shares

    def __repr__(self):
        return f"Stock('{self.name}', {self.shares}, {self.price})"

现在,让我们测试我们修改后的 Stock 类。在你的终端中运行以下命令来打开 Python 交互式 shell:

python3

交互式 shell 打开后,尝试以下命令:

>>> import stock
>>> goog = stock.Stock('GOOG', 100, 490.10)
>>> goog
Stock('GOOG', 100, 490.1)

你还可以看看 __repr__ 方法在股票投资组合中是如何工作的。以下是一个示例:

>>> import reader
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> portfolio
[Stock('AA', 100, 32.2), Stock('IBM', 50, 91.1), Stock('CAT', 150, 83.44), Stock('MSFT', 200, 51.23), Stock('GE', 95, 40.37), Stock('MSFT', 50, 65.1), Stock('IBM', 100, 70.44)]

如你所见,__repr__ 方法让我们的 Stock 对象在交互式 shell 或调试器中显示时更具信息性。它现在展示了重新创建每个对象所需的代码,这对于调试和理解对象的状态非常有用。

测试完成后,你可以通过运行以下命令退出 Python 解释器:

>>> exit()

使用 __eq__ 使对象可比较

在 Python 中,当你使用 == 运算符比较两个对象时,Python 实际上会调用 __eq__ 特殊方法。默认情况下,这个方法比较对象的标识,即检查它们是否存储在相同的内存地址,而不是比较它们的内容。

让我们来看一个例子。假设我们有一个 Stock 类,并且创建了两个具有相同值的 Stock 对象。然后我们尝试使用 == 运算符来比较它们。在 Python 解释器中,你可以这样做:

首先,在终端中运行以下命令启动 Python 解释器:

python3

然后,在 Python 解释器中执行以下代码:

>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
False

如你所见,尽管两个 Stock 对象 ab 的属性(namesharesprice)具有相同的值,但 Python 认为它们是不同的对象,因为它们存储在不同的内存位置。

为了解决这个问题,我们可以在 Stock 类中实现 __eq__ 方法。每当对 Stock 类的对象使用 == 运算符时,就会调用这个方法。

现在,再次打开 stock.py 文件。在 Stock 类中,添加以下 __eq__ 方法:

def __eq__(self, other):
    return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
                                         (other.name, other.shares, other.price))

让我们来分析一下这个方法的作用:

  1. 首先,它使用 isinstance 函数检查 other 对象是否是 Stock 类的实例。这很重要,因为我们只想将 Stock 对象与其他 Stock 对象进行比较。
  2. 如果 otherStock 对象,它会比较 self 对象和 other 对象的属性(namesharesprice)。
  3. 只有当两个对象都是 Stock 实例且它们的属性完全相同时,它才会返回 True

添加 __eq__ 方法后,你完整的 Stock 类应该如下所示:

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

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

    def sell(self, shares):
        self.shares -= shares

    def __repr__(self):
        return f"Stock('{self.name}', {self.shares}, {self.price})"

    def __eq__(self, other):
        return isinstance(other, Stock) and ((self.name, self.shares, self.price) ==
                                             (other.name, other.shares, other.price))

现在,让我们测试改进后的 Stock 类。再次启动 Python 解释器:

python3

然后,在 Python 解释器中运行以下代码:

>>> import stock
>>> a = stock.Stock('GOOG', 100, 490.1)
>>> b = stock.Stock('GOOG', 100, 490.1)
>>> a == b
True
>>> c = stock.Stock('GOOG', 200, 490.1)
>>> a == c
False

太棒了!现在我们的 Stock 对象可以根据其内容进行正确比较,而不是根据它们的内存地址。

__eq__ 方法中的 isinstance 检查至关重要。它确保我们只比较 Stock 对象。如果没有这个检查,将 Stock 对象与非 Stock 对象进行比较可能会引发错误。

测试完成后,你可以通过运行以下命令退出 Python 解释器:

>>> exit()

创建上下文管理器

上下文管理器是 Python 中的一种特殊对象。在 Python 中,对象可以有不同的方法来定义其行为。上下文管理器特别定义了两个重要的方法:__enter____exit__。这些方法与 with 语句一起使用。with 语句用于为一段代码设置特定的上下文。可以将其看作是创建一个小环境,在这个环境中会发生某些事情,当代码块执行完毕后,上下文管理器会负责清理工作。

在这一步中,我们将创建一个具有非常实用功能的上下文管理器。它将临时重定向标准输出(sys.stdout)。标准输出是 Python 程序的正常输出所指向的地方,通常是控制台。通过重定向它,我们可以将输出发送到文件中。当你想保存原本只会显示在控制台的输出时,这非常有用。

首先,我们需要创建一个新文件来编写上下文管理器代码。我们将这个文件命名为 redirect.py。你可以在终端中使用以下命令创建它:

touch /home/labex/project/redirect.py

现在文件已经创建好了,在编辑器中打开它。打开后,将以下 Python 代码添加到文件中:

import sys

class redirect_stdout:
    def __init__(self, out_file):
        self.out_file = out_file

    def __enter__(self):
        self.stdout = sys.stdout
        sys.stdout = self.out_file
        return self.out_file

    def __exit__(self, ty, val, tb):
        sys.stdout = self.stdout

让我们来分析一下这个上下文管理器的作用:

  1. __init__:这是初始化方法。当我们创建 redirect_stdout 类的实例时,会传入一个文件对象。这个方法将该文件对象存储在实例变量 self.out_file 中。这样,它就记住了我们要将输出重定向到的位置。
  2. __enter__
    • 首先,它保存当前的 sys.stdout。这很重要,因为我们稍后需要恢复它。
    • 然后,它将当前的 sys.stdout 替换为我们的文件对象。从这一点开始,任何原本会输出到控制台的内容都将输出到文件中。
    • 最后,它返回文件对象。这很有用,因为我们可能想在 with 代码块中使用这个文件对象。
  3. __exit__
    • 这个方法恢复原来的 sys.stdout。因此,在 with 代码块执行完毕后,输出将像往常一样回到控制台。
    • 它接受三个参数:异常类型(ty)、异常值(val)和回溯信息(tb)。这些参数是上下文管理器协议所要求的。它们用于处理 with 代码块中可能发生的任何异常。

现在,让我们测试一下我们的上下文管理器。我们将使用它将表格的输出重定向到一个文件中。首先,启动 Python 解释器:

python3

然后,在解释器中运行以下 Python 代码:

>>> import stock, reader, tableformat
>>> from redirect import redirect_stdout
>>> portfolio = reader.read_csv_as_instances('portfolio.csv', stock.Stock)
>>> formatter = tableformat.create_formatter('text')
>>> with redirect_stdout(open('out.txt', 'w')) as file:
...     tableformat.print_table(portfolio, ['name','shares','price'], formatter)
...     file.close()
...
>>> ## Let's check the content of the output file
>>> print(open('out.txt').read())
      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

太棒了!我们的上下文管理器按预期工作。它成功地将表格输出重定向到了 out.txt 文件中。

上下文管理器是 Python 中非常强大的特性。它们有助于你正确地管理资源。以下是上下文管理器的一些常见用例:

  • 文件操作:当你打开一个文件时,上下文管理器可以确保文件被正确关闭,即使发生错误也不例外。
  • 数据库连接:它可以确保在你使用完数据库连接后将其关闭。
  • 线程程序中的锁:上下文管理器可以以安全的方式处理资源的加锁和解锁。
  • 临时更改环境设置:你可以为一段代码更改某些设置,然后自动恢复它们。

这种模式非常重要,因为它确保了即使在 with 代码块中发生异常,资源也能被正确清理。

测试完成后,你可以退出 Python 解释器:

>>> exit()

总结

在本次实验中,你学习了如何使用 __repr__ 方法自定义对象的字符串表示形式,如何使用 __eq__ 方法使对象可比较,以及如何使用 __enter____exit__ 方法创建上下文管理器。这些特殊的“双下划线方法”是 Python 面向对象特性的基石。

在你的类中实现这些方法,能让你的对象表现得像内置类型一样,并与 Python 的语言特性无缝集成。特殊方法能实现各种功能,如自定义字符串表示、对象比较和上下文管理。随着你在 Python 学习之路上不断前进,你会发现更多特殊方法,从而充分利用其强大的对象模型。