使用__lt__代替__cmp__

111 投票
5 回答
72062 浏览
提问于 2025-04-15 12:34

在Python 2.x中,有两种方法可以重载比较运算符,一种是__cmp__,另一种是一些叫做“丰富比较运算符”的东西,比如__lt__虽然说丰富比较运算符更受欢迎,但为什么呢?

丰富比较运算符每个实现起来都比较简单,但你需要实现好几个几乎逻辑相同的运算符。不过,如果你能使用内置的cmp和元组排序,那么__cmp__就会变得很简单,而且可以满足所有的比较需求:

class A(object):
  def __init__(self, name, age, other):
    self.name = name
    self.age = age
    self.other = other
  def __cmp__(self, other):
    assert isinstance(other, A) # assumption for this example
    return cmp((self.name, self.age, self.other),
               (other.name, other.age, other.other))

这种简单性似乎比重载所有6个(!)丰富比较运算符要更符合我的需求。(不过,如果你依赖“交换参数”/反射行为,可以把它减少到“仅仅”4个,但在我看来,这样反而会增加复杂性。)

如果我只重载__cmp__,是否有我需要注意的意外问题?

我知道<<===等运算符可以被重载用于其他目的,并且可以返回任何对象。我并不是在询问这种方法的优缺点,而只是想了解在使用这些运算符进行比较时,与数字的比较有什么不同。

更新:正如Christopher指出的cmp在3.x中正在消失。有没有什么替代方案可以像上面的__cmp__一样简单地实现比较?

5 个回答

9

这部分内容可以在PEP 207 - 丰富比较中找到。

另外,__cmp__这个东西在Python 3.0中被去掉了。(注意,在http://docs.python.org/3.0/reference/datamodel.html上找不到它,但在http://docs.python.org/2.7/reference/datamodel.html上是有的。)

55

为了简化这个情况,Python 2.7+ 和 3.2+ 中有一个类装饰器,叫做 functools.total_ordering,可以用来实现 Alex 提出的建议。下面是文档中的一个例子:

@total_ordering
class Student:
    def __eq__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))
100

没错,使用像 __lt__ 这样的方式来实现功能其实很简单,可以通过混入类(mixin class)、元类(metaclass)或者类装饰器(class decorator)来做到,具体看你喜欢哪种方式。

举个例子:

class ComparableMixin:
  def __eq__(self, other):
    return not self<other and not other<self
  def __ne__(self, other):
    return self<other or other<self
  def __gt__(self, other):
    return other<self
  def __ge__(self, other):
    return not self<other
  def __le__(self, other):
    return not other<self

这样你的类只需要定义 __lt__,然后从 ComparableMixin 继承(在它需要的其他基类之后,如果有的话)。类装饰器的做法也差不多,只是把类似的函数作为新类的属性插入进去(这样在运行时可能会快一点点,但内存开销也会相应减少)。

当然,如果你的类有特别快速的方法来实现(比如) __eq____ne__,那么它应该直接定义这些方法,这样就不会使用混入类里的版本(例如,dict 就是这样的情况)——实际上 __ne__ 可能会被定义成这样来方便实现:

def __ne__(self, other):
  return not self == other

不过在上面的代码中,我想保持只用 < 的对称性,嘿嘿。至于为什么 __cmp__ 要被去掉,因为我们已经有了 __lt__ 和其他相关的方法,为什么还要保留另一种不同的方式来做同样的事情呢?这在每个 Python 运行环境中都是多余的负担(比如 Classic、Jython、IronPython、PyPy 等等)。那些绝对不会出错的代码就是那些不存在的代码——这也是 Python 的一个原则:理想情况下,应该有一种明显的方法来完成一项任务(顺便提一下,C 语言在 ISO 标准的“C 的精神”部分也有类似的原则)。

这并不是说我们要故意禁止某些东西(例如,混入类和类装饰器在某些用法上几乎是等价的),但这确实意味着我们不喜欢在编译器和/或运行时中携带那些冗余的代码,只是为了支持多种等效的方法来完成同样的任务。

进一步补充:其实还有一种更好的方法可以为许多类提供比较和哈希功能,包括问题中的类——那就是 __key__ 方法,正如我在对问题的评论中提到的。因为我还没来得及为它写 PEP,所以如果你想用的话,目前只能通过混入类来实现:

class KeyedMixin:
  def __lt__(self, other):
    return self.__key__() < other.__key__()
  # and so on for other comparators, as above, plus:
  def __hash__(self):
    return hash(self.__key__())

对于实例之间的比较,通常只需要比较每个实例的几个字段组成的元组——而哈希也应该基于同样的原则来实现。__key__ 这个特殊方法正好满足了这个需求。

撰写回答