介绍Python基本特殊方法

 

Python中有一些特殊方法,它们允许我们的类和Python更好地集成。在标准库参考(Standard Library Reference)中,它们被称为基本特殊方法,是与Python的其他特性无缝集成的基础。

例如,我们用字符串来表示一个对象的值。Object 基类包含了__repr__() 和__str__() 的默认实现,它们提供了一个对象的字符串描述。遗憾的是,这些默认的实现不够详细。我们几乎总会想重写它们中的一个或两个。我们还会介绍__format__() ,它更加复杂一些,但是和上面两个方法的作用相同。

我们还会介绍其他的转换方法,尤其是__hash__() 、__bool__() 和__bytes__() 。这些方法可以把一个对象转换成一个数字、一个布尔值或者一串字节。例如,当我们实现了__bool__() ,我们就可以像下面这样在if语句中使用我们的对象:if someobject: 。

接下来,我们会介绍实现了比较运算符的几个特殊方法:__lt__() 、__le__() 、__eq__() 、__ne__() 、__gt__() 和__ge__() 。

当我们定义一个类时,几乎总是需要使用这些基本的特殊方法。

我们会在最后介绍__new__() 和__del__() ,因为它们的使用更加复杂,而且相比于其他的特殊方法,我们并不会经常使用它们。

我们会详细地介绍如何用这些特殊方法来扩展一个简单类。我们需要了解从object 继承而来的默认行为,这样,我们才能理解应该在什么时候使用重写,以及如何使用它。

对于一个对象,Python提供了两种字符串表示。它们和内建函数repr() 、str() 、print() 及string.format() 的功能是一致的。

  • 通常,str() 方法表示的对象对用户更加友好。这个方法是由对象的__str__ 方法实现的。
  • repr() 方法的表示通常会更加技术化,甚至有可能是一个完整的Python表达式。文档中写道:

    对于大多数类型,这个方法会尝试给出和调用eval() 一样的结果。

这个方法是由__repr__() 方法实现的。

  • print() 函数会调用str() 来生成要输出的对象。
  • 字符串的format() 函数也可以使用这些方法。当我们使用{!r} 或者{!s} 格式时,我们实际上分别调用了__repr__() 或者__str__() 方法。

下面我们先来看一下这些方法的默认实现。

下面是一个很简单的类。

class 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)

我们定义了两个简单类,每个类包含4个属性。

下面是在命令行中使用NumberCard 类的结果。

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

  • 非集合对象 :一个不包括任何其他集合对象的“简单”对象,这类对象的格式化通常不会特别复杂。
  • 集合对象 :一个包含集合的对象,这类对象的格式化会更为复杂。

正如我们在前面看到的,__str__() 和__repr__() 并没有提供有用的信息,我们几乎总是需要重载它们。下面是当对象中不包括集合时我们可以使用的一种方法。这些方法是我们前面定义的Card 类的方法。

   def __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() 方法的属性值。

涉及集合的时候,我们需要格式化集合中的单个对象以及这些对象的整体容器。下面是一个包含__str__() 和__repr__() 的简单集合。

class 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.调用map 函数对集合中的每个对象使用str() 方法,这会基于返回的字符串集合创建一个迭代器。

2.用",".join() 将所有对象的字符串表示连接成一个长字符串。

__repr__() 方法更加复杂。

1.调用map 函数对集合中的每个对象应用repr() 方法,这会基于返回的结果集创建一个迭代器。

2.使用".".join() 连接所有对象的字符串表示。

3.用__class__ 、集合字符串和__dict__ 中的不同属性创建一些关键字。我们将集合字符串命名为_card_str ,这样就不会和现有的属性冲突。

4.用"{__class__.__name__}({dealer_card!r}, {_cards_str})".format() 来连接类名和之前连接的对象字符串。我们使用!r 格式化来保证属性也会使用repr() 来转换。

在一些情况下,我们可以优化这个过程,让它更加简单。在格式化中使用位置参数可以在一定程度上简化模板字符串。

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

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__() 方法的参数使用。

当格式化一个包含集合的对象时,我们有两个难题:如何格式化整个对象和如何格式化集合中的对象。以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实例上。

内置的hash( ) 函数默认调用了__hash__() 方法。哈希是一种将相对复杂的值简化为小整数的计算方式。理论上说,一个哈希值可以表示出源值的所有位。还有一些其他的哈希方法,会得出非常大的值,这样的算法通常用于密码学。

Python中有两个哈希库。其中,hashlib 可以提供密码级别的哈希函数,zlib模块包含两个高效的哈希函数:adler32() 和crc32() 。对于相对简单的值,我们不使用这些内置的函数,对于复杂的或者很大的值,这些内置的函数可以提供很大的帮助。

hash() 函数(以及与其相关联的__hash__() 方法)主要被用来创建set 、frozenset 和dict 这些集合类型的键。这些集合利用不可变对象的哈希值来高效地查找集合中的对象。

在这里,不可变性是非常重要的,我们还会多次提到它。不可变对象不会改变自己的状态。例如,数字3不会改变状态,它永远是3。对于更复杂的对象,同样可以有一个不变的状态。Python中的string 是不可变的,所以它们可以被用作map 和set 的键。

object 中默认的__hash__() 方法的实现是基于对象内部的ID值生成哈希值。这个ID值可以用id() 函数查看:

>>> x = object()
>>>hash(x)
269741571
>>>id(x)
4315865136
>>>id(x) / 16
269741571.0

可以看到,在笔者的系统中,哈希值是用对象的id除以16算出来的。对于不同的平台,哈希值的计算方法有可能不同。例如,CPython使用portable c库,而Jython则基于JVM。

这里最关键的是,在__hash__() 和内部的ID之间有很强的依赖关系。__hash__() 方法默认的行为是要保证每一个对象都是可哈希的,并且哈希值是唯一的,即使这些对象包含同样的值。

如果我们希望包含同样值的不同对象有相同的哈希值,就需要修改这个方法。在下一节中,我们会展示一个例子,这个例子中,具有相同值的两个Card 实例被当作相同的对象。

并非每个对象都需要提供一个哈希值,尤其是,当我们创建一个包含有状态、可改变对象的类时,这个类不应该返回哈希值。__hash__ 的定义应该是None 。

另外,对于不可变的对象,可以显式地返回一个哈希值,这样这个对象就可以用作字典中的一个键或者集合中的一个成员。在这种情况下,哈希值需要和相等性判断的实现方式兼容。相同的对象返回不同的哈希值是很糟糕的实践。反之,具有相同哈希值的对象互相不等是可以接受的。

我们将在比较运算符一章中讲解的__eq__() 方法也和哈希有紧密的关联。

等价性比较有3个层次。

  • 哈希值相等 :这意味两个对象可能相等。哈希值是判断两个对象有可能相等的快捷方式。如果哈希值不同,两个对象不可能相等,也不可能是同一个对象。
  • 比较结果相等 :这意味着两个对象的哈希值已经是相等的,这个比较用的是==运算符。如果结果相等,那么两个对象有可能是同一个。
  • IDD相等 :这意味着两个对象是同一个对象。它们的哈希值相同,并且使用==的比较结果相等,这个比较用的是is 运算符。

基本哈希法(Fundamental Law of Hash,FLH)定义如下:比较相等的对象的哈希值一定相同。

我们可以认为哈希比较是等价性比较的第1步。

反之则不成立,有相同哈希值的对象不一定相等。当创建集合或字典时,这带来了==预期的处理开销。我们没有办法从更大的数据结构中可靠地创建64位不同的哈希值,这时就会出现不同的对象的哈希值碰巧相等的情况。

巧合的是,当使用sets 和dicts 的时候,计算哈希值相等是预期的开销。这些集合中有一些内置的算法,当哈希值出现冲突的时候,它们会使用备用的位置。

