
One of the most under-appreciated packages in Python is the fractions
package. I already mentioned in this blog that floating points are a good way to shoot yourself in the foot, and rational fractions is often the saner alternative.
Basically this package offers a Fraction
class that represents a rational number with two integers, a numerator and a denominator, like you learnt in school.
a = fractions.Fraction(1, 3) a → Fraction(1, 3) str(a) → ⅓ float(a) → 0.3333333333333333 a * 3 → Fraction(1, 1) a * 3 == 1 → True a + 3 → Fraction(10, 3)
So far, so good. Now what happens when you convert a float into a fraction.
b = fractions.Fraction(0.3) b → Fraction(5404319552844595, 18014398509481984)
What happened here? The fraction representing 0.3 should be fractions.Fraction(3, 10)
! This is the floating point representation roaring its head, the fraction does not represent 0.3, but the floating point representation of 0.3. That conversion is actually exact. Let’s dig a bit more.
hex(b.numerator) → '0x13333333333333' hex(b.denominator) → '0x40000000000000' c = fractions.Fraction(3, 10) - b c → Fraction(1, 90071992547409920) hex(c.denominator) → '0x140000000000000'
But you can force a more compact representation using the limit_denominator
method.
fractions.Fraction(0.3).limit_denominator(100) → Fraction(3, 10)
Now, of course, you can’t convert all floating point values to fractions.
fractions.Fraction(float('Inf')) → OverflowError: cannot convert Infinity to integer ratio