Python 使用total_ordering定义类,total_ordering
装饰器用于定义实现各种比较运算的算子类,既可用于numbers.Number
的子类,也可用于半数值型类。
下面以扑克牌为例说明使用半数值型类的场景。扑克牌有点数,也有花色,有些玩法中点数起的作用非常大。与数字类似,扑克牌可以排序,点数也可以相加。但牌与牌之间做乘法毫无意义,这一点又与数字不同。
可以通过继承NamedTuple
类定义一个扑克牌类,如下所示:
from typing import NamedTuple
class Card1 (NamedTuple):
rank: int
suit: str
该定义看上去不错,但有个问题:所有实例的比较都必须包含点数和花色,因此当比较黑桃2和梅花2时会出现下面的情况:
>>> c2s= Card1(2, '\u2660')
>>> c2h= Card1(2, '\u2665')
>>> c2s
Card1(rank=2, suit='♣')
>>> c2h= Card1(2, '\u2665')
>>> c2h
Card1(rank=2, suit='♥')
>>> c2h == c2s
False
不难发现这个类的默认比较方式不适合许多玩法。
大多数扑克牌玩法都只需要比较点数,更实用的定义如下所示:
from functools import total_ordering
from numbers import Number
from typing import NamedTuple
@total_ordering
class Card2(NamedTuple):
rank: int
suit: str
def __eq__(self, other: Any) -> bool:
if isinstance(other, Card2):
return self.rank == other.rank
elif isinstance(other, int):
return self.rank == other
return NotImplemented
def __lt__(self, other: Any) -> bool:
if isinstance(other, Card2):
return self.rank < other.rank
elif isinstance(other, int):
return self.rank < other
return NotImplemented
这里的Card2
类继承了NamedTuple
类,使用父类的__str__()
方法将实例以字符串的形式打印出来。
类中定义了两个比较方法:一个定义相等,一个定义顺序。其他比较方法由@total_ordering
基于这两个定义完成,包括__le__()
、__gt__()
和__ge__()
等,不等比较方法__ne__()
默认基于__eq__()
生成,装饰器无须参与。
以上方法实现了两种比较:两个Card2
对象之间,以及Card2
对象和整形数值之间。__eq__()
和__lt__()
参数的类型标示必须是Any
,以保证与父类兼容,虽然写成Union[Card2, int]
更精确,但会与父类冲突。
首先这个类提供了仅基于点数的比较,如下所示:
>>> c2s= Card2(2, '\u2660')
>>> c2h= Card2(2, '\u2665')
>>> c2h == c2s
True
>>> c2h == 2
True
>>> 2 == c2h
True
这个类可用于扑克牌之间基于点数的比较,装饰器自动生成了多种比较运算符,如下所示:
>>> c2s= Card2(2, '\u2660')
>>> c3h= Card2(3, '\u2665')
>>> c4c= Card2(4, '\u2663')
>>> c2s <= c3h < c4c
True
>>> c3h >= c3h
True
>>> c3h > c2s
True
>>> c4c != c2s
True
这里无须手动编写比较运算符,装饰器会自动生成,但它生成的运算符并不是完全理想的。对于本例,如果要比较整数和Card2
对象,就出现了问题。
由于运算符解析机制的限制,类似于c4c > 3
和3 < c4c
这样的操作会出现TypeError
异常,这是total_ordering
无法正确处理的情形。虽然在实际应用中这样的情况不常见,但当确实需要这样的比较时,所有的比较运算符都要手动定义,而不能使用@total_ordering
装饰器自动生成。
面向对象编程并不是函数式编程的对立面,很多时候二者是互补的。Python生成不可变对象的能力恰好契合了函数式编程的要求。我们完全可以既避免创建带有复杂状态的对象,又能将相关方法封装在一起使用。尤其当类属性中包含复杂计算时,将复杂计算封装在类定义中会让应用的逻辑更易理解。