对于以下3种情况,需要使用__eq__() 和__hash__() 方法来定义相等性测试和哈希值。

  • 不可变对象 :这些是不可以修改的无状态类型对象,例如tuple 、namedtuple 和frozenset 。我们针对这种情况有两个选择。

    • 不用自定义__hash__() 和__eq__() 。这意味着直接使用继承而来的行为。这种情况下,__hash__() 返回一个简单的函数代表对象的ID值,然后__eq__() 比较对象的ID值。默认相等性测试的行为有时候比较反常。我们的应用程序可能会需要Card(1, Clubs) 的两个实例来测试相等性和计算哈希值,但是默认情况下这不会发生。

    • 自定义__hash__() 和__eq__() 。请注意,这种自定义必须是针对不可变对象。

  • 可变对象 :这些都是有状态的对象,它们允许从内部修改。设计时,我们有一个选择如下。

    • 自定义__eq__() ,但是设置__hash__ 为None 。这些对象不可以用作dict 的键和set 中的项目。

除了上面的选择之外,还有一种可能的组合:自定义__hash__() 但使用默认的__eq__() 。但是,这简直是浪费代码,因为默认的__eq__() 方法和is 操作符是等价的。对于相同的行为,使用默认的__hash__() 方法只需要写更少的代码。

接下来,我们细致地分析一下以上3种选择。

首先,我们来看看默认行为是如何工作的。下面是一个使用了默认__hash__() 和__eq__() 的简单类。

class Card:
   insure= False
   def __init__( self, rank, suit, hard, soft ):
     self.rank= rank
     self.suit= suit
     self.hard= hard
     self.soft= soft
   def __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__)

class NumberCard( Card ):
   def __init__( self, rank, suit ):
     super().__init__( str(rank), suit, rank, rank )

class AceCard( Card ):
   def __init__( self, rank, suit ):
     super().__init__( "A", suit, 1, 11 )

class FaceCard( Card ):
   def __init__( self, rank, suit ):
     super().__init__( {11: 'J', 12: 'Q', 13: 'K' }[rank], suit, 10, 10 )

这是一个基本的不可变对象的类结构。我们还没有实现防止属性更新的特殊方法。我们会在下一章中介绍属性访问。

接下来,我们使用之前定义的类。

>>> c1 = AceCard( 1, '♣' )
>>> c2 = AceCard( 1, '♣' )

我们定义了两个看起来一样的Card 实例。我们可以用下面的代码获得id() 的值。

>>>print( id(c1), id(c2) )
4302577232 4302576976

可以看到,它们的id() 值不同,说明它们是两个不同的对象。这正是我们期望的行为。

我们还可以用is 运算符检测它们是否相同。

>>>c1 is c2
False

“is测试”基于id() 的值,它表明,这两个对象确实是不同的。

我们可以看到,它们的哈希值也是不同的。

>>>print( hash(c1), hash(c2) )
268911077 268911061

这些哈希值是根据id() 的值计算出来的。对于继承的方法,这正是我们期望的行为。在这个例子中,我们可以用下面的代码用id() 计算出哈希值。

>>>id(c1) / 16
268911077.0
>>>id(c2) / 16
268911061.0

由于哈希值不同,因此它们比较的结果肯定不同。这符合哈希和相等性的定义。但是,这和我们对这个类的预期不同。下面是一个相等性测试。

>>>print( c1 == c2 )
False

我们之前用相等的参数创建了这两个对象,但是它们不相等。在一些应用程序中,这样的行为可能不是所期望的。例如,当统计庄家牌的点数时,我们不想因为使用了6副牌而把同一张牌统计6次。

可以看到,由于我们可以把它们存入set 中,因此它们一定是不可变对象。

>>>print( set( [c1, c2] ) )
{AceCard(suit='♣', rank=1), AceCard(suit='♣', rank=1)}

这是标准参考库中记录的行为。默认地,我们会得到一个基于对象ID值的__hash__() 方法,这样每一个实例都是唯一的。但我们并非总是需要这样的行为。

下面是一个重载了__hash__() 和__eq__() 定义的简单类。

class Card2:
   insure= False
   def __init__( self, rank, suit, hard, soft ):
     self.rank= rank
     self.suit= suit
     self.hard= hard
     self.soft= soft
   def __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__)
   def __eq__( self, other ):
     return self.suit == other.suit and self.rank == other.rank
   def __hash__( self ):
     return hash(self.suit) ^ hash(self.rank)
class AceCard2( Card2 ):
   insure= True
   def __init__( self, rank, suit ):
     super().__init__( "A", suit, 1, 11 )

原则上,这个对象应该是不可变的。但是,我们还没有引入让它成为真正的不可变对象的机制。在第3章中,我们会探讨如何防止属性值被改变。

同时,请注意,上述代码中省略了上个例子中的两个子类,因为它们的代码和之前一样。

__eq__() 方法比较了两个初始值:suit 和rank ,而没有比较对象中从rank 继承而来的值。

21点的规则让这样的定义看起来有些奇怪。在21点中,suit 并不重要。那么是不是我们只需要比较rank 就可以了?我们是否应该再定义一个方法只比较rank ?或者,我们是否应该相信应用程序可以用合适的方式比较rank ?对于这3个问题,没有最好的答案,因为这些都是权宜的方法。

hash() 方法函数通过对两个基本数字的所有位取异或计算出一种新的位模式。用^运算符是另外一种快速但不好的方法。对于更复杂的对象,最好能使用更合理的方法。在开始自己造轮子之前可以先看看ziplib 。

接下来,我们看看这些类的对象是如何工作的。我们预期它们是等价的,并且能够用于set 和dict 中。以下是两个对象。

>>> c1 = AceCard2( 1, '♣' )
>>> c2 = AceCard2( 1, '♣' )

我们定义了两个看起来似乎相同的对象。但是,通过查看ID的值,我们可以确保它们事实上是不同的。

>>>print( id(c1), id(c2) )
4302577040 4302577296
>>>print( c1 is c2 )
False

这两个对象的id() 返回值不同。如果用is 运算符比较它们,可以看到,它们是两个不同的对象。

接下来,我们比较它们的哈希值。

>>>print( hash(c1), hash(c2) )
1259258073890 1259258073890

可以看到,哈希值是相同的,也就是说它们有可能相等。

==运算符比较的结果和我们预期的一样,它们是相等的。

>>>print( c1 == c2 )
True

由于这两个都是不可变的对象,因此我们可以将它们放进set 里。

>>>print( set( [c1, c2] ) )
{AceCard2(suit='♣', rank='A')}

对于复杂的不可变对象,这样的行为和我们预期的一致。我们必须同时重载这两个特殊方法来使结果一致并且有意义。

这个例子会继续使用Cards 类。可变的牌听起来有些奇怪,甚至是错误的。但是,我们只会对前面的例子做一个小改变。

下面的类层次结构中,我们重载了可变对象的__hash__() 和__eq__() 。

class Card3:
   insure= False
   def __init__( self, rank, suit, hard, soft ):
     self.rank= rank
     self.suit= suit
     self.hard= hard
     self.soft= soft
   def __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__)
   def __eq__( self, other ):
     return self.suit == other.suit and self.rank == other.rank
     # and self.hard == other.hard and self.soft == other.soft
   __hash__ = None
class AceCard3( Card3 ):
   insure= True
   def __init__( self, rank, suit ):
     super().__init__( "A", suit, 1, 11 )

接下来,让我们看看这些类对象的行为。我们期望的行为是,它们在比较中是相等的,但是不可以用于set 和dict 。我们创建了如下两个对象。

>>> c1 = AceCard3( 1, '♣' )
>>> c2 = AceCard3( 1, '♣' )

我们再次定义了两个看起来相同的牌。

下面,我们看看它们的ID值,确保它们实际上是不同的两个实例。

>>>print( id(c1), id(c2) )
4302577040 4302577296

和我们预期的一样,它们的ID值不同。接下来,让我们看看是否可以获得哈希值。

>>>print( hash(c1), hash(c2) )
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'AceCard3'

因为__hash__ 被设为None ,所以这些用Card3 生成的对象不可以被哈希,也就无法通过hash() 函数提供哈希值了。这正是我们预期的行为。

我们可以用下面的代码比较这两个对象。

>>>print( c1 == c2 )
True

