与 Python 无缝集成:基本特殊方法(1)

467 查看

注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python

有许多特殊方法允许类与Python紧密结合,标准库参考将其称之为基本,基础或本质可能是更好的术语。这些特殊方法构成了创建与其他Python特性无缝集成的类的基础。

例如,对于给定对象的值,我们需要字符串表示。基类、对象都有默认的__repr__()__str__()用于提供对象的字符串表示。遗憾的是,这些默认表示不提供信息。我们总是想要覆盖这些默认定义中的一个或两个。我们可以看看__format__(),这个更复杂但目的和前面是一样的。

我们也可以看看其他转换,特别是__hash__()__bool__()、和__bytes__()。这些方法将一个对象转换成数字、true/false值或字符串的字节。例如,当我们实现__bool__()时,可以在if语句中使用对象,如下:if someobject:

然后,我们可以看看实现比较操作符的特殊方法__lt__()__le__()__eq__()__ne__()__gt__()__ge__()

这些基本的特殊方法在类中定义中几乎总是需要的。

最后我们来看看__new__()__del__(),因为这些方法的用例相当复杂。每当我们需要其他基本特殊方法时,是不需要这些的。

我们会详细看看如何添加这些特殊方法来扩大一个简单的类定义。我们需要看一下两个从对象继承的默认行为,这样我们可以了解需要什么样的覆盖以及何时真正需要。

__repr__()__str__() 方法

对于一个对象,Python有两种字符串表示方法。这些都和内置函数__repr__()__str__()__print__()以及string.format()方法紧密结合。

  • str()方法表示的对象通常是适用于人理解的,由对象的__str__()方法创建。
  • repr()方法表示的对象通常是适用于解释器理解的,可能是完整的Python表达式来重建对象。文档中是这样说的:对于许多类型,这个函数试图返回一个字符串,将该字符串传递给eval()会重新生成对象。
    这是由对象的__repr__()方法创建的。
  • print()函数会使用str()准备对象用于打印。
  • 字符串的format()方法也可以访问这些方法。当我们使用{!r}{!s}格式,我们分别需要__repr__()__str__()

首先让我们看看默认实现。

下面是一个简单的类层次结构:

我们已经定义了带有四个属性的两个简单类。

以下是一个与其中一个类对象的交互:

从这个输出知道默认的__str__()__repr__()实现不是很丰富。

当我们需要覆盖__str__()__repr__()时我们考虑下面两个广泛的设计用例:

  • 非集合对象:一个不包含其他对象集合的“简单”对象,通常不涉及非常复杂格式的集合。
  • 集合对象:一个包含一组更复杂格式的对象。

1. 非集合__str__()__repr__()

正如我们之前看到的,__str__()__repr__()的输出不是很丰富。我们几乎总是需要覆盖它们。以下是当没有包含集合的时候覆盖__str__()__repr__()的方法。这些方法从属于之前定义的Card类:

这两个方法依赖于传递内部对象的实例变量__dict__()format()函数。对于对象使用__slots__是不适合的;通常,这些是不可变的对象。在格式说明符中使用名称会使得格式化更显式。当然也使得格式模板更长了。在__repr__()中,我们传递内部__dict__加上对象的__class__作为关键字参数值给format()函数。

模板字符串使用两种格式说明符:

  • {__class__.__name__}模板也可以写成{__class__.__name__!s}从而当只提供简单字符串的类名时变得更显式。
  • {suit!r}{rank!r }模板都使用!r格式说明符生成属性值的repr()方法。

__str__()中,我们只有传递内部__dict__对象。格式化使用隐式的{!s}格式说明符来生成属性值的str()方法。

2. 集合__str__()__repr__()

当包含一个集合时,我们需要格式化集合中的每个项目以及整个容器。以下是带有__str__()__repr__()方法的简单集合:

__str__()方法是一个简单的设计,如下:

  1. 映射str()到集合中的每一项。这将在生成的每个字符串值上创建一个迭代器。
  2. 使用", ".join()合并所有项的字符串到一个长字符串中。

