关于python:__lt__而不是__cmp__

__lt__ instead of __cmp__

python 2.x有两种方法来重载比较运算符,即__cmp__或"富比较运算符",如__lt__。丰富的比较重载被认为是首选,但为什么会这样呢?

富比较运算符实现每一个都比较简单,但必须用几乎相同的逻辑实现其中的几个。但是,如果您可以使用内置的cmp和tuple排序,那么__cmp__将变得非常简单,并完成所有比较:

1
2
3
4
5
6
7
8
9
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,但在我看来,这会导致并发症的净增加。)

如果我只是超载了dOCx1(0),我是否需要意识到一些不可预见的陷阱?

我了解<<===等。操作员可以出于其他目的超载,并且可以返回他们喜欢的任何对象。我并不是在问这种方法的优点,而是在使用这些运算符进行比较时的区别,这些运算符的意义与它们对数字的意义相同。

更新:正如克里斯托弗指出的那样,cmp正在3.x中消失。有没有其他方法可以像上面的__cmp__那样容易地实现比较?


是的,用一个混合类(或者一个元类,或者一个类修饰器,如果你的口味是这样的话)来实现所有东西是很容易的。

例如:

1
2
3
4
5
6
7
8
9
10
11
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__,那么它应该直接定义它们,这样就不会使用mixin的版本(例如,dict就是这样),事实上,__ne__可以很好地定义为方便实现这一点:

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

但在上面的代码中,我希望保持仅使用<的令人愉快的对称性。至于为什么__cmp__必须走,既然我们有了__lt__和朋友,为什么还要用另一种不同的方式做同样的事情呢?在每个python运行时(classic、jython、ironpython、pypy,…)中,这都是非常沉重的负担。绝对不会有bug的代码是不存在的代码——python的原则是,理想情况下,应该有一种明显的方法来执行任务(在ISO标准的"C的精神"部分,btw中,C有相同的原则)。

这并不意味着我们会不遗余力地禁止某些事情(例如,在某些情况下,mixin和类修饰器之间几乎是等价的),但这确实意味着我们不喜欢在编译器和/或运行时携带冗余的代码,而这些代码只支持多个等价的方法来执行完全相同的任务。

进一步编辑:实际上还有更好的方法可以为许多类提供比较和散列,包括问题中的类——__key__方法,正如我在对问题的评论中提到的。因为我从未为它编写过PEP,所以如果您愿意,您当前必须使用mixin(&c)来实现它:

1
2
3
4
5
6
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__特殊方法直接解决了需要解决的问题。


为了简化这个例子,在python 2.7+/3.2+、functools.total_ordering中有一个类修饰符,可以用来实现Alex的建议。文档示例:

1
2
3
4
5
6
7
8
@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()))


PEP 207-丰富的比较涵盖了这一点。

另外,在python 3.0中,__cmp__也消失了。(请注意,它不在http://docs.python.org/3.0/reference/datamodel.html上,但在http://docs.python.org/2.7/reference/datamodel.html上)


受Alex Martelli的ComparableMixinKeyedMixin答案的启发,我想出了以下混音。它允许您实现单个_compare_to()方法,该方法使用基于键的比较类似于KeyedMixin,但允许类根据other的类型选择最有效的比较键。(请注意,对于可以测试相等性而不是顺序的对象来说,这种混合没有多大帮助)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class ComparableMixin(object):
   """mixin which implements rich comparison operators in terms of a single _compare_to() helper"""

    def _compare_to(self, other):
       """return keys to compare self to other.

        if self and other are comparable, this function
        should return ``(self key, other key)``.
        if they aren't, it should return ``None`` instead.
       """

        raise NotImplementedError("_compare_to() must be implemented by subclass")

    def __eq__(self, other):
        keys = self._compare_to(other)
        return keys[0] == keys[1] if keys else NotImplemented

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

    def __lt__(self, other):
        keys = self._compare_to(other)
        return keys[0] < keys[1] if keys else NotImplemented

    def __le__(self, other):
        keys = self._compare_to(other)
        return keys[0] <= keys[1] if keys else NotImplemented

    def __gt__(self, other):
        keys = self._compare_to(other)
        return keys[0] > keys[1] if keys else NotImplemented

    def __ge__(self, other):
        keys = self._compare_to(other)
        return keys[0] >= keys[1] if keys else NotImplemented

(编辑6/17/17,考虑到评论。)

我尝试了上面类似的混合答案。我遇到了"没有"的麻烦。这里是一个修改过的版本,它处理与"无"的相等比较。(我认为没有理由费心进行不平等的比较,因为没有理由缺乏语义):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ComparableMixin(object):

    def __eq__(self, other):
        if not isinstance(other, type(self)):
            return NotImplemented
        else:
            return not self<other and not other<self

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

    def __gt__(self, other):
        if not isinstance(other, type(self)):
            return NotImplemented
        else:
            return other<self

    def __ge__(self, other):
        if not isinstance(other, type(self)):
            return NotImplemented
        else:
            return not self<other

    def __le__(self, other):
        if not isinstance(other, type(self)):
            return NotImplemented
        else:
            return not other<self