5

With abstract base classes, Python provides a way to know the behavior of objects without actually trying it out. In the standard library, we have some ABCs defined for containers in collections.abc. For example, one can test that an argument is iterable:

from collections.abc import Iterable
def function(argument):
    if not isinstance(argument, Iterable):
        raise TypeError('argument must be iterable.')
    # do stuff with the argument

I was hoping that there would be one such ABC for deciding whether instances of a class can be compared but couldn't find one. Testing for the existence of __lt__ methods is not sufficient. For example, dictionaries cannot be compared but __lt__ is still defined (same with object actually).

>>> d1, d2 = {}, {}
>>> d1 < d2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: dict() < dict()
>>> hasattr(d1, '__lt__')
True

So my question is: is there a simple way to do it without doing the comparisons themselves and catching the TypeError?

My use case is similar to a sorted container: I'd like to raise an exception when I insert the first element and not to wait for a second element. I thought about comparing the element with itself, but is there a better approach:

def insert(self, element):
    try:
        element < element
    except TypeError as e:
        raise TypeError('element must be comparable.')
    # do stuff
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Francis Colas
  • 3,459
  • 2
  • 26
  • 31

1 Answers1

5

No, there is no such ABC, because an ABC only dictates what attributes are there. ABCs cannot test for the nature of the implementation (or even if those attributes are actually methods).

The presence of comparison methods (__lt__, __gt__, __le__, __ge__ and __eq__) does not dictate that the class is going to be comparable with everything else. Usually you can only compare objects of the same type or class of types; numbers with numbers for example.

As such, most types* implement the comparison methods but return the NotImplemented sentinel object when comparing with other incompatible types. Returning NotImplemented signals to Python to give the right-hand value a say in the matter too. If a.__lt__(b) returns NotImplemented then b.__gt__(a) is tested too.

The base object provides default implementations for the methods, returning NotImplemented:

>>> class Foo:
...     pass
... 
>>> Foo() < Foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: Foo() < Foo()
>>> Foo().__lt__
<method-wrapper '__lt__' of Foo object at 0x10f1cf860>
>>> Foo().__lt__(Foo())
NotImplemented

which is exactly what dict.__lt__ does:

>>> {}.__lt__({})
NotImplemented

Numbers, however, only return NotImplemented when the other type is not comparable:

>>> (1).__lt__(2)
True
>>> (1).__lt__('2')
NotImplemented
>>> 1 < 2
True
>>> 1 < '2'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unorderable types: int() < str()

As such, your best choice is to simply catch the TypeError thrown when values are not comparable.


* I am not aware of any types in the Python 3 standard library that do not implement the comparison methods at this time.

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • ABCs do not only state what attributes are there. That's all they can *enforce*, but since they are opt-in (need to inherit from it or explicitly register with it), most ABCs can and do define a contract. It is more than reasonable to assume, when `isinstance(x, abc.Sized)`, that `x.__len__` is a method returning a non-negative integer. It isn't checked programmatically, of course, but with Python being as dynamic as it is, nothing can really be checked. –  Apr 05 '15 at 12:30
  • @delnan: sure, that is the intention of ABCs. But since the contract for the comparison methods is to return `NotImplemented` when passed an object type that is incompatible, you cannot test for implementation of that contract to that extent using ABCs. – Martijn Pieters Apr 05 '15 at 12:33
  • True, the double dispatch makes it more complicated. I'm mostly objecting to the general statements "only dictates what attributes are there" and "(or even if those attributes are actually methods)". –  Apr 05 '15 at 12:36
  • Thanks for the detailed answer. Do you know if there are many cases of objects that can be compared with other types, besides numbers? – Francis Colas Apr 05 '15 at 13:24
  • @Fracis: in Python 2 almost all types are comparable to other types; this is a frequent source of bugs (especially when numbers and strings are involved); this was done to support sorting heterogenous sequences. In Python 3 I can't currently think of any examples outside of numbers (where floats and integers have clearly defined ordering). – Martijn Pieters Apr 05 '15 at 14:04
  • Something interesting with respect to the implicit contract of ABCs is the example of `Hashable`: a list is not hashable, a tuple is (even if its content is not); both have the `__hash__` attribute but it's not callable (`None`) for the list and for the tuple you get a `TypeError` if some of the content is not hashable. That is, indeed, another example of an ABC not being able to tell the full story. – Francis Colas Apr 06 '15 at 08:52
  • Contrary to user395760's assertion, only some ABCs are opt-in. ABCs like `collections.abc.Sized` (there is no `abc.Sized`) automatically detect objects with the corresponding method and return `True` for `isinstance`, without any explicit inheritance or registration. – user2357112 Aug 29 '19 at 20:40
  • @user2357112 that’s not what Delnan was talking about. Yes, many collection.abc ABCs (all the non-datatype classes, basically) define a [`__subclasshook__`](https://docs.python.org/3/library/abc.html#abc.ABCMeta.__subclasshook__), so `isonstance()` and `issubclass()` tests succeed when the methods are present. He was arguing about what will happen when you call the methods, as ABCs can’t enforce implementation or argument or return types. – Martijn Pieters Aug 30 '19 at 02:42
  • @MartijnPieters: The comment doesn't make it explicit, but the "need to inherit from it or explicitly register with it" part suggests a belief that `isinstance` won't return `True` unless you explicitly inherit or register. I don't know what else delnan might have been thinking `register` opts into, besides `True` results for `isinstance` tests. – user2357112 Aug 30 '19 at 03:10