__repr__()方法是一个多部分的设计,如下:

  1. 映射repr()到集合中的每一项。这将在生成的每个字符串值上创建一个迭代器。
  2. 使用", ".join()合并所有项的字符串。
  3. 创建一组带有__class__的关键字、集合字符串和来自__dict__的各种属性。我们已经命名集合字符串为_card_str,与现有的属性不冲突。
  4. 使用"{__class__.__name__}({dealer_card!r}, {_card_str})".format()将类名与项目值的长字符串结合。我们使用!r进行格式化以确保属性也使用了repr()转换。

在某些情况下,这可以使一些事情变得稍微简单些。位置参数的格式化可以一定程度上缩短模板字符串的长度。

__format__() 方法

和内置函数format()一样,string.format()使用__format__()方法。这两个接口都是用来从给定对象得到像样的字符串的方式。

以下是将参数提供给__format__()的两种方法:

  • someobject.__format__(""):发生在应用程序使用format(someobject)或等价的"{0}".format(someobject)的时候。在这些情况下,提供了长度为零的字符串说明符。这会产生一个默认格式。
  • someobject.__format__(specification):发生在应用程序使用format(someobject, specification)或等价的"{0:specification}".format(someobject)的时候。

请注意,类似于"{0!r}".format()"{0!s}".format()的方法没有使用__format__()方法。它们直接使用了__repr__()__str__()

带有""说明符的合理响应是返回str(self)。它对各种对象的字符串提供了显式的一致性表示。

格式说明符必须都是文本,且在格式字符串的":"之后。当我们写"{0:06.4f}"06.4f是适用于第0项参数列表的格式说明符。

Python标准库文档中的6.1.3.1节定义了一个复杂的数值说明符作为九个部分字符串,这是格式说明符的一种迷你语言。有如下语法:

我们可以用正则表达式解析这些标准说明符,如下面代码片段所示:

这个正则将说明符拆分成八组。第一组和原说明符一样有fillalignment字段。我们可以使用这些得出我们已定义类的格式化数值数据。

然而,Python的格式说明符迷你语言可能不适用于我们的类定义。因此,我们需要定义我们自己的说明符迷你语言并在类的__format__方法中执行。如果我们定义数值类型,我们应该坚持预定义的迷你语言。然而,对于其他类型则没有理由再坚持预定义的语言。

作为一个示例,这里有个微不足道的语言,使用字符%r%s分别给我们展示牌值和花色。在结果字符串中%%字符变成%。所有其他字符是重复的。

我们可以通过格式化扩展我们的Card类,如下面代码片段所示:

这个定义会检查格式说明符。如果没有说明符,则使用str()函数。如果提供了一个说明符,会合拢牌值、花色和任何%字符格式说明符,将其转化为输出字符串。

这允许我们像下面这样格式化Card

格式说明符("%r of %s")被作为format的参数传递给我们的__format__()方法。使用这个,我们能够提供一个一致的接口来表示我们已经定义的类的对象。

或者,我们可以定义如下:

这个的优势在于把所有字符串放置到__format__()方法,而不是分开到的__format__()__str__()。劣势在于,我们不总是需要实现__format__(),但我们几乎总是需要实现__str__()

1. 嵌套格式化说明符

string.format()方法可以处理嵌套的{}实例来执行简单的关键字置换到格式说明符中。这个置换完成,会创建最终格式字符串并传递给类的__format__()方法。这种嵌套置换通过参数化通用说明符简化了某些相对复杂的数值格式。

下面的例子,我们可以在format参数中很容易的修改width

我们定义了一个通用的格式,"{hand:%r%s } {count:{width}d}",这需要一个width参数让它变成适用的格式说明符。

format()方法提供width=参数的值被用于替代{width}嵌套说明符。一旦被替换,最终格式会作为一个整体提供给__format__()方法。

2. 集合与委托格式说明符

格式化一个包括集合的复杂对象,有两个格式化问题:如何格式化整体对象以及如何格式化集合中的项目。当我们看到Hand,例如,我们看到我们有单独的Card类集合。我们需要Hand委托格式化细节给单独的Card实例。

下面是一个适用于Hand__format__()方法:

format_specification参数将用于每个Hand集合里面的Card实例。格式说明符"{0:{fs}}"使用嵌套格式说明符技术将format_specification字符串置入到应用于Card实例的创建。使用这种方法我们可以格式化Hand对象、player_hand,如下所示:

这将应用%r%s格式说明符到Hand对象中的Card实例。