比较的结果和我们预期的一样,这样我们就仍然可以使用==来比较它们,只是这两个对象不可以存放在set 中或者用作dict 的键。

下面是当我们试图将这两个对象插入set 中时的结果。

>>>print( set( [c1, c2] ) )
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: unhashable type: 'AceCard3'

当试图插入set 中时,我们得到了一个适当的异常。

很明显,对于生活中的一些不可变的对象,例如一张牌,这样的定义并不合适。这种定义方式更适合有状态的对象,例如Hand ,因为手中的牌时常改变。下面的部分,我们会展示第2个有状态对象的例子。

如果我们想要统计特定的Hand 实例,我们可能希望创建一个字典,然后将一个Hand 实例映射为一个计数。在映射中,不能使用一个可变的Hand 类作为键。但是,我们可以模仿set和frozenset 设计,定义两个类:Hand 和FrozenHand 。FrozenHand 允许我们“冻结”一个Hand 类,冻结的版本是不可变的,所以可以作为字典的键。

下面是一个简单的Hand 定义。

class 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__ )
   def __eq__( self, other ):
     return self.cards == other.cards and self.dealer_card == 
other.dealer_card
   __hash__ = None

这是一个包含适当的相等性比较的可变对象(__hash__ 是None )。

下面是不可变的Hand版本。

import sys
class FrozenHand( Hand ):
   def __init__( self, *args, **kw ):
     if len(args) == 1 and isinstance(args[0], Hand):
       # Clone a hand
       other= args[0]
       self.dealer_card= other.dealer_card
       self.cards= other.cards
     else:
       # Build a fresh hand
       super().__init__( *args, **kw )
   def __hash__( self ):
     h= 0
     for c in self.cards:
       h = (h + hash(c)) % sys.hash_info.modulus
     return h

不变的版本中有一个构造函数,从另外一个Hand 类创建一个Hand 类。同时,还定义了一个__hash__() 方法,用sys.hash_info.modulus 的值来计算cards 的哈希值。大多数情况下,这种基于模计算复合对象哈希值的方法能够满足我们的要求。

现在我们可以开始使用这些类了,如下所示。

stats = defaultdict(int)
d= Deck()
h = Hand( d.pop(), d.pop(), d.pop() )
h_f = FrozenHand( h )
stats[h_f] += 1

我们初始化了一个数据字典——stats ,作为一个可以存储整数的defaultdict 字典。我们也可以用collections.Counter 对象作为这个字典。

Hand 类冻结后,我们就可以将它用作字典的键,用这个键对应的值来统计实际的出牌次数。

Python中有很多关于真假性的定义。参考手册中列举了许多和False 等价的值,包括False 、0 、'' 、() 、[] 和{} 。其他大部分的对象都和True 等价。

通常,我们会用下面的语句来测试一个对象是否“非空”。

if some_object:
   process( some_object )

默认情况下,这个是内置的bool() 函数的逻辑。这个函数依赖于一个给定对象的__bool__() 方法。

默认的__bool__() 方法返回True 。我们可以通过下面的代码来验证这一点。

>>> x = object()
>>>bool(x)
True

对大多数类来说,这是完全正确的。大多数对象都不应该和False 等价。但是,对于集合,这样的行为并不总是正确的。一个空集合应该和False 等价,而一个非空集合应该返回True 。或许,应该给我们的Deck 集合对象增加一个类似的方法。

如果我们在封装一个列表,我们可能会写下面这样的代码。

def __bool__( self ):
   return bool( self._cards )

这段代码将__bool__() 的计算委托给了内部的集合_cards 。

如果我们在扩展一个列表,可能会写下面这样的代码:

def __bool__( self ):
   return super().__bool__( self )

这段代码使用了基类中定义的__bool__() 函数。

在这两个例子中,我们都将布尔值的计算委托给其他对象。在封装的例子中,我们委托给了一个内部的集合。在扩展的例子中,我们委托给了基类。不管是封装还是扩展,一个空集合的布尔值都是False 。这会让我们很清楚Deck 对象是否已经被处理完了。

现在,我们就可以像下面这样使用Deck 。

d = Deck()
while d:
   card= d.pop()
   # process the card

这段代码会处理完Deck 中所有的牌,当所有的牌都处理完时,也不会抛出IndexError 异常。

只有很少的情景需要我们把对象转换为字节。在第2部分“持久化和序列化”中,我们会详细探讨这个主题。

通常,应用程序会创建一个字符串,然后使用Python的IO类内置的编码方法将字符串转换为字节。对于大多数情况,这种方法就足够了。只有当我们自定义一种新的字符串时,我们会需要定义这个字符串的编码方法。

依据不同的参数,bytes() 函数的行为也不同。

  • bytes(integer) :返回一个不可变的字节对象,这个对象包含了给定数量的0x00值。
  • bytes(string) :这个版本会将字符串编码为字节。其他的编码和异常处理的参数会定义编码的具体过程。
  • bytes(something) :这个版本会调用something.__bytes__() 创建字节对象。这里不用编码或者错误处理参数。

基本的object 对象没有定义__bytes__() 。这意味着所有的类在默认情况下都没有提供__bytes__() 方法。

在一些特殊情况下,在写入文件之前,我们需要将一个对象直接编码成字节。通常使用字符串并且使用str类型为我们提供字符串的字节表示会更简单。要注意,当操作字节时,没有什么快捷方式可以解码文件或者接口中的字节。内置的bytes 类只能解码字符串,对于我们的自定义对象,是无法解码的。在这种情况下,我们需要解析从字节解码出来的字符串,或者我们可以显式地调用struct 模块解析字节,然后基于解析出来的值创建我们的自定义对象。

下面我们来看看如何把Card 编码和解码为字节。由于Card 只有52个可能的值,所以每一张牌都应该作为一个单独的字节。但是,我们已经决定用一个字符表示suit ,用另外一个字符表示rank 。此外,我们还需要适当地重构Card 的子类,所以我们必须对下面这些项目进行编码。

  • Card 的子类(AceCard 、NumberCard 、FaceCard )。
  • 子类的__init__() 参数。

注意,我们有一些__init__() 方法会将一个数值类型的rank 转换为一个字符串,导致丢失了原始的数值。为了使字节编码可逆,我们需要重新创建rank 的原始数值。

下面是__bytes__() 的一种实现,返回了Card 、rank 和suit 的UTF-8编码。

def __bytes__( self ):
   class_code= self.__class__.__name__[0]
   rank_number_str = {'A': '1', 'J': '11', 'Q': '12', 'K': '13'}.get( self.rank, self.rank )
   string= "("+" ".join([class_code, rank_number_str, self.suit,] ) + ")"
   return bytes(string,encoding="utf8")

这种实现首先用字符串表示Card 对象,然后将字符串编码为字节。这通常是最简单也是最灵活的方法。

当我们拿到一串字节时,我们可以将这串字节解码为一个字符串,然后将字符串转换为一个新的Card 对象。下面是基于字节创建Card 对象的方法。

def card_from_bytes( buffer ):
   string = buffer.decode("utf8")
   assert string[0 ]=="(" and string[-1] == ")"
   code, rank_number, suit = string[1:-1].split()
   class_ = { 'A': AceCard, 'N': NumberCard, 'F': FaceCard }[code]
   return class_( int(rank_number), suit )

在上面的代码中,我们将字节解码为一个字符串。然后我们将字符串解析为数值。基于这些值,现在我们可以重建原始的Card 对象。

我们可以像下面这样生成一个Card 对象的字节表示。

b= bytes(someCard)

然后我们可以用生成的字节重新创建Card 对象。

someCard = card_from_bytes(b)

需要特别注意的是,通常自己定义字节表示是非常有挑战性的,因为我们试图表示一个对象的状态。Python中已经内置了很多字节表示的方式,通常这些方法足够我们使用了。

如果需要定义一个对象底层的字节表示方式,最好使用pickle或者json模块。在第9章“序列化和保存——JSON、YAML、Pickle、CSV和XML”中,我们会详细探讨这个主题。

