__lt__代替__cmp__

问题:__lt__代替__cmp__

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__

Python 2.x has two ways to overload comparison operators, __cmp__ or the “rich comparison operators” such as __lt__. The rich comparison overloads are said to be preferred, but why is this so?

Rich comparison operators are simpler to implement each, but you must implement several of them with nearly identical logic. However, if you can use the builtin cmp and tuple ordering, then __cmp__ gets quite simple and fulfills all the comparisons:

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))

This simplicity seems to meet my needs much better than overloading all 6(!) of the rich comparisons. (However, you can get it down to “just” 4 if you rely on the “swapped argument”/reflected behavior, but that results in a net increase of complication, in my humble opinion.)

Are there any unforeseen pitfalls I need to be made aware of if I only overload __cmp__?

I understand the <, <=, ==, etc. operators can be overloaded for other purposes, and can return any object they like. I am not asking about the merits of that approach, but only about differences when using these operators for comparisons in the same sense that they mean for numbers.

Update: As Christopher pointed out, cmp is disappearing in 3.x. Are there any alternatives that make implementing comparisons as easy as the above __cmp__?


回答 0

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

Yep, it’s easy to implement everything in terms of e.g. __lt__ with a mixin class (or a metaclass, or a class decorator if your taste runs that way).

For example:

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

Now your class can define just __lt__ and multiply inherit from ComparableMixin (after whatever other bases it needs, if any). A class decorator would be quite similar, just inserting similar functions as attributes of the new class it’s decorating (the result might be microscopically faster at runtime, at equally minute cost in terms of memory).

Of course, if your class has some particularly fast way to implement (e.g.) __eq__ and __ne__, it should define them directly so the mixin’s versions are not use (for example, that is the case for dict) — in fact __ne__ might well be defined to facilitate that as:

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

but in the code above I wanted to keep the pleasing symmetry of only using <;-). As to why __cmp__ had to go, since we did have __lt__ and friends, why keep another, different way to do exactly the same thing around? It’s just so much dead-weight in every Python runtime (Classic, Jython, IronPython, PyPy, …). The code that definitely won’t have bugs is the code that isn’t there — whence Python’s principle that there ought to be ideally one obvious way to perform a task (C has the same principle in the “Spirit of C” section of the ISO standard, btw).

This doesn’t mean we go out of our way to prohibit things (e.g., near-equivalence between mixins and class decorators for some uses), but it definitely does mean that we don’t like to carry around code in the compilers and/or runtimes that redundantly exists just to support multiple equivalent approaches to perform exactly the same task.

Further edit: there’s actually an even better way to provide comparison AND hashing for many classes, including that in the question — a __key__ method, as I mentioned on my comment to the question. Since I never got around to writing the PEP for it, you must currently implement it with a Mixin (&c) if you like it:

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__())

It’s a very common case for an instance’s comparisons with other instances to boil down to comparing a tuple for each with a few fields — and then, hashing should be implemented on exactly the same basis. The __key__ special method addresses that need directly.


回答 1

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

To simplify this case there’s a class decorator in Python 2.7+/3.2+, functools.total_ordering, that can be used to implement what Alex suggests. Example from the docs:

@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()))

回答 2

这是通过覆盖PEP 207 -丰富的比较

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

This is covered by PEP 207 – Rich Comparisons

Also, __cmp__ goes away in python 3.0. ( Note that it is not present on http://docs.python.org/3.0/reference/datamodel.html but it IS on http://docs.python.org/2.7/reference/datamodel.html )


回答 3

(已于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    

(Edited 6/17/17 to take comments into account.)

I tried out the comparable mixin answer above. I ran into trouble with “None”. Here is a modified version that handles equality comparisons with “None”. (I saw no reason to bother with inequality comparisons with None as lacking semantics):


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    

回答 4

受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

Inspired by Alex Martelli’s ComparableMixin & KeyedMixin answers, I came up with the following mixin. It allows you to implement a single _compare_to() method, which uses key-based comparisons similar to KeyedMixin, but allows your class to pick the most efficient comparison key based on the type of other. (Note that this mixin doesn’t help much for objects which can be tested for equality but not order).

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