Implementing FixedPoint comparison operators

The following are the six comparison operators and the special methods that implement them:

Method

Operator

object.__lt__(self, other)

<

object.__le__(self, other)

<=

object.__eq__(self, other)

==

object.__ne__(self, other)

!=

object.__gt__(self, other)

>

object.__ge__(self, other)

>=

 

The is operator compares object IDs. We can't meaningfully override this since it's independent of any specific class. The in comparison operator is implemented by object.__contains__(self, value). This isn't meaningful for numeric values.

Note that equality testing is a subtle business. Since floats are approximations, we have to be very careful to avoid direct equality testing with float values. We must compare to see whether the values are within a small range, that is, epsilon. Equality tests should never be written as a == b. The general approach to compare floating-point approximations should be abs(a-b) <= eps, or, more generally, abs(a-b)/a <= eps.

In our FixedPoint class, the scale indicates how close two values need to be for a float value to be considered equal. For a scale of 100, the epsilon could be 0.01. We'll actually be more conservative than that and use 0.005 as the basis for comparison when the scale is 100.

Additionally, we have to decide whether FixedPoint(123, 100) should be equal to FixedPoint(1230, 1000). While they're mathematically equal, one value is in cents and one is in mills.

This can be taken as a claim about the different accuracies of the two numbers; the presence of an additional significant digit may indicate that they're not supposed to simply appear equal. If we follow this approach, then we need to be sure that the hash values are different too.

For this example, we've decided that distinguishing among scale values is not appropriate. We want FixedPoint(123, 100) to be equal to FixedPoint(1230, 1000). This is the assumption behind the recommended __hash__() implementation too. The following are the implementations for our FixedPoint class comparisons:

def __eq__(self, other: Any) -> bool:
if isinstance(other, FixedPoint):
if self.scale == other.scale:
return self.value == other.value
else:
return self.value * other.scale // self.scale == other.value
else:
return abs(self.value / self.scale - float(other)) < .5 / self.scale

def __ne__(self, other: Any) -> bool:
return not (self == other)

def __le__(self, other: 'FixedPoint') -> bool:
return self.value / self.scale <= float(other)

def __lt__(self, other: 'FixedPoint') -> bool:
return self.value / self.scale < float(other)

def __ge__(self, other: 'FixedPoint') -> bool:
return self.value / self.scale >= float(other)

Each of the comparison functions tolerates a value that is not a FixedPoint value. This is a requirement imposed by the superclass: the Any type hint must be used to be compatible with that class. The only requirement is that the other value must have a floating-point representation. We've defined a __float__() method for the FixedPoint objects, so the comparison operations will work perfectly well when comparing the two FixedPoint values.

We don't need to write all six comparisons. The @functools.total_ordering decorator can generate the missing methods from just two FixedPoint values. We'll look at this in Chapter 9, Decorators and Mixins – Cross-Cutting Aspects.

In the next section, we'll see how to compute a numeric hash.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset