简介
在这个实验中,你将学习如何通过重新定义特殊方法来自定义对象的行为。你还将改变用户定义对象的打印方式,并使对象具有可比性。
此外,你将学习创建一个上下文管理器。本实验中需要修改的文件是 stock.py。
使用 __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 对象 a 和 b 的属性(name、shares 和 price)具有相同的值,但 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))
让我们来分析一下这个方法的作用:
- 首先,它使用
isinstance函数检查other对象是否是Stock类的实例。这很重要,因为我们只想将Stock对象与其他Stock对象进行比较。 - 如果
other是Stock对象,它会比较self对象和other对象的属性(name、shares和price)。 - 只有当两个对象都是
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
让我们来分析一下这个上下文管理器的作用:
__init__:这是初始化方法。当我们创建redirect_stdout类的实例时,会传入一个文件对象。这个方法将该文件对象存储在实例变量self.out_file中。这样,它就记住了我们要将输出重定向到的位置。__enter__:- 首先,它保存当前的
sys.stdout。这很重要,因为我们稍后需要恢复它。 - 然后,它将当前的
sys.stdout替换为我们的文件对象。从这一点开始,任何原本会输出到控制台的内容都将输出到文件中。 - 最后,它返回文件对象。这很有用,因为我们可能想在
with代码块中使用这个文件对象。
- 首先,它保存当前的
__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 学习之路上不断前进,你会发现更多特殊方法,从而充分利用其强大的对象模型。