Python有6个比较运算符。这些运算符分别对应一个特殊方法的实现。根据文档,运算符和特殊方法的对应关系如下所示。

  • x < y 调用x.__lt__(y) 。
  • x <=y 调用x.__le__(y) 。
  • x == y 调用x.__eq__(y) 。
  • x != y 调用x.__ne__(y) 。
  • x &gt; y 调用x.__gt__(y) 。
  • x &gt;= y 调用x.__ge__(y) 。

我们会在第7章“创建数值类型”中再探讨比较运算符。

对于实际上使用了哪个比较运算符,还有一条规则。这些规则依赖于作为左操作数的对象定义需要的特殊方法。如果这个对象没有定义,Python会尝试改变运算顺序。

  下面是两条基本的规则:
首先,运算符的实现基于左操作数:A < B相当于A.__lt__(B) 。 其次,相反的运算符的实现基于右操作数:A < B相当于B.__gt__(A) 。
如果右操作数是左操作数的一个子类,那这样的比较基本不会有什么异常发生;同时,Python会首先检测右操作数,以确保这个子类可以重载基类。

下面,我们通过一个例子看看这两条规则是如何工作的,我们定义了一个只包含其中一个运算符实现的类,然后把这个类用于另外一种操作。

下面是我们使用类中的一段代码。

class BlackJackCard_p:
   def __init__( self, rank, suit ):
     self.rank= rank
     self.suit= suit
   def __lt__( self, other ):
     print( "Compare {0} < {1}".format( self, other ) )
     return self.rank < other.rank
   def __str__( self ):
     return "{rank}{suit}".format( **self.__dict__ )

这段代码基于21点的比较规则,花色对于大小不重要。我们省略了比较方法,看看当缺少比较运算符时,Python将如何回退。这个类允许我们进行<比较。但是有趣的是,通过改变操作数的顺序,Python也可以使用这个类进行>比较。换句话说,x <y 和y >x 是等价的。这遵从了镜像反射法则;在第7章“创建数值类型”中,我们会再探讨这个部分。

当我们试图评估不同的比较运算时就会看到这种现象。下面,我们创建两个Cards 类,然后用不同的方式比较它们。

>>> two = BlackJackCard_p( 2, '♠' )
>>> three = BlackJackCard_p( 3, '♠' )
>>> two < three
Compare 2♠ < 3♠
True
>>> two > three
Compare 3♠ < 2♠
False
>>> two == three
False
>>> two <= three
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: unorderable types: BlackJackCard_p() <= BlackJackCard_p()

从代码中,我们可以看到,two < three 调用了two.__lt__(three) 。

但是,对于two > three ,由于没有定义__gt__() ,Python使用three.__lt__(two) 作为备用的比较方法。

默认情况下,__eq__() 方法从object 继承而来,它比较不同对象的ID值。当我们用于==或!=比较对象时,结果如下。

>>> two_c = BlackJackCard_p( 2, '♣' )
>>>two == two_c
False

可以看到,结果和我们预期的不同。所以,我们通常都会需要重载默认的__eq__() 实现。

此外,逻辑上,不同的运算符之间是没有联系的。但是从数学的角度来看,我们可以基于两个运算符完成所有必需的比较运算。Python没有实现这种机制。相反,Python默认认为下面的4组比较是等价的。

x < y ≡ y > x

x ≤ y ≡ y ≥ x

x = y ≡ y = x

x ≠ y ≡ y ≠ x

这意味着,我们必须至少提供每组中的一个运算符。例如,我可以提供__eq__() 、__ne__() 、__lt__() 和__le__() 的实现。

@functools.total_ordering 修饰符打破了这种默认行为的局限性,它可以从__eq__() 或者__lt__() 、__le__() 、__gt__() 和__ge__() 的任意一个中推断出其他的比较方法。在第7章“创建数值类型”中,我们会详细探讨这种方法。

当设计比较运算符时,要考虑两个因素。

  • 如何比较同一个类的两个对象。
  • 如何比较不同类的对象。

对于一个有许多属性的类,当我们研究它的比较运算符时,通常会觉得有很明显的歧义。或许这些比较运算符的行为和我们的预期不完全相同。

再次考虑我们21点的例子。例如card1==card2 这样的表达式,很明显,它们比较了rank 和suit ,对吗?但是,这总是和我们的预期一致吗?毕竟,suit 对于21点中的比较结果没有影响。

如果我们想决定是否能分牌,我们必须决定下面两个代码片段哪一个更好。下面是第1个代码段。

if hand.cards[0] == hand.cards[1]

下面是第2个代码段。

if hand.cards[0].rank == hand.cards[1].rank

虽然其中一个更短,但是简洁的并不总是最好的。如果我们比较牌时只考虑rank ,那么当我们创建单元测试时会有问题,例如一个简单的TestCase.assertEqual() 方法就会接受很多不同的Cards 对象,但是一个单元测试应该只关注正确的Cards 对象。

例如card1 <= 7 ,很明显,这个表达式想要比较的是rank 。

我们是否需要在一些比较中比较Cards 对象所有的属性,而在另一些比较中只关注rank ?如果我们想要按suit 排序需要做什么?而且,相等性比较必须同时计算哈希值。我们在哈希值的计算中使用了多个属性值,那么也必须在相等性比较中使用它们。在这种情况下,很明显相等性的比较必须比较完整的Card 对象,因为在计算哈希值时使用了rank 和suit 。

但是,对于Card 对象间的排序比较,应该只需要基于rank 。类似地,如果和整数比较,也应该只关注rank 。对于判断是否要发牌的情况,很明显,用hand.cards[0]. rank == hand.cards[1].rank 判断是很好的方式,因为它遵守了发牌的规则。

下面我们通过一个更完整的BlackJackCard 类来看一下简单的同类比较。

class BlackJackCard:
   def __init__( self, rank, suit, hard, soft ):
     self.rank= rank
     self.suit= suit
     self.hard= hard
     self.soft= soft
   def __lt__( self, other ):
     if not isinstance( other, BlackJackCard ): return 
NotImplemented
     return self.rank < other.rank

   def __le__( self, other ):
     try:
       return self.rank <= other.rank
     except AttributeError:
       return NotImplemented
   def __gt__( self, other ):
     if not isinstance( other, BlackJackCard ): return 
NotImplemented
     return self.rank > other.rank
   def __ge__( self, other ):
     if not isinstance( other, BlackJackCard ): return 
NotImplemented
     return self.rank >= other.rank
   def __eq__( self, other ):
     if not isinstance( other, BlackJackCard ): return 
NotImplemented
     return self.rank == other.rank and self.suit == other.suit
   def __ne__( self, other ):
     if not isinstance( other, BlackJackCard ): return 
NotImplemented
     return self.rank != other.rank and self.suit != other.suit
   def __str__( self ):
     return "{rank}{suit}".format( **self.__dict__)

现在我们定义了6个比较运算符。

我们已经展示了两种类型检查的方法:显式的和隐式的。显式的类型检查调用了isinstance() 。隐式的类型检查使用了一个try: 语句块。理论上,使用try: 语句块有一个小小的优点:它避免了重复的类名称。有的人完全可能会想创建一种和这个BlackJackCard 兼容的Card 类的变种,但是并没有适当地定义为一个子类。这时候使用isinstance() 有可能导致一个原本正确的类出现异常。

使用try: 语句块可以让一个碰巧也有一个rank 属性的类仍然可以正常工作。不用担心这样会带来什么难,因为它除了在此处被真正使用外,这个类在程序的其他部分都无法被正常使用。而且,谁会真的去比较一个Card 的实例和一个金融系统中恰好有rank 属性的类呢?

后面的例子中,我们主要会关注try: 语句块的使用。isinstance() 方法是Python中惯用的方式,而且也被广泛应用。我们通过显式地返回NotImplemented 告诉Python这个运算符在当前类型中还没有实现。这样,Python 可以尝试交换操作数的顺序来看看另外一个操作数是否提供了对应的实现。如果没有找到正确的运算符,那么Python会抛出TypeError 异常。

我们没有给出3个子类和工厂函数:card21() 的代码,它们作为本章的习题。

我们也没有给出类内比较的代码,这个我们会在下一个部分中详细讲解。用上面定义的这个类,我们可以成功地比较不同的牌。下面是一个创建并比较3张牌的例子。

>>> two = card21( 2, '♠' )
>>> three = card21( 3, '♠' )
>>> two_c = card21( 2, '♣' )

用上面定义的Cards 类,我们可以进行像下面这样的一系列比较。

>>> two == two_c
False
>>> two.rank == two_c.rank
True
>>> two< three
True
>>> two_c < three
True

这个类的行为与我们预期的一致。

我们会继续以BlackJackCard 类为例来看看当两个比较运算中的两个操作数属于不同的类时会发生什么。

下面我们将一个Card 实例和一个int 值进行比较。

>>> two = card21( 2, '♣' )
>>> two < 2
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: unorderable types: Number21Card() < int()
>>> two > 2
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: unorderable types: Number21Card() > int()

可以看到,这和我们预期的行为一致,BlackJackCard 的子类Number21Card 没有实现必需的特殊方法,所以产生了一个TypeError 异常。

但是,再考虑下面的两个例子。

>>> two == 2
False
>>> two == 3
False

为什么用等号比较可以返回结果呢?因为当Python遇到NotImplemented 的值时,会尝试交换两个操作数的顺序。在这个例子中,由于整型的值定义了一个int.__eq__() 方法,所以可以和一个非数值类型的对象比较。

接下来,我们定义Hand 类,这样它可以有意义地比较不同的类。和其他的比较一样,我们必须确定我们要比较的内容。

对于Hand 类之间相等性的比较,我们应该比较所有的牌。

而对于Hand 类之间顺序的比较,我们需要比较每一个Hand 对象的属性。对于与int 值的比较,我们应该将当前Hand 对象的总和与int 值进行比较。为了获得当前总和,我们需要弄清21点中硬总和与软总和的细微差别。

当手上有一张A牌时,下面是两种可能的总和。

  • 软总和把A牌当作11点。如果软总和超过21点,那么这张A牌就不可用。
  • 硬总和把A牌当作1点。

也就是说,手中牌的总和不是简单地累加所有的牌面值。

首先,我们需要确定手中是否有A牌。然后,我们才能确定是否有一个可用的(小于或者等于21点)的软总和。否则,我们就要使用硬总和。

对于确定子类与基类的关系逻辑的实现是否依赖于isinstance() ,是判断多态使用是否合理的标志。通常,这样的做法不符合基本的封装原则。一个好的子类定义应该只依赖于相同的方法签名。理想状态下,类的定义是不可见的,我们也没有必要知道类内部的细节。而不合理的多态则会广泛地使用isinstance() 。在一些情况下,isinstance() 是必需的,尤其是当使用Python内置的类时。但是,我们不应该向内置类中追加任何方法函数,而且为了加入一个多态的方法而去使用继承也是不值得的。

在一些没有继承的特殊方法中,我们可以看到必须使用isinstance() 来实现不同类的对象间的交互。在下一个部分中,我们会展示在没有关系的类间使用isinstance() 的方法。

对于与Card相关的类,我们希望用一个方法(或者一个属性)就可以识别一张A牌,而不需要调用isinstance() 。这个方法是一个多态的辅助方法,它可以确保我们能够辨别不同的牌。

这里,我们有两个选择。

  • 新增一个类级别的属性。
  • 新增一个方法。

由于保险注的存在,有两个原因让我们检测是否有A牌。如果庄家牌是A牌,那么就会触发一个保险注。如果庄家或者玩家的手上有A牌,那么需要对比软总和与硬总和。

对于A牌而言,硬总和与软总和总是需要通过card.soft-card.hard 的值来区分。仔细看看AceCard 的定义就可以知道这个值是10。但是,仔细地分析这个类的实现,我们就会发现这个版本的实现会破坏封装性。

我们可以把BlackJackCard 看作不可见的,所以我们仅仅需要比较card.soft- card.hard!=0 的值是否为真。如果结果为真,那么我们就可以用硬总和与软总和算出手中牌的总和。

下面是total 方法的一种实现,它使用硬总和与软总和的差值计算出当前手中牌的总和。

def total( self ):
   delta_soft = max( c.soft-c.hard for c in self.cards )
   hard = sum( c.hard for c in self.cards )
   if hard+delta_soft <= 21: return hard+delta_soft
   return hard

我们用delta_soft 记录硬总和与软总和之间的最大差值。对于其他牌而言,这个差值是0。但是对于A牌,这个差值不是0。

得到了delta_soft 和硬总和之后,我们就可以决定返回值是什么。如果hard + delta_soft 小于或者等于21,那么就返回软总和。如果软总和大于21,那么就返回硬总和。

我们可以考虑把21定义为宏。有时候一个有意义的名字比一个字面值更有用。但是,因为21在21点中几乎不可能变成其他值,所以很难找到其他比21更有意义的名字。

定义了Hand 对象的总和之后,我们可以合理地定义Hand 实例间的比较函数和Hand 与int 间的比较函数。为了确定我们在进行哪种类型的比较,必须使用isinstance() 。

下面是定义了比较方法的Hand 类的部分代码。

class 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__ )


   def __eq__( self, other ):
     if isinstance(other,int):
       return self.total() == other
     try:
       return (self.cards == other.cards 
         and self.dealer_card == other.dealer_card)
     except AttributeError:
       return NotImplemented
   def __lt__( self, other ):
     if isinstance(other,int):
       return self.total() < other
     try:
       return self.total() < other.total()
     except AttributeError:
       return NotImplemented
   def __le__( self, other ):
     if isinstance(other,int):
       return self.total() <= other
     try:
       return self.total() <= other.total()
     except AttributeError:
       return NotImplemented
   __hash__ = None
   def total( self ):
     delta_soft = max( c.soft-c.hard for c in self.cards )
     hard = sum( c.hard for c in self.cards )
     if hard+delta_soft <= 21: return hard+delta_soft
     return hard

这里我们只定义了3个比较方法。

为了和Hand 对象交互,我们需要一些Card 对象。

>>> two = card21( 2, '♠' )
>>> three = card21( 3, '♠' )
>>> two_c = card21( 2, '♣' )
>>> ace = card21( 1, '♣' )
>>> cards = [ ace, two, two_c, three ]

我们会把这些牌用于两个不同Hand 对象。

第1个Hand对象有一张不相关的庄家牌和我们上面创建的4张牌,包括一张A牌:

>>> h= Hand( card21(10,'♠'), *cards )
>>> print(h)
A♣, 2♠, 2♣, 3♠
>>> h.total()
18

软总和是18,硬总和是8。

下面是第2个Hand 对象,除了上面第1个Hand 对象的4张牌,还包括了另一张牌。

>>> h2= Hand( card21(10,'♠'), card21(5,'♠'), *cards )
>>> print(h2)
5♠, A♣, 2♠, 2♣, 3♠
>>> h2.total()
13

硬总和是13,由于总和超过了21点,所以没有软总和。

从下面的代码中可以看到,Hand 对象之间的比较结果和我们预期的一致。

>>> h < h2
False
>>> h > h2
True

我们可以用比较运算符对Hand 对象排序。

我们也可以像下面这样把Hand 对象和int 比较。

>>> h == 18
True
>>> h < 19
True
>>> h > 17
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: unorderable types: Hand() > int()

只要Python没有强制使用后备的比较方法,Hand 对象和整数的比较就可以很好地工作。上面的例子也展示了当没有定义__gt__() 方法时会发生什么。Python检查另一个操作数,但是整数17也没有任何与Hand 相关的__lt__() 方法定义。

我们可以添加必要的__gt__() 和__ge__() 函数,这样Hand 就可以很好地与整数进行比较。

__del__() 方法有一个让人费解的使用场景。

这个方法的目的是在将一个对象从内存中清除之前,可以有机会做一些清理工作。如果使用上下文管理对象或者with 语句来处理这种需求会更加清晰,这也是第5章“可调用对象和上下文的使用”的内容。对于Python的垃圾回收机制而言,创建一个上下文比使用__del__() 更加容易预判。

