7.4 The fractions Module: Rational Numbers
The fractions module provides support for rational number arithmetic through the Fraction class, allowing exact representation of numbers that can be expressed as a ratio of two integers. This is fundamentally different from float, which uses binary floating-point approximation. The Fraction type is invaluable in domains requiring precise fractional calculations, such as financial applications, symbolic mathematics, and educational contexts where exact results are paramount.
Creating Fraction Objects
A Fraction instance can be constructed in several ways. The most straightforward method is by passing two integers representing the numerator and denominator. If only one argument is provided, it is taken as the numerator with a denominator of 1. The constructor also accepts other rational number types, including other Fraction instances, strings, and decimal.Decimal objects. The module automatically normalizes fractions to their simplest form upon creation, using the greatest common divisor (GCD).
from fractions import Fraction
# From numerator and denominator
f1 = Fraction(3, 4) # Represents 3/4
f2 = Fraction(6, 8) # Will be normalized to 3/4
# From a single integer
f3 = Fraction(5) # Represents 5/1
# From a string
f4 = Fraction('22/7') # Represents 22/7
f5 = Fraction('3.14159') # Converts from decimal string
# From a float (generally not recommended)
f6 = Fraction(0.125) # Exact: 1/8
f7 = Fraction(0.1) # May be imprecise due to float's inherent inaccuracy
print(f7) # Output: 3602879701896397/36028797018963968
Why Constructing from Float is a Pitfall
It is crucial to understand the significant pitfall of initializing a Fraction directly from a float. A float is an approximation stored in binary. When you pass 0.1 to Fraction, it doesn’t represent the decimal number 0.1; it represents the exact value of the underlying binary approximation. This often results in a fraction with a very large and unexpected numerator and denominator, as shown with f7 above. The best practice is to avoid this method entirely. Instead, use integers, strings, or Decimal objects for precise initialization.
# Correct ways to represent 1/10
good_fraction1 = Fraction(1, 10)
good_fraction2 = Fraction('0.1')
good_fraction3 = Fraction('1/10')
from decimal import Decimal
good_fraction4 = Fraction(Decimal('0.1'))
Arithmetic Operations and Mixing with Other Types
Fraction objects seamlessly support all basic arithmetic operations (+, -, *, /, **). When performing operations with other Fraction objects or integers, the result is another Fraction, preserving exactness. This behavior is a key advantage over floats.
f1 = Fraction(1, 2)
f2 = Fraction(1, 3)
# Operations with other fractions and integers
result_add = f1 + f2 # Fraction(5, 6)
result_sub = f1 - 2 # Fraction(-3, 2)
result_mul = f1 * f2 # Fraction(1, 6)
result_div = f1 / f2 # Fraction(3, 2)
When a Fraction is used in an operation with a float, the result is promoted to a float. This happens because float is considered the broader, more approximate type in Python’s type hierarchy. This implicit conversion can silently introduce the imprecision of floating-point arithmetic into your calculation, undermining the purpose of using fractions.
mixed_result = f1 + 0.1 # Result is a float: 0.6
print(mixed_result == 0.6) # Output: False (due to float imprecision)
Accessing Numerator and Denominator and Conversion
Every Fraction instance has numerator and denominator attributes, which are integers in their simplest, normalized form. You can convert a fraction to other numeric types, but each conversion has implications. Converting to int truncates towards zero. Converting to float loses the exactness and subjects the value to floating-point representation limits. The limit_denominator() method is highly useful for finding a rational approximation with a bounded denominator, which is excellent for approximating irrational numbers or recovering from a float initialization error.
f = Fraction(31, 4)
num = f.numerator # 31
den = f.denominator # 4
as_int = int(f) # 7 (truncation)
as_float = float(f) # 7.75
# Approximating pi
pi_approx = Fraction('3.141592653589793').limit_denominator(10000)
print(pi_approx) # Output: 355/113 (a famous approximation of pi)
Practical Applications and Best Practices
The primary use case for Fraction is any scenario where precision and exact representation are more important than performance. Its best practices include:
- Always Prefer String or Integer Input: Never initialize a
Fractionfrom afloatif you require exactness. Use the string constructor (Fraction('0.1')) or integer arguments (Fraction(1, 10)). - Be Mindful of Performance: Arithmetic with fractions is significantly slower than with floats or integers because each operation involves GCD calculations for normalization. It is not suitable for high-performance numerical computing.
- Use for Exact Comparisons: Fractions are ideal for algorithms that require exact equality checks, which are unreliable with floats.
- Employ
limit_denominatorfor Float Recovery: If you must get a fraction from a float, useFraction.limit_denominator()to get a manageable and intuitive fraction.