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
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