但是,如果一个Python对象包含了一些操作系统的资源,__del__() 方法是把资源从程序中释放的最后机会。例如,引用了一个打开的文件、安装好的设备或者子进程的对象,如果我们将资源释放作为__del__() 方法的一部分实现,那么我们就可以保证这些资源最后会被释放。

很难预测什么时候__del__() 方法会被调用。它并不总是在使用del 语句删除对象时被调用,当一个对象因为命名空间被移除而被删除时,它也不一定被调用。Python文档中用不稳定来描述__del__() 方法的这种行为,并且提供了额外的关于异常处理的注释:运行期的异常会被忽略,相对地,会使用sys.stderr 打印一个警告。

基于上面的这些原因,通常更倾向于使用上下文管理器,而不是实现__del__() 。

CPython的实现中,对象会包括一个引用计数器。当对象被赋值给一个变量时,这个计数器会递增;当变量被删除时,这个计数器会递减。当引用计数器的值为0时,表示我们的程序不再需要这个对象并且可以销毁这个对象。对于简单对象,当执行删除对象的操作时会调用__del__() 方法。

对于包含循环引用的复杂对象,引用计数器有可能永远也不会归零,这样就很难让__del__() 被调用。

我们用下面的一个类来看看这个过程中到底发生了什么。

class Noisy:
   def __del__( self ):
     print( "Removing {0}".format(id(self)) )

我们可以像下面这样创建和删除这个对象。

>>> x= Noisy()
>>>del x
Removing 4313946640

我们先创建,然后删除了Noisy 对象,几乎是立刻就看到了__del__() 方法中输出的消息。这也就是说当变量x 被删除后,引用计数器正确地归零了。一旦变量被删除,就没有任何地方引用Noisy 实例,所以它也可以被清除。

下面是浅复制中一种常见的情形。

>>> ln = [ Noisy(), Noisy() ]
>>> ln2= ln[:]
>>> del ln

Python没有响应del 语句。这说明这些Noisy 对象的引用计数器还没有归零,肯定还有其他地方引用了它们,下面的代码验证了这一点。

>>> del ln2
Removing 4313920336
Removing 4313920208

ln2 变量是ln 列表的一个浅复制。有两个列表引用了Noisy 对象,所以在这两个列表被删除并且引用计数器归零之前,Python不会销毁这两个Noisy 对象。

还有很多种创建浅复制的方法。下面是其中的一些。

a = b = Noisy()
c = [ Noisy() ] * 2

这里的关键是,由于浅复制在Python中非常普遍,所以我们往往对存在的对象的引用感到非常困惑。

下面是一种常见的循环引用的情形。一个父类包含一个子类的集合,同时集合中的每个子类实例又包含父类的引用。

下面我们用这两个类来看看循环引用。

class Parent:
   def __init__( self, *children ):
     self.children= list(children)
     for child in self.children:
       child.parent= self
   def __del__( self ):
     print( "Removing {__class__.__name__} {id:d}".
format( __class__=self.__class__, id=id(self)) )
class Child:
   def __del__( self ):
     print( "Removing {__class__.__name__} {id:d}".
format( __class__=self.__class__, id=id(self)) )

一个Parent 的instance 包括一个children 的列表。

每一个Child 的实例都有一个指向Parent 类的引用。当向Parent 内部的集合中插入新的Child 实例时,这个引用就会被创建。

我们故意把这两个类写得比较复杂,所以下面让我们看看当试图删除对象时,会发生什么。

>>>> p = Parent( Child(), Child() )
>>> id(p)
4313921808
>>> del p

Parent 和它的两个初始Child 实例都不能被删除,因为它们之间互相引用。

下面,我们创建一个没有Child 集合的Parent 实例。

>>> p= Parent()
>>> id(p)
4313921744
>>> del p
Removing Parent 4313921744

和我们预期的一样,这个Parent 实例成功地被删除了。

由于互相之间有引用存在,因此我们不能从内存中删除Parent 实例和它包含的Child 实例的集合。如果我们导入垃圾回收器的接口——gc ,我们就可以回收和显示这些不能被删除的对象。

下面的代码中,我们使用了gc.collect() 方法回收所有定义了__del__() 方法但是无法被删除的对象。

>>> import gc
>>> gc.collect()
174
>>> gc.garbage
[<__main__.Parent object at 0x101213910>, <__main__.Child object at 0x101213890>, <__main__.Child object at 0x101213650>, <__main__.Parent object at 0x101213850>, <__main__.Child object at 0x1012130d0>, <__main__.Child object at 0x101219a10>, <__main__.Parent object at 0x101213250>, <__main__.Child object at 0x101213090>, <__main__.Child object at 0x101219810>, <__main__.Parent object at 0x101213050>, <__main__.Child object at 0x101213210>, <__main__.Child object at 0x101219f90>, <__main__.Parent object at 0x101213810>, <__main__.Child object at 0x1012137d0>, <__main__.Child object at 0x101213790>]

可以看到,我们的Parent 对象(例如,4313921808的ID = 0x101213910)在不可删除的垃圾对象列表中很突出。为了让引用计数器归零,我们需要删除所有Parent 对象中的children 列表,或者删除所有Child 实例中对Parent的引用。

注意,即使把清理资源的代码放在__del__() 方法中,我们也没办法解决循环引用的问题。因为__del__() 方法是在循环引用被解除并且引用计数器已经归零之后被调用的。当有循环引用时,我们不能只是简单地依赖于Python中计算引用数量的机制来清理内存中的无用对象。我们必须显式地解除循环引用或者使用可以保证垃圾回收的weakref引用。

如果我们需要循环引用,但是又希望将清理资源的代码写在__del__() 中,这时候我们可以使用弱引用。循环引用的一个常见场景是互相引用:一个父类中包含了一个集合,集合中的每一个实例也包含了一个指向父类的引用。如果一个Player 对象中包含多个Hand 实例,那么在每一个Hand 对象中都包括一个指向对应的Player 类的引用可能会更方便。

默认的对象间的引用可以被称为强引用,但是,叫直接引用可能更好。Python的引用计数机制会直接使用它们,而且如果引用计数无法删除这些对象的话,垃圾回收机器也能及时发现。它们是不可忽略的对象。

对一个对象的强引用就是直接引用,下面是一个例子。

当我们遇到如下语句。

a= B()

变量a直接引用了B类的一个对象。此时B的引用计数至少是1,因为a变量包含了一个指向它的引用。

想要找个一个弱引用相关的对象需要两个步骤。一个弱引用会调用x.parent() ,这个函数将弱引用作为一个可调用对象来查找它真正的父对象。这个过程让引用计数器得以归零,垃圾回收器可以回收引用的对象,但是不回收这个弱引用。

weakref 定义了一系列使用了弱引用而没有使用强引用的集合。它让我们可以创建一种特殊的字典类型,当这种字典的对象没有用时,可以保证被垃圾回收。

我们可以修改Parent 和Child 类,在Child 指向Parent 的引用中使用弱引用,这样就可以简单地保证无用对象会被销毁。

下面是修改后的类,它在Child 指向Parent 的引用中使用了弱引用。

import weakref
class Parent2:
   def __init__( self, *children ):
     self.children= list(children)
     for child in self.children:
       child.parent= weakref.ref(self)
   def __del__( self ):
     print( "Removing {__class__.__name__} {id:d}".format( __class__= self.__class__, id=id(self)) )

我们将child 中的parent 引用改为一个weakref 对象的引用。

Child 类中,我们必须用上面说的两步操作来定位parent 对象:

p = self.parent()
if p is not None:
   # process p, the Parent instance
else:
   # the parent instance was garbage collected.

我们可以显式地确认引用的对象是否已经找到,因为有可能该引用已经变成虚引用。

当我们使用这个新的Parent2 类时,可以看到引用计数成功地归零同时对象也被删除了:

>>> p = Parent2( Child(), Child() )
>>> del p
Removing Parent2 4303253584
Removing Child 4303256464
Removing Child 4303043344

当一个weakref 引用变成死引用时(因为引用被销毁了),我们有3个可能的方案。

  • 重新创建引用对象,或重新从数据库中加载。
  • 当垃圾回收器在低内存情况下错误地删除了一些对象时,使用warnings 模块记录调试信息。
  • 忽略这个问题。

通常,weakref 引用变成死引用是因为响应的对象已经被删除了。例如,变量的作用域已经执行结束,一个没有用的命名空间,应用程序正在关闭。对于这个原因,通常我们会采取第3种响应方法。因为试图创建这个引用的对象时很可能马上就会被删除。

__del__() 最常见的用途是确保文件被关闭。

通常,包含文件操作的类都会有类似下面这样的代码。

__del__ = close

这会保证__del__() 方法同时也是close() 方法。

其他更复杂的情况最好使用上下文管理器。详情请看第5章“可调用对象和上下文的使用”,我们会在第5章提供更多和上下文管理器有关的信息。

__new__ 方法的一个用途是初始化不可变对象。__new__() 方法中允许创建未初始化的对象。这允许我们在__init__() 方法被调用之前先设置对象的属性。

由于不可变类的__init__() 方法很难重载,因此__new__ 方法提供了一种扩展这种类的方法。

下面是一个错误定义的类,我们定义了float 的一个包含单位信息的版本。

class Float_Fail( float ):
   def __init__( self, value, unit ):
     super().__init__( value )
     self.unit = unit

我们试图(不合理地)初始化一个不可变对象。

下面是当我们试图使用这个类时会发生的情况。

>>> s2 = Float_Fail( 6.5, "knots" )
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
TypeError: float() takes at most 1 argument (2 given)

可以看到,对于内置的float 类,我们不能简单地重载__init__ 方法。对于其他的内置不可变类型,也有类似的问题。我们不能在不可变对象self 上设置新的属性值,因为这是不可变性的定义。我们只能在对象创建的过程中设置属性值,对象创建之后__new__() 方法就会被调用。

__new__() 方法天生就是一个静态方法。即使没有使用@staticmethod 修饰符,它也是静态的。它没有使用self 变量,因为它的工作是创建最终会被赋值给self 变量的对象。

这种情况下,我们会使用的方法签名是__new__( cls, *args, **kw) 。cls 变量是准备创建的类的实例。下一个部分关于元类型的例子,会比这里展示的args 的参数序列更加复杂。

__new__() 方法的默认实现如下。

return super().__new__( cls ) 将调用基类的__new__() 方法创建对象。这个工作最终委托给了object.__new__() ,这个方法创建了一个简单的空对象。除了cls 以外,其他的参数和关键字最终都会传递给__init__() 方法,这是Python定义的标准行为。

除了有下面的两个例外,这就是我们期望的行为。

  • 当我们需要继承一个不可变的类的时候,我们会在后面的部分详细讲解。
  • 当我们需要创建一个元类型的时候,这是下一个部分的主题,因为它与创建不可变对象是完全不同的。

当创建一个内置的不可变类型的子类时,不能重载__init__() 方法。取而代之的是,我们必须通过重载__new__() 方法在对象创建的过程中扩展基类的行为。下例是扩展float 类的正确方式。

class Float_Units( float ):
   def __new__( cls, value, unit ):
     obj= super().__new__( cls, value )
     obj.unit= unit
     return obj

上面的代码在对象创建的过程中设置了一个属性的值。

下面的代码使用上面定义的类创建了一个带单位的浮点数。

>>>speed= Float_Units( 6.5, "knots" )
>>>speed
6.5
>>>speed * 10
65.0
>>> speed.unit
'knots'

注意,像speed * 10 这种表达式不会创建一个Float_Units 对象。这个类的定义继承了float 中所有的运算符;float 的所有算术特殊方法也都只会创建float 对象。创建Float_Units 对象会在第7章“创建数值类型”中介绍。

__new__() 方法的另一种用途,作为元类型的一部分,主要是为了控制如何创建一个类。这和之前的如何用__new__() 控制一个不可变对象是完全不同的。

一个元类型创建一个类。一旦类对象被创建,我们就可以用这个类对象创建不同的实例。所有类的元类型都是type ,type() 函数被用来创建类对象。

另外,type() 函数还可以被用作显示当前对象类型。

下面是一个很简单的例子,直接使用type() 作为构造器创建了一个新的但是几乎完全没有任何用处的类:

Useless= type("Useless",(),{})

一旦我们创建了这个类,我们就可以开始创建这个类的对象。但是,这些对象什么都做不了,因为我们没有定义任何方法和属性。

为了最大化利用这个类,在下面的例子中,我们使用这个新创建的Useless 类来创建对象。

>>> Useless()
<__main__.Useless object at 0x101001910>
>>> u=_
>>> u.attr= 1 
>>> dir(u)
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', 
'__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', 
'__hash__', '__init__', '__le__', '__lt__', '__module__', '__ne__', 
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', 
'__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'attr']

我们可以向这个类的对象中增加属性。至少,作为一个对象,它工作得很好。

这样的类定义与使用types.SimpleNamespace 或者像下面这样定义一个类的方式几乎相同。

class Useless:
   pass

这带来一个重要的问题:为什么我们一开始要复杂化定义一个类的方法呢?

答案是,类中一些默认的特性无法应用到一些特殊的类上。下面,我们会列举4种应该使用元类型的场景。

  • 我们可以使用元类型来保留一个类源码中的文本信息。一个使用内置的type创建的类型会使用dict来存储不同的方法和类级属性。因为字典是无序的,所以属性和方法没有特别的排列顺序。所以极有可能这些信息会以和源码中不同的顺序出现。我们会在第1个例子中讲解这点。
  • 在第4~7章中我们会看到元类型被用来创建抽象基类。一个抽象基类基于__new__() 方法来确定子类的完整性。在第4章“抽象基类设计的一致性”中,我们会介绍这点。
  • 元类型可以被用来简化对象序列化的某些方面。在第9章“序列化和保存——JSON、YAML、Pickle、CSV和XML”中,我们会详细介绍这一点。
  • 作为最后一个也是最简单的例子,我们会看看一个类中对自己的引用。我们会设计一个引用了master类的类。这不是一种基类—子类的关系。这是一些平行的子类,但是引用了这些子类中的一个作为master。为了和它平行的类保持一致,主类需要包含一个指向自身的引用,如果不用元类型,不可能实现这样的行为。这是我们的第2个例子。

这是Python Language Reference 3.3.3节“自定义Python的类创建”中的经典例子,这个元类型会记录属性和方法的定义顺序。

下面是实现的3个具体步骤。

1.创建一个元类型。元类型的__prepare__() 和__new__() 方法会改变目标类创建的方式,会将原本的dict 类替换为OrderedDict 类。

2.创建一个基于此元类型的抽象基类。这个抽象类简化了其他类继承这个元类型的过程。

3.创建一个继承于这个抽象基类的子类,这样它就可以获得元类型的默认行为。

下面是使用该元类型的例子,它将保留属性创建的顺序。

import collections
class Ordered_Attributes(type):
   @classmethod
   def __prepare__(metacls, name, bases, **kwds):
     return collections.OrderedDict()
   def __new__(cls, name, bases, namespace, **kwds):
     result = super().__new__(cls, name, bases, namespace)
     result._order = tuple(n for n in namespace if not 
n.startswith('__'))
     return result

这个类用自定义的__prepare__() 和__new__() 方法扩展了内置的默认元类型type 。

__prepare__() 方法会在类创建之前执行,它的工作是创建初始的命名空间对象,类定义最后被添加到这个对象中。这个方法可以用来处理任何在类的主体开始执行前需要的准备工作。

__new__() 静态方法在类的主体被加入命名空间后开始执行。它的参数是要创建的类对象、类名、基类的元组和创建好的命名空间匹配对象。这个例子很经典:它将__new__() 的真正工作委托给了基类;一个元类型的基类是内置的type ;然后我们使用type.__new__() 创建一个稍后可以修改的默认类。

这个例子中的__new__() 方法向类中增加了一个_order 属性,用于存储原始的属性创建顺序。

当我们定义新的抽象基类时,我们可以用这个元类型而非type。

class Order_Preserved( metaclass=Ordered_Attributes ):
   pass

然后,我们可以将这个新的抽象基类作为任何其他自定义类的基类,如下所示。

class Something( Order_Preserved ):
   this= 'text'
   def z( self ):
     return False
   b= 'order is preserved'
   a= 'more text'

我们可以用下面的代码来介绍Something 类的使用。

>>> Something._order
>>> ('this', 'z', 'b', 'a')

我们可以考虑利用这些信息来正确序列化对象或者用于提供原始代码定义的调试信息。

接下来,我们看看一个关于单位换算的例子。例如,长度单位包括米、厘米、英寸、英尺和许多其他的单位。正确地管理单位换算是非常有挑战性的。表面上看,我们需要一个表示不同单位间转换因子的矩阵。例如,英尺转换为米、英尺转换为英寸、英尺转换为码、米转换为英寸、米转换为码等可能的组合。

但是,在实践中,一个更好的方案是定义一个长度的标准单位。我们可以把任何其他单位转换为标准单位,也可以把标准单位转换为任何其他单位。通过这种方式,我们可以很容易地将单位转换变成一致的两步操作,而不用再考虑包含了所有可能转换的复杂矩阵:英尺转换为标准单位,英寸转换为标准单位,码转换为标准单位,米转换为标准单位。

在下面的例子中,我们不准备继承float 或者numbers.Number 。相比于将单位和数值绑定在一起,我们更倾向于允许让每一个值仅仅代表一个简单的数字。这是享元模式的一个例子,类中不会定义包含相关值的对象,对象中仅仅包括转换因子。

另一种方案(将值和单位绑定)会造成需要相当复杂的三围分析。虽然这很有趣,但是太复杂了。

我们会定义两个类:Unit 和Standard_Unit 。我们可以很容易保证每个Unit 类中都正确地包含一个指向它的Standard_Unit 的引用。但是,我们如何能够保证每一个Standard_Unit 类中都有一个指向自己的引用呢?在类定义中实现子引用是不可能的,因为此时都还没有定义类。

下面是我们的Unit类的定义。

class Unit:
   """Full name for the unit."""
   factor= 1.0
   standard= None # Reference to the appropriate StandardUnit
   name= "" # Abbreviation of the unit's name.
   @classmethod
   def value( class_, value ):
     if value is None: return None
     return value/class_.factor
   @classmethod
   def convert( class_, value ):
     if value is None: return None
     return value*class_.factor

这个类的目的是Unit.value() 可以将一个值从给定的单位转换为标准单位,而Unit.convert() 方法可以将一个值从标准单位转换为给定的单位。

这让我们可以用下面的方式转换单位。

>>> m_f= FOOT.value(4)
>>> METER.convert(m_f)
1.2191999999999998

创建的值类型是内置的float 类型。对于温度的计算,我们需要重载默认的value() 和convert() 方法,因为简单的乘法运算不能满足实际物景。

对于Standard_Unit ,我们可能会使用下面这样的代码:

class INCH:
   standard= INCH

但是,这段代码无效。因为INCH 还没有定义在INCH 类中。在完成定义之前,这个类都是不存在的。

我们可以用下面的备用方法来处理这种情况。

class INCH:
   pass
INCH.standard= INCH

但是,这样的做法相当丑陋。

我们还可以像下面这样定义一个修饰符。

@standard
class INCH:
   pass

这个修饰符方法可以用来向类定义中加入一个属性。在第8章“装饰器和mixin——横切方面”中,我们再详细探讨这种方法。

现在,我们会定义一个可以向类定义中插入一个循环引用的元类型,如下所示。

class UnitMeta(type):
   def __new__(cls, name, bases, dict):
     new_class= super().__new__(cls, name, bases, dict)
     new_class.standard = new_class
     return new_class

这段代码强制地将变量standard 作为类定义的一部分。

对大多数单位,SomeUnit.standard 引用了TheStandardUnit 类。类似地,我们也让TheStandardUnit.standard 引用TheStandardUnit 类。Unit 和Standard_Right click for menu to add groups and entries. Edit or re-order any item. Use right click in editor to select which entry to paste.Unit 类之间这种一致的结构能够帮助我们书写文档和自动化单位转换。

下面是Standard_Unit 类:

class Standard_Unit( Unit, metaclass=UnitMeta ):
   pass

Unit 继承的单位转换因子是1.0,所以它并没有提供任何值。它包括了特殊的元类型定义,这样它就会有自引用,这个自引用表明这个类是这一特定维度的测量标准。

作为一种优化的手段,我们可以重载value() 和convert() 方法来禁止乘法和除法运算。

下面是一些单位类的例子。

class INCH( Standard_Unit ):
   """Inches"""
   name= "in"

class FOOT( Unit ):
   """Feet"""
   name= "ft"
   standard= INCH
   factor= 1/12

class CENTIMETER( Unit ):
   """Centimeters"""
   name= "cm"
   standard= INCH
   factor= 2.54

class METER( Unit ):
   """Meters"""
   name= "m"
   standard= INCH
   factor= .0254

我们将INCH定为标准单位,其他单位需要转换成英寸或者从英寸转换而来。

在每一个单位类中,我们都提供了一些文档信息:全名写在docstring 中并且用name 属性记录缩写。从Unit 继承而来的convert() 和value() 方法会自动应用转换因子。

有了这些类的定义,我们就可以在程序中像下面这样编码。

>>> x_std= INCH.value( 159.625 )
>>> FOOT.convert( x_std )
13.302083333333332
>>> METER.convert( x_std )
4.054475
>>> METER.factor
0.0254

我们可以根据给定的英寸值设置一种特定的测量方式并且可以将该值转换为任何兼容的单位。

由于元类型的存在,我们可以像下面这样从单位类中查询。

>>> INCH.standard.__name__
'INCH'
>>> FOOT.standard.__name__
'INCH'

这种引用方式让我们可以追踪一个指定维度上的不同单位。

我们已经介绍了许多基本的特殊方法,它们是我们在设计任何类时的基本特性。这些方法已经包含在每个类中,只是它们的默认行为不一定能满足我们的需求。

我们几乎总是需要重载__repr__() 、__str__() 、和__format__() 。这些方法的默认实现不是非常有用。

我们几乎不需要重载__bool__() 方法,除非我们想自定义集合。这是第6章“创建容器和集合”的主题。

我们常常需要重载比较运算符和__hash__() 方法。默认的实现只适合于比较简单不可变对象,但是不适用于比较可变对象。我们不一定要重写所有的比较运算符,在第8章“装饰器和mixin——横切方面”中,我们会详细介绍functools. total_ordering 修饰符。

另外两个较为特殊的方法__new__() 和__del__() 有更特殊的用途。大多数情况下,使用__new__() 来扩展不可变类型。

基本的特殊方法和__init__() 方法几乎会出现在我们定义的所有类中。其他的特殊方法则有更特殊的用途,它们分为6个不同的类别。

  • 属性访问 :这些特殊方法实现的是表达式中object.attribute 的部分,它通常用在一个赋值语句的左操作数以及del 语句中。
  • 可调用对象 :一个实现了将函数作为参数的特殊方法,很像内置的len() 函数。
  • 集合 :这些特殊方法实现了集合的很多特性,包括sequence[index] 、mapping[index] 和set | set 。
  • 数字 :这些特殊方法提供了算术运算符和比较运算符。我们可以用这些方法扩展Python支持的数值类型。
  • 上下文 :有两个特殊方法被我们用来实现可以和with 语句一起使用的上下文管理器。
  • 迭代器 :有一些特殊方法定义了一个迭代器。没有必要一定要使用这些方法,因为生成器函数很好地实现了这种特性。但是,我们可以了解如何实现自定义的迭代器。

在下一章中,我们会着重探讨属性、特性和修饰符。

原文地址:https://www.cnblogs.com/a00ium/p/14078484.html