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.
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.
Nice recipe.
Why do you use string replaces instead of a dictionary?
Note that this doesn't work on Python 3.
In Python 2.5 (still used by Google app engine),
int
doesn't have any relational methods, butfloat
andcomplex
do. Thus, you need to changeopfunc.__doc__ = getattr(int, opname).__doc__
toopfunc.__doc__ = getattr(float, opname).__doc__
"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: