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

__bool__()方法

Python对假有个很好的定义。参考手册列出了大量的值来被检测为False。这包括诸如:False0\'\'[](){}。大多数其他对象将被检测为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)

它委托布尔函数到内部_cards集合。

如果我们扩展列表,我们需要如下操作:

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

它委托到__bool__()函数的超类定义。

在这两种情况下,我们特意地委托布尔检测。在包装那个例子,我们委托给集合。在扩展的例子,我们委托给超类。无论哪种方式,包装或扩展,空集合将是False。这将给我们一种途径去看Deck对象是否已经完全处理完且是空的。

我们可以按照如下代码片段所示去做:

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

此循环将处理所有的牌,在整副牌都没有了的时候不会出现IndexError异常。

__bytes__()方法

有时候会出现将一个对象转换成字节的情况。我们将在第2部分《持久化和序列化》中详细看看相关内容。

在最常见的情况下,应用程序可以创建一个字符串表示,Python IO类内置的编码功能可以将字符串转换成字节。几乎任何情况下都能完成的很好。唯一的例外是当我们定义了一种新的字符串。在这种情况下,我们需要定义该字符串编码。

bytes()函数可以做很多事情,但这取决于它的参数:

  • bytes(integer):返回给定整数个0x00的不可变字节对象。

  • bytes(string):将给定字符串编码成字节。额外的编码参数和错误处理将定义编码处理的细节。

  • bytes(something):这将调用something.__bytes__()来创建一个字节对象。这里将不会使用编码或错误参数。

基本object类没有定义__bytes__()。这意味着我们的类默认不提供__bytes__()方法。

有一些特殊的情况下,我们可能需要有一个在写入到文件之前被直接编码到字节中的对象。通常是简单的字符串并允许str类型为我们生成字节。在处理字节时,重要的是要注意,没有简单的方法从文件或接口来解码。内置的bytes类只会解码字符串,不是我们独有的新对象。我们可能需要从字节解码来解析字符串。或者,我们可能需要使用struct模块显式地解析字节,通过解析好的值创建独有对象。

我们看看编码和解码Card成字节。有52个牌值,每张牌可以打包到一个字节。然而,我们已经选择使用一个字符代表suit和一个字符来表示rank。此外,我们需要正确地重构Card子类,所以我们必须编码几件事情:

  • Card(AceCard, NumberCard, FaceCard)子类

  • 子类定义的__init__()的参数

注意,我们的替代方法__init__()将一个牌值转换成一个字符串,失去原来的数值。为了一个可逆的字节编码,我们需要重构这个原始牌值。

下面是__bytes__()的实现,它返回一个utf-8编码的Cards类、ranksuit

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已经有很多对我们类定义工作的很好的表示。

通常是使用picklejson模块比发明低级的字节来表示一个对象要更好。这是第九章《序列化和存储JSON、YAML、Pickle、CSV和XML》的主要内容。

比较运算符方法

Python有六个比较运算符。这些操作符有特殊的方法实现。根据文档,其映射工作如下:

  • x < y calls x.__lt__(y)

  • x <= y calls x.__le__(y)

  • x == y calls x.__eq__(y)

  • x != y calls x.__ne__(y)

  • x > y calls x.__gt__(y)

  • x >= y calls x.__ge__(y)

第七章《创建数字》我们会再次回到比较运算符这块。

有一些关于哪个操作符被真实实现的额外规则。这些规则是基于左边对象的类所需的特殊方法。如果没有,Python可以改变顺序来尝试另一种操作。

这里有两个基本规则

首先,左边的操作数是由操作符的实现方法来检查的:A < B意味着A.__lt__(B)

第二,右边的操作数是由相反的操作符的实现方法来检查的:A < B意味着B.__gt__(A)

罕见的例外发生在右操作数是左操作数的一个子类;然后,右操作数是第一个被检查的,允许子类覆盖超类。

我们可以看到这是如何工作的当一个类只有一个操作符的时候,然后供其他操作符使用。

以下是我们可以使用的部分类:

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。这是镜像反射规则;我们将在第七章《创造数字》再次见到它。

我们将看到试图评估不同的比较操作。创建两个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 \"\", line 1, in 
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默认处理以下四对相反的检测:

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__()中任意的一个,推导出其余的比较。我们将在第7章《创造数字》再次讨论这个。

1. 设计比较

在比较运算符有两个顾虑:

  • 显而易见的问题是怎样比较相同类的两个对象

  • 不太明显的问题是怎样比较不同类的对象

对于一个类有多个属性,我们考虑比较运算符的时候经常模棱两可。可能不是很清楚我们要比较什么。

再次考虑不起眼的扑克牌。表达式如card1 == card2显然是为了比较ranksuit。对吗?或者总是为真?终究,suit在21点没有意义。

如果我们想决定Hand对象是否可以分牌,我们最好看下这两个代码片段。以下是第一个代码片段:

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

下面是第二个代码片段:

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

虽然有一个是更短,简洁并不总是最好的。如果我们定义相等只考虑rank,我们将很难定义单元测试,因为当一个单元测试应该关注完全正确的牌时,一个简单的TestCase.assertEqual()方法会容忍各种各样的牌。

表达式如card1 < = 7显然是为了比较rank

我们想要一些比较来比较牌的所有属性,一些比较比较rank?我们该怎样通过suit来排序?此外,相等性的比较必须并行计算hash。如果我们在hash中包含多个属性,我们需要将其包含在相等性的比较中。在这种情况下,似乎牌之间的相等和不相等必须全部的Card比较,因为我们的hash包括了ranksuit

Card之间的比较顺序,无论如何都应该只有rank。和整数的比较同样也应该只有rank。当发现分牌这一特殊情况,hand.cards[0].rank == hand.cards[1].rank会处理的很好,因为在分牌规则中它是显式的。

2. 同一个类的对象的比较实现

我们来看看一个简单的同一类的比较通过观察一个更完整的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__)

现在我们已经定义了所有六个比较运算符。

我们已经向您展示了两种类型检查:显式隐式。显式类型检查使用isinstance()。隐式类型检查使用try:块。使用try:块有概念上的小优势,它能避免重复的类名。有可能会有人想发明一种变体牌来兼容BlackJackCard但不是定义为适当的子类。使用isinstance()可以防止一个无效类来保证工作正常。

try:块会允许一个类使用rank属性。这变成一个难以解决问题的风险会变为零,而作为类在应用程序的其他地方可能会失败。同样,Card实例与金融建模应用程序类比较出现根据牌值排序的属性。

在接下来的例子中,我们将关注try:块。isinstance()方法检查一直是Python惯用方法且应用广泛。我们通过显式地返回NotImplemented来告知Python,这个操作符并不是用来实现这种类型数据的。Python可以颠倒参数顺序来看看另一个操作数是否提供了实现方法。如果没有找到有效的操作符,则TypeError异常将被抛出。

我们省略了三个子类定义和工厂函数,留下card21()作为一个练习。

我们也省略了同类的比较,我们将在下一节看到。通过这个类,我们可以成功的比较牌。下面是一个例子,我们创建并比较三张牌:

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

定义似乎和预期的一样。

3. 混合类对象的比较实现

我们使用BlackJackCard类为例,当我们尝试比较来自不同类的两个操作数时,看看会发生什么。

下面是Card实例,我们可以和int值相比较:

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

这就是我们期望的,BlackJackCardNumber21Card的子类没有提供所需的特殊方法,所以有TypeError异常。

然而,考虑下面的两个例子:

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

为什么是这样的结果?当面临一个NotImplemented值,Python会对调操作数。在这种情况下,整数值定义int.__eq__()方法,容忍一个意想不到的类对象。

4. Hard点数、Soft点数和多态性

我们定义Hand这样它将执行一些有意义的混合类比较。与其他的比较,我们必须确定这正是我们要比较的。

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

对于Hands比较顺序,我们需要比较每个Hand对象的一个属性。为了逐个和int相比较,我们应该让Hand对象的总数逐个相比较。为了有一个总数,在21点游戏中我们必须分类hard点数和soft点数。

当有一个A在手,会有下面两个候选点数:

  • soft点数把A当作11。如果soft点数超过21,那么A当作1。

  • hard点数把A当作1。

这意味着手牌的总和不是简单的牌的总和。

首先我们必须确定是否有一个A在手。确定这些,我们可以确定是否有一个有效的(小于或等于21)的soft点数。否则,我们将依靠hard点数。

多态性的一个症状是依靠isinstance()来确定子类的成员。一般来说,这违反了基本的封装特性。一组好的多态的子类定义应该完全对等且带有相同的方法签名。理想情况下,类的定义是不透明的,我们不需要看类的内部定义。一组多态的类使用广泛的isinstance()检测。在某些情况下,isinstance()是必要的。当使用一个内置类的时候都会出现这样。我们不能追溯添加方法函数到内置类中,子类化它们来添加一个多态性辅助方法可能是不值得的。

对于一些特殊的方法,有必要看到isinstance()用于实现跨多个类对象的操作,没有简单的继承层次结构。在下一节,与之无关的类中,我们将向您展示isinstance()的惯用方法。

对于我们牌的类层次结构,我们想要一个方法(或属性)来标识A,而不是用isinstance()。这是一个多态辅助方法。它确保我们可以辨别否则等价类会分开。

我们有两个选择:

  • 添加一个类级别的属性

  • 添加一个方法

因为保守的赌注方式,我们有两个原因去检查A。如果庄家的牌是A,它会触发一个保险的赌注。如果庄家手牌(或玩家的手牌)有一个A,会有一个soft点数与hard点数的计算。

hard点数和soft点数是card.soft - card.hard值的差。我们可以在AceCard里面的定义看到这个值是10。然而,深入类的内部可以看到该实现违背了封装性。

我们可以将BlackjackCard作为透明的,然后检查card.soft - card.hard != 0是否为真。如果为真,这些信息足够算出hard点数和soft点数。

下面是一个使用total方法计算soft值和hard值之间差值的版本:

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

我们将计算hard点数和soft点数差作为delta_soft。对于大多数牌,差异是零。对于A,差异是非零。

鉴于hard点数和delta_soft,我们可以确定返回那个总数。如果是hard + delta_soft小于或等于21,值是soft点数。如果soft点数大于21,又恢复到hard点数。

我们可以考虑让值21为显式常量。一个有意义的名字有时比文字更有帮助。因为21点的规则,21不太可能会改变到一个不同的值。没有比文字含义的21更有意义的了。

5. 混合类比较示例

Hand对象给定一个点数,我们可以有意义地定义Hand实例之间的比较以及Handint之间的对比。为了确定我们做哪一种比较,我们被迫使用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

我们定义了三个比较,不是所有的六个。

为了与Hand交互我,们需要几个Card对象:

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

我们将使用这个序列的牌来看看两个不同的hand实例。

第一个Hands对象有一个无关紧要的庄家的Card对象和先前创建的四张牌。Card对象中的一个是A:

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

soft点数是18,hard点数是8。

下面是第二个对象,有一个额外的Card对象:

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

hard点数是13。没有soft点数,因为它超过了21。

Hands之间的比较工作得很好,如下代码片段所示:

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

我们基于比较运算符对Hands进行排名。

我们也可以让Hands与整数进行比较,如下代码片段所示:

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

只要与整数的比较正常工作,Python不会被迫撤回。前面的例子告诉我们当没有__gt__()方法。Python检查相反的操作数,对于Hand整数17也没有适当的__lt__()方法。

我们可以添加必要的__gt__()__ge__()函数使得Hand与整数正常工作。