Python total_ordering定义类

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 > 33 < c4c这样的操作会出现TypeError异常,这是total_ordering无法正确处理的情形。虽然在实际应用中这样的情况不常见,但当确实需要这样的比较时,所有的比较运算符都要手动定义,而不能使用@total_ordering装饰器自动生成。

面向对象编程并不是函数式编程的对立面,很多时候二者是互补的。Python生成不可变对象的能力恰好契合了函数式编程的要求。我们完全可以既避免创建带有复杂状态的对象,又能将相关方法封装在一起使用。尤其当类属性中包含复杂计算时,将复杂计算封装在类定义中会让应用的逻辑更易理解。

Python教程

Java教程

Web教程

数据库教程

图形图像教程

大数据教程

开发工具教程

计算机教程