且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

《Python面向对象编程指南》——2.2 __format__()方法

更新时间:2022-09-29 08:48:58

本节书摘来自异步社区《Python面向对象编程指南》一书中的第2章,第2.2节,作者[美]Steven F. Lott, 张心韬 兰亮 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.2 __format__()方法

string.format()和内置的format()函数都使用了__format__()方法。它们都是为了获得给定对象的一个符合要求的字符串表示。

下面是给__format__()传参的两种方式。

  • someobject.__format__(""):当应用程序中出现format(someobject)或者"{0}".format(someobject)时,会默认以这种方式调用__format__()。在这些情况下,会传递一个空字符串,__format__()的返回值会以默认格式表示。
  • someobject.__format__(specification):当应用程序中出现format (someobject, specification)或者"{0:specification}".format (someobject)"时,会默认以这种方式调用__format__()。

注意,"{0!r}".format()和"{0!s}".format()并不会调用__format__()方法。它们会直接调用__repr__()或者__str__()。

当specification是""时,一种合理的返回值是return str(self),这为各种对象的字符串表示形式提供了明确的一致性。

在一个格式化字符串中,":"之后的文本都属于格式规范。当我们写"{0:06.4f}"时,06.4f是应用在项目0上的格式规范。

Python标准库的6.1.3.1节定义了一个复杂的数值规范,它是一个包括9个部分的字符串。这就是格式规范的基本语法,它的语法如下。

[[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%])?" )

这个正则表达式将规范分解为8个部分。第1部分同时包括了原本规范中的fill和alignment字段。我们可以利用它们定义我们的类中的数值类型的格式。

但是,Python格式规范的语法有可能不能很好地应用到我们之前定义的类上。所以,我们可能需要定义我们自己的规范化语法,并且使用我们自己的__format__()方法来处理它。如果我们定义的是数值类型,那么我们应该使用Python中内建的语法。但是,对于其他类型,没有理由坚持使用预定义的语法。

例如,下面是我们自定义的一个微型语言,用%r来表示rank,用%s来表示suit,用%代替%%,所有其他的文本保持不变。

我们可以用下面的格式化方法扩展Card类。

def __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

方法签名中,需要一个format_spec作为格式规范参数。如果没有提供这个参数,那么就会使用str()函数来返回结果。如果提供了格式规范参数,就会用rank、suit和%字符替换规范中对应的部分,来生成最后的结果。

这允许我们使用下面的方法来格式化牌。

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

其中,("%r of %s")作为格式化参数传入__format__()方法。通过这种方式,我们能够为描述自定义对象提供统一的接口。

或者,我们可以用下面的方法:

default_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__()。

2.2.1 内嵌格式规范

string.format()方法可以处理{}中内嵌的实例,替换其中的关键字,生成新的格式规范。这种替换是为了生成最后传入__format__()中的格式化字符串。通过使用这种内嵌的替换,我们可以使用一种更加简单的带参数的更加通用的格式规范,而不是使用相对复杂的数值格式。

下面是使用内嵌格式规范的一个例子,它让format参数中的width更容易改变:

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

我们定义了一个通用的格式,"{hand}{count:{width}d}",它需要一个width参数,才算是一个正确的格式规范。

通过width=参数提供的值会被用来替换{width}。替换完成后,完整的格式化字符串会作为__format__()方法的参数使用。

2.2.2 集合和委托格式规范

当格式化一个包含集合的对象时,我们有两个难题:如何格式化整个对象和如何格式化集合中的对象。以Hand为例,其中包含了Cards类的集合。我们会更希望可以将Hand中一部分格式化的逻辑委托给Card实例完成。

下面是Hand中的format()方法。

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

Hand集合中的每个Card实例都会使用format_specification参数。对于每一个Card对象,都会使用内嵌格式规范的方法,用format_specification创建基于"{0:{fs}}"的格式。通过这样的方法,一个Hand对象,player_hand,可以以下面的方法格式化:

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

这会将%r%s格式规范应用在Hand对象中的每个Card实例上。