摘要:比较运算符方法有六个比较运算符。根据文档,其映射工作如下第七章创建数字我们会再次回到比较运算符这块。同一个类的对象的比较实现我们来看看一个简单的同一类的比较通过观察一个更完整的类现在我们已经定义了所有六个比较运算符。
__bool__()方法注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python
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}[code] 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 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显然是为了比较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 "", line 1, in TypeError: unorderable types: Number21Card() < int() >>> two > 2 Traceback (most recent call last): File " ", line 1, in 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 "", line 1, in TypeError: unorderable types: Hand() > int()
只要与整数的比较正常工作,Python不会被迫撤回。前面的例子告诉我们当没有__gt__()方法。Python检查相反的操作数,对于Hand整数17也没有适当的__lt__()方法。
我们可以添加必要的__gt__()和__ge__()函数使得Hand与整数正常工作。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/44187.html
摘要:这些基本的特殊方法在类中定义中几乎总是需要的。和方法对于一个对象,有两种字符串表示方法。这些都和内置函数以及方法紧密结合。带有说明符的合理响应是返回。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python 有许多特殊方法允许类与Python紧密结合,标准库参考将其称之为基本,基础或本质可能是更好的术语。这些特殊...
摘要:有三个用例通过和方法定义相等性检测和值不可变对象对于有些无状态对象,例如这些不能被更新的类型。请注意,我们将为不可变对象定义以上两个。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python __hash__() 方法 内置hash()函数会调用给定对象的__hash__()方法。这里hash就是将(可能是复杂的)值缩减...
摘要:当引用计数为零,则不再需要该对象且可以销毁。这表明当变量被删除时引用计数正确的变为零。方法只能在循环被打破后且引用计数已经为零时调用。这两步的过程允许引用计数或垃圾收集删除已引用的对象,让弱引用悬空。这允许在方法设置对象属性值之前进行处理。 注:原书作者 Steven F. Lott,原书名为 Mastering Object-oriented Python __del__()方法 ...
摘要:第二章与的无缝集成基本特殊方法笔记中有有一些特殊的方法它们允许我们的类和更好的集成和方法通常方法表示的对象对用户更加友好这个方法是有对象的方法实现的什么时候重写跟非集合对象一个不包括其他集合对象的简单对象这类对象格式通常不会特别复 第二章 与Python的无缝集成----基本特殊方法.(Mastering Objecting-oriented Python 笔记) python中有有一...
阅读 886·2023-04-25 19:17
阅读 2181·2021-09-10 11:26
阅读 1901·2019-08-30 15:54
阅读 3414·2019-08-30 15:53
阅读 2683·2019-08-30 11:20
阅读 3397·2019-08-29 15:12
阅读 1234·2019-08-29 13:16
阅读 2389·2019-08-26 12:19