'''rational.py: Module to do rational arithmetic.
For full documentation, see http://www.nmt.edu/tcc/help/lang/python/examples/rational/.
Exports:
gcd ( a, b ):
[ a and b are integers ->
return the greatest common divisor of a and b ]
Rational ( a, b ):
[ (a is a nonnegative integer) and
(b is a positive integer) ->
return a new Rational instance with
numerator a and denominator b ]
.n: [ the numerator ]
.d: [ the denominator ]
.__add__(self, other):
[ other is a Rational instance ->
return the sum of self and other as a Rational instance ]
.__sub__(self, other):
[ other is a Rational instance ->
return the difference of self and other as a Rational
instance ]
.__mul__(self, other):
[ other is a Rational instance ->
return the product of self and other as a Rational
instance ]
.__div__(self, other):
[ other is a Rational instance ->
return the quotient of self and other as a Rational
instance ]
.__str__(self):
[ return a string representation of self ]
.__float__(self):
[ return a float approximation of self ]
.mixed(self):
[ return a string representation of self as a mixed
fraction ]
'''
def gcd ( a, b ):
'''Greatest common divisor function; Euclid's algorithm.
[ a and b are integers ->
return the greatest common divisor of a and b ]
'''
if b == 0:
return a
else:
return gcd(b, a%b)
class Rational:
"""An instance represents a rational number.
"""
def __init__ ( self, a, b ):
"""Constructor for Rational.
"""
if b == 0:
raise ZeroDivisionError, ( "Denominator of a rational "
"may not be zero." )
else:
g = gcd ( a, b )
self.n = a / g
self.d = b / g
def __add__ ( self, other ):
"""Add two rational numbers.
"""
return Rational ( self.n * other.d + other.n * self.d,
self.d * other.d )
def __sub__ ( self, other ):
"""Return self minus other.
"""
return Rational ( self.n * other.d - other.n * self.d,
self.d * other.d )
def __mul__ ( self, other ):
"""Implement multiplication.
"""
return Rational ( self.n * other.n, self.d * other.d )
def __div__ ( self, other ):
"""Implement division.
"""
return Rational ( self.n * other.d, self.d * other.n )
def __str__ ( self ):
'''Display self as a string.
'''
return "%d/%d" % ( self.n, self.d )
def __float__ ( self ):
"""Implement the float() conversion function.
"""
return float ( self.n ) / float ( self.d )
def mixed ( self ):
"""Render self as a mixed fraction in string form.
"""
#-- 1 --
# [ whole := self.n / self.d, truncated
# n2 := self.n % self.d ]
whole, n2 = divmod ( self.n, self.d )
#-- 2 --
# [ if self.d == 1 ->
# return str(self.n)
# else if whole == zero ->
# return str(n2)+"/"+str(self.d)
# else ->
# return str(whole)+" and "+str(n2)+"/"+str(self.d) ]
if self.d == 1:
return str(self.n)
elif whole == 0:
return "%s/%s" % (n2, self.d)
else:
return "%s and %s/%s" % (whole, n2, self.d)
3. Contents of the rational.py
module
The actual code of the rational.py
module is displayed here, with commentary. This document is therefore an example of lightweight literate programming; see the author's Lightweight literate programming page for more information about the tools and techniques used in this document.
The rational.py
file starts with a module documentation string that describes the class interface. This is basically a restatement of the interface as described above, using Cleanroom intended functions to document each attribute and method. For more information on the Cleanroom methodology, see the author's Cleanroom page.
'''rational.py: Module to do rational arithmetic. For full documentation, see http://www.nmt.edu/tcc/help/lang/python/examples/rational/.
To simplify fractions (for example, reducing 4/8 to 1/2), we will need a function to find the greatest common divisor of two numbers.
Exports: gcd ( a, b ): [ a and b are integers -> return the greatest common divisor of a and b ]
Here is the class constructor.
Rational ( a, b ): [ (a is a nonnegative integer) and (b is a positive integer) -> return a new Rational instance with numerator a and denominator b ]
We make the numerator and denominator values available outside the class as visible attributes named n
and d
, respectively.
.n: [ the numerator ] .d: [ the denominator ]
We implement all four of the common mathematical operators: +
, -
, *
, and /
. These operations are implemented by defining methods that use certain special names, such as __add__
for addition.
.__add__(self, other): [ other is a Rational instance -> return the sum of self and other as a Rational instance ] .__sub__(self, other): [ other is a Rational instance -> return the difference of self and other as a Rational instance ] .__mul__(self, other): [ other is a Rational instance -> return the product of self and other as a Rational instance ] .__div__(self, other): [ other is a Rational instance -> return the quotient of self and other as a Rational instance ]
The built-in Python functions str()
and float()
are also implemented using special method names.
.__str__(self): [ return a string representation of self ] .__float__(self): [ return a float approximation of self ]
Finally, the .mixed()
method that converts an instance to a string displaying the fraction as a mixed fraction:
.mixed(self): [ return a string representation of self as a mixed fraction ] '''
This function implements Euclid's algorithm for finding the greatest common divisor of two numbers a
and b
.
def gcd ( a, b ): '''Greatest common divisor function; Euclid's algorithm. [ a and b are integers -> return the greatest common divisor of a and b ] '''
Euclid's algorithm is easily defined as a recursive function. See Structure and Interpretation of Computer Programs by Abelson and Sussman, ISBN 0-262-01153-0, pp. 48-49.
-
The GCD of any number
x
and zero is zero. -
The GCD of any two nonzero numbers
a
andb
is the same asGCD(
.b
,a
modulob
)
Defined recursively, this amounts to:
if b == 0: return a else: return gcd(b, a%b)
Here begins the actual class definition.
class Rational: """An instance represents a rational number. """
The constructor takes two external arguments, the numerator and the denominator. It finds the GCD of those two numbers and divides both of them by that GCD, to reduce the fraction to its lowest terms. It then stores the reduced numerator and denominator in the instance namespace under the attribute names n
and d
.
def __init__ ( self, a, b ): """Constructor for Rational. """ if b == 0: raise ZeroDivisionError, ( "Denominator of a rational " "may not be zero." ) else: g = gcd ( a, b ) self.n = a / g self.d = b / g
This method will be invoked to perform the “+
” operator whenever a Rational
instance appears on the left side of that operator. To simplify life, we assume here that the operand on the right side is also a Rational
instance.
Basically, what we are doing is adding two fractions. Here is the algebraic rule for adding fractions:
In this method, self
is the left-hand operand and other
is the right-hand operand.
def __add__ ( self, other ): """Add two rational numbers. """ return Rational ( self.n * other.d + other.n * self.d, self.d * other.d )
This method is called when a Rational
instance appears on the left side of the “-
” operator. The right-hand operand must be aRational
instance as well. See Section 3.5, “Rational.__add__()
: Implement the addition (+
) operator” for the algebra of this operation.
def __sub__ ( self, other ): """Return self minus other. """ return Rational ( self.n * other.d - other.n * self.d, self.d * other.d )
Here's the formula for multiplying two fractions:
def __mul__ ( self, other ): """Implement multiplication. """ return Rational ( self.n * other.n, self.d * other.d )
Here's the formula for dividing one fraction by another:
def __div__ ( self, other ): """Implement division. """ return Rational ( self.n * other.d, self.d * other.n )
The __str__
method of a class is invoked whenever an instance of that class must be converted to a string. This happens, for instance, when you print an instance with a print
statement, or when you use the str()
function on an instance.
def __str__ ( self ): '''Display self as a string. ''' return "%d/%d" % ( self.n, self.d )
This method is called whenever Python's built-in float()
function is called to convert an instance of the Rational
class. To do this, we convert the numerator and the denominator to float
type and then use a floating division.
def __float__ ( self ): """Implement the float() conversion function. """ return float ( self.n ) / float ( self.d )
This method is used to convert a rational number (which may be an improper fraction) to a “mixed fraction”. The general form of a mixed fraction is a phrase of the form “w
and n
/d
” For example, the improper fraction 22/7 is equivalent to the mixed fraction “3 and 1/7”.
The result is returned as a string. There are three cases:
-
If the denominator is 1, we display just the whole-number part. Example: 17/1 becomes simply “17”.
-
If the whole-number part is zero but the fractional part is not, we'll display only the fractional part. Example: 5/6 becomes “5/6”, not “0 and 5/6”.
-
In the general case, there is both a whole-number part and a fractional part. Example: 22/7 becomes “3 and 1/7”.
First we find the whole-number part and the numerator of the fractional part (the denominator of the fractional part will be the same as the denominator of the original rational). Python conveniently provides the divmod()
function, which provides both the quotient and the remainder.
def mixed ( self ): """Render self as a mixed fraction in string form. """ #-- 1 -- # [ whole := self.n / self.d, truncated # n2 := self.n % self.d ] whole, n2 = divmod ( self.n, self.d )
Then we separate the three cases.
#-- 2 -- # [ if self.d == 1 -> # return str(self.n) # else if whole == zero -> # return str(n2)+"/"+str(self.d) # else -> # return str(whole)+" and "+str(n2)+"/"+str(self.d) ] if self.d == 1: return str(self.n) elif whole == 0: return "%s/%s" % (n2, self.d) else: return "%s and %s/%s" % (whole, n2, self.d)
This script exercises the class's functions.
#!/usr/bin/env python #================================================================ # rationaltest: A test driver for the rational.py module. #---------------------------------------------------------------- import sys from rational import * def main(): """ """ generalTests() mixedTests() errorTests() def generalTests(): """Test basic functionality """ print " -- Ambition/distraction/uglification/derision" third=Rational(1,3) print "Should be 1/3:", third fifth=Rational(1,5) print "Should be 1/5:", fifth print "Should be 8/15:", third + fifth print "Should be 1/15:", third * fifth print "Should be 2/15:", third-fifth print "Should be 3/5:", fifth/third print " -- float()" print "Should be 0.2:", float(fifth) print "Should be 0.3333...:", float(third) def mixedTests(): """Test the .mixed() method cases """ print " -- mixed()" badPi = Rational(22,7) print "Should be '3 and 1/7':", badPi.mixed() properFraction = Rational(3,5) print "Should be 3/5:", properFraction.mixed() wholeNum = Rational ( 8,2 ) print "Should be 4:", wholeNum.mixed() zero = Rational(0,1) print "Should be 0:", zero.mixed() def errorTests(): """Test error conditions """ try: badIdea = Rational ( 5, 0 ) print "Fail: didn't detect zero denominator." except ZeroDivisionError, detail: print "Pass: Zero denominator blowed up real good." #================================================================ # Epilogue #---------------------------------------------------------------- if __name__ == "__main__": main()