Welcome, guest | Sign In | My Account | Store | Cart

Given a class defining one or more ordering methods, this decorator supplies the rest. This simplifies and speeds-up the approach taken in recipe 576529.

Python, 28 lines
 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
def total_ordering(cls):
    'Class decorator that fills-in missing ordering methods'    
    convert = {
        '__lt__': [('__gt__', lambda self, other: other < self),
                   ('__le__', lambda self, other: not other < self),
                   ('__ge__', lambda self, other: not self < other)],
        '__le__': [('__ge__', lambda self, other: other <= self),
                   ('__lt__', lambda self, other: not other <= self),
                   ('__gt__', lambda self, other: not self <= other)],
        '__gt__': [('__lt__', lambda self, other: other > self),
                   ('__ge__', lambda self, other: not other > self),
                   ('__le__', lambda self, other: not self > other)],
        '__ge__': [('__le__', lambda self, other: other >= self),
                   ('__gt__', lambda self, other: not other >= self),
                   ('__lt__', lambda self, other: not self >= other)]
    }
    if hasattr(object, '__lt__'):
        roots = [op for op in convert if getattr(cls, op) is not getattr(object, op)]
    else:
        roots = set(dir(cls)) & set(convert)
    assert roots, 'must define at least one ordering operation: < > <= >='
    root = max(roots)       # prefer __lt __ to __le__ to __gt__ to __ge__
    for opname, opfunc in convert[root]:
        if opname not in roots:
            opfunc.__name__ = opname
            opfunc.__doc__ = getattr(int, opname).__doc__
            setattr(cls, opname, opfunc)
    return cls
  • Uses dir to find all methods defined for a class.
  • If ordering methods are defined, picks one to use as a root.
  • Defines each missing operation with one based on the root.

The recipe can be built-out to define __eq__ and __ne__ automatically, but I think it wise to leave equality testing separate. Equality tests should usually do type testing and return False if the types mismatch. Inequality tests should demand comparison only to known types and return NotImplemented for types it doesn't know how to compare.

It's also possible to build-out the recipe to allow methods to be overwritten (just remove the opname not in roots test). The idea is that overwriting helps ensure that the ordering relations are consistent. I don't think overwriting is a good idea though. When a programmer explicitly includes a method, it's not wise to use an implicit action to overwrite it -- the programmer may have had good reason. Also, an implicit overwriting action leaves the code text out-of-sync with what the class actually does -- that creates maintenance challenges. Better to leave this stone unturned and respect the programmer's wishes.

The function above was added to Python 2.7 and Python 3.2 in the functools module.

4 comments

D Torpey 15 years ago  # | flag

Nice recipe.

Why do you use string replaces instead of a dictionary?

Benjamin Peterson 13 years, 7 months ago  # | flag

Note that this doesn't work on Python 3.

Sam Denton 13 years, 3 months ago  # | flag

In Python 2.5 (still used by Google app engine), int doesn't have any relational methods, but float and complex do. Thus, you need to change opfunc.__doc__ = getattr(int, opname).__doc__ to opfunc.__doc__ = getattr(float, opname).__doc__

pzelnip 11 years, 9 months ago  # | flag

"Inequality tests should demand comparison only to known types and return NotImplemented for types it doesn't know how to compare."

But this recipe doesn't do this, if we take a user defined class that has been decorated with total_ordering, and compare it to a string, you get:

"sdfafdas" < MyClass() File "/usr/lib/python2.7/functools.py", line 56, in <lambda> '__lt__': [('__gt__', lambda self, other: other < self), File "/usr/lib/python2.7/functools.py", line 56, in <lambda> '__lt__': [('__gt__', lambda self, other: other < self), File "/usr/lib/python2.7/functools.py", line 56, in <lambda> '__lt__': [('__gt__', lambda self, other: other < self), File "/usr/lib/python2.7/functools.py", line 56, in <lambda> '__lt__': [('__gt__', lambda self, other: other < self), ... many lines deleted .... RuntimeError: maximum recursion depth exceeded

Created by Raymond Hettinger on Tue, 10 Mar 2009 (MIT)
Python recipes (4591)
Raymond Hettinger's recipes (97)
HongxuChen's Fav (39)

Required Modules

  • (none specified)

Other Information and Tasks