__lt__代替__cmp__


100

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
请参阅我对您的最后一个问题的回答,但实际上有一种设计可使许多类(包括您的类)变得更加轻松(现在您需要使用mixin,元类或类装饰器来应用它):如果存在关键的特殊方法,它必须返回一个值的元组,并且所有比较器AND 哈希都是根据该元组定义的。当我向他解释时,Guido很喜欢我的想法,但是后来我忙于其他事情,却从没写过PEP ...也许是3.2 ;-)。同时,我一直为此使用我的mixin!-)
Alex Martelli 2009年

Answers:


90

是的,很容易__lt__用mixin类(或元类,如果您的口味是这样的话,可以使用类装饰器)来实现所有内容。

例如:

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__可以将其定义为方便作为:

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

但是在上面的代码中,我想保持仅使用<;-)的对称性。至于为什么__cmp__要走,既然我们确实__lt__朋友,为什么还要用另一种不同的方法做完全一样的事情呢?在每个Python运行时(经典,Jython,IronPython,PyPy等)中,它是如此沉重。该代码绝对不会有虫子的是不存在的代码-那里Python的原则是,应该是一个理想的执行任务明显的方式(C具有相同的原则的“C的精神”一节中ISO标准,顺便说一句)。

这并不意味着我们走的路,禁止的事情了(例如,混入以及一些应用类装饰之间近乎等价),但绝对意味着我们不喜欢随身携带的编译器和代码/或冗余存在的运行时仅用于支持多种等效方法来执行完全相同的任务。

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

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__直接需要特殊的方法解决。


抱歉@R的延迟。佩特,我决定,因为无论如何我都必须进行编辑,所以我应该提供最详尽的答案,而不是匆忙处理(我只是再次编辑以建议我从未解决过的旧关键思想,以及如何用mixin实现它)。
Alex Martelli,2009年

我真的很喜欢这个关键想法,要去使用它,看看它的感觉。(尽管命名为cmp_key或_cmp_key而不是保留名称。)

TypeError: Cannot create a consistent method resolution order (MRO) for bases object, ComparableMixin当我在Python 3中尝试时。请参见gist.github.com/2696496
Adam Parkin

2
在Python 2.7 + / 3.2 +中,您可以使用functools.total_ordering而不是构建自己的ComparableMixim。正如jmagnusson的答案
日,天

4
由于,使用Python 3 <实现__eq__是一个非常糟糕的主意TypeError: unorderable types
Antti Haapala'8

49

为了简化这种情况,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()))

9
total_ordering虽然没有实现__ne__,所以要当心!
Flimm 2013年

3
@Flimm,不是,但是__ne__。但这是因为__ne__具有委托给的默认实现__eq__。因此,这里没有什么值得提防的。
Jan Hudec

必须定义至少一个排序操作:<> <=> =...。a <b和b <a然后a = b
Xanlantos

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上


PEP只关心为什么需要丰富的比较,以NumPy用户希望A <B返回序列的方式。

我没有意识到这肯定会消失,这让我很难过。(但感谢您指出这一点。)

PEP还讨论了“为什么”选择它们。从本质上讲,它可以归结为效率:1.无需实现对您的对象没有意义的操作(例如无序集合)。2.一些集合在某些比较中具有非常有效的操作。如果您定义了比较,那么丰富的比较使解释器可以利用它。
Christopher

1
关于1,如果它们没有意义,则不要实现cmp。关于Re 2,拥有这两个选项可以让您根据需要进行优化,同时仍然可以快速进行原型设计和测试。没有人告诉我为什么将其删除。(从本质上来说,这归结为开发人员的效率。)采用cmp后备功能时,丰富的比较是否有可能效率较低?这对我来说毫无意义。

1
@R。就像我在回答中解释的那样,Pate并没有真正的损失(因为mixin,decoror或元类,让您可以轻松地根据<定义所有内容),因此所有Python实现都可以随身携带冗余代码将永远归为cmp-只是为了让Python用户以两种等效的方式表达事物-将对Python的运行率达到100%。
Alex Martelli,2009年

2

(已于6/17/17修改以考虑评论。)

我尝试了上面类似的mixin答案。我遇到了“无”的麻烦。这是处理与“无”的相等比较的修改版本。(由于没有语义,我没有理由不理会与None进行的不平等比较):


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    

您如何看待这self可能是单身NoneNoneType,并在同一时间实现你的ComparableMixin?事实上这个配方是坏的Python 3
安蒂·哈帕拉

3
self永远None,这样的分支可以完全去。不要用type(other) == type(None); 只需使用other is None。而不是特殊情况下None,测试其他类型是否是的类型的实例,如果不是self,则返回NotImplemented单例if not isinstance(other, type(self)): return NotImplemented。对所有方法都执行此操作。然后,Python可以给另一个操作数一个机会来提供答案。
马丁·彼得斯

1

受Alex Martelli的ComparableMixinKeyedMixin答案启发,我想到了以下mixin。它允许您实现一个_compare_to()方法,该方法使用类似于的基于键的比较KeyedMixin,但允许您的类根据的类型选择最有效的比较键other。(请注意,对于可以测试是否相等但不能测试顺序的对象,此mixin并没有太大帮助)。

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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.