转载

[译] 与 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__()

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

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

pythonclass Card:  insure = False  def __init__(self, rank, suit):   self.suit = suit   self.rank = rank   self.hard, self.soft = self._points() class NumberCard(Card):  def _points(self):   return int(self.rank), int(self.rank)  

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

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

>>> x=NumberCard( '2', '♣') >>> str(x) '<__main__.NumberCard object at 0x1013ea610>' >>> repr(x) '<__main__.NumberCard object at 0x1013ea610>' >>> print(x) <__main__.NumberCard object at 0x1013ea610> 

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

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

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

  • 集合对象:一个包含一组更复杂格式的对象。

1. 非集合 __str__()__repr__()

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

pythondef __repr__(self):     return "{__class__.__name__}(suit={suit!r}, rank={rank!r})".format(             __class__=self.__class__, **self.__dict__) def __str__(self):     return "{rank}{suit}".format(**self.__dict__) 

这两个方法依赖于传递内部对象的实例变量 __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__() 方法的简单集合:

pythonclass Hand:  def __init__(self, dealer_card, *cards):   self.dealer_card = dealer_card   self.cards = list(cards)  def __str__(self):   return ", ".join(map(str, self.cards))  def __repr__(self):   return "{__class__.__name__}({dealer_card!r}, {_cards_str})".format(     __class__=self.__class__, _cards_str=", ".join(     map(repr, self.cards)),**self.__dict__)  

__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节定义了一个复杂的数值说明符作为九个部分字符串,这是格式说明符的一种迷你语言。有如下语法:

[[fill]align][sign][#][0][width][,][.precision][type] 

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

re.compile( r"(?P<fill_align>.?[/</>=/^])?" "(?P<sign>[-+ ])?" "(?P<alt>#)?" "(?P<padding>0)?" "(?P<width>/d*)" "(?P<comma>,)?" "(?P<precision>/./d*)?" "(?P<type>[bcdeEfFgGnosxX%])?" ) 

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

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

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

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

pythondef __format__(self, format_spec):     if format_spec == "":         return str(self)     rs = format_spec.replace("%r", self.rank).replace("%s", self.suit)     rs = rs.replace("%%", "%")     return rs 

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

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

pythonprint( "Dealer Has {0:%r of %s}".format(hand.dealer_card)) 

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

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

pythondefault_format = "some specification" def __str__(self):     return self.__format__(self.default_format) def __format__(self, format_spec):     if format_spec == "":           format_spec = self.default_format     # process the format specification. 

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

1. 嵌套格式化说明符

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

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

pythonwidth = 6 for hand, count in statistics.items():     print( "{hand} {count:{width}d}".format(hand=hand, count=count, width=width)) 

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

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

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

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

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

pythondef __format__(self, format_specification):     if format_specification == "":         return str(self)     return ", ".join("{0:{fs}}".format(c, fs=format_specification) for c in self.cards) 

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

python"Player: {hand:%r%s}".format(hand=player_hand) 

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

正文到此结束
Loading...