Showing posts with label rational numbers. Show all posts
Showing posts with label rational numbers. Show all posts

Thursday, May 8, 2008

More on Python Rationals

Coercing to floats internal to some other operation is inherently bad. And so Jared replaces ...

def __gt__(self, arg):
if not isinstance(arg, type(self)):
arg = self.__class__(arg)
return float(self) > float(arg)

... and friends with ...

def __gt__(self, arg):
if not isinstance(arg, type(self)):
return self.numerator > self.denominator * arg
return self.numerator * arg.denominator > arg.numerator * self.denominator

... or even ...

def __eq__(self, arg):
return self.numerator == self.denominator * arg

... which is especially nice.

Will sign trickiness ever give a wrong result? A couple dozen random cases work here and so I've put revised and reposted the implementation.

Of course what Jared's really interested in is whether the class can — or, in fact, already does — work as rational field. I'm just happy he caught that the fact that we can ditch the __cmp__ override because the presence of __gt__, __eq__, __lt__ and friends.

One question that remains is what to do with int. The current implementation goes towards zero and not towards negative infinity. So int(Rational(-6, 5)) gives -1 and not -2.

def __int__(self):
result = abs(self.numerator) // abs(self.denominator)
if self >= 0:
return result
else:
return -result

But is this right? Perhaps Google will break a tie ...

Tuesday, May 6, 2008

A good python Rational class

Python implements integers, floats, decimals and complex numbers. But no rationals. PEP 239, "Adding a Rational Type to Python", suggests ... the addition of a rational type to python. But alas. Guido said no. And that was 2001.

So developers everywhere implement their own. Duration math depends crucially on rational arithmetic and so Víctor and I are no exception. My first implementation was in 2005, I think Víctor followed soon after, and we've shared a relatively robust implementation this year. And, just this week, we've reimplemented yet again.

Some features.

The initializer requires numerator but leaves denominator optional.

>>> p = Rational(13, 8)
>>> p
13/8

>>> q = Rational(2)
>>> q
2

Unary negation, inversion and absolute value work the way you think they do.

>>> -p
-13/8

>>> ~p
8/13

>>> abs(p)
13/8

So do the inequalities.

>>> p < q, p <= q, p == q, p >= q, p > q
(True, True, False, False, False)

Integers work too.

>>> p
13/8

>>> p < 2, p <= 2, p == 2, p > 2, p >= 2
(True, True, False, False, False)

Arithmetic __add__, __sub__, __mul__, __div__ and even __truediv__ all work.

>>> p, q
(13/8, 2)

>>> p + q
29/8

>>> p - q
-3/8

>>> p * q
13/4

>>> p / q
13/16

Things that are supposed to be commutative are. Others aren't.

>>> p + q == q + p, p * q == q * p
(True, True)

>>> p - q == q - p, p / q == q / p
(False, False)

Right-operators __radd__, __rsub__, __rmul__, __rdiv__ and __rtruediv__ make integers work here too.

>>> p
13/8

>>> 2 + p
29/8

>>> 2 - p
-3/8

>>> 2 * p
13/4

>>> 2 / p
16/13

Floor division comes up every once in a while.

>>> Rational(93, 8) // 2
5

And you can compose with rational mod.

>>> Rational(93, 8) % 2
13/8

>>> Rational(93, 8) % Rational(2, 3)
7/24

>>> 93 % Rational(2, 3)
1/3

Coercion works for int and float

>>> p
13/8

>>> int(p)
1

>>> float(p)
1.625

And there're set and copy methods.

>>> p.set(15, 16)
>>> p
15/16

>>> p.copy( )
15/16
>>> id(p) == id(_)
False

The code is here. If you like -- or find bugs -- let us know.