注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python
__bool__()
方法
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)
它委托布尔函数到内部_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
类、rank
和suit
:
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}
return class_(int(rank_number), suit)
在前面的代码中,我们将字节解码为一个字符串。然后我们将该字符串解析为各个值。从这些值,我们可以定位类且构建原始
Card
对象。我们可以构建
Card
对象的字节表示,如下:b = bytes(someCard)
我们可以通过字节重构
Card
对象,如下:someCard = card_from_bytes(b)
重要的是要注意,外部字节表示通常是具有挑战性的设计。我们创建一个对象状态表示。Python已经有很多对我们类定义工作的很好的表示。
通常是使用
pickle
或json
模块比发明低级的字节来表示一个对象要更好。这是第九章《序列化和存储JSON、YAML、Pickle、CSV和XML》的主要内容。比较运算符方法
Python有六个比较运算符。这些操作符有特殊的方法实现。根据文档,其映射工作如下:
x < y
callsx.__lt__(y)
x <= y
callsx.__le__(y)
x == y
callsx.__eq__(y)
x != y
callsx.__ne__(y)
x > y
callsx.__gt__(y)
x >= y
callsx.__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 "<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默认处理以下四对相反的检测:
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
显然是为了比较rank
和suit
。对吗?或者总是为真?终究,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包括了rank
和suit
。
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 "<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
为什么是这样的结果?当面临一个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
实例之间的比较以及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
我们定义了三个比较,不是所有的六个。
为了与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 "<stdin>", line 1, in <module>
TypeError: unorderable types: Hand() > int()
只要与整数的比较正常工作,Python不会被迫撤回。前面的例子告诉我们当没有__gt__()
方法。Python检查相反的操作数,对于Hand
整数17也没有适当的__lt__()
方法。
我们可以添加必要的__gt__()
和__ge__()
函数使得Hand
与整数正常工作。