In this lab, you’ll work with basic class design and implementation to model fractions.
To get started with this lab exercise do the following things.
lab08
.
fraction.py
.
import
sys
Fraction
.
At this point we have the basic structure for our file, but nothing interesting yet happens. In the next sections, we will add functionality to our class, and then test that functionality in the main code area at the bottom of our file.
The state of an object is represented by the values of its attributes. For example an object representing the fraction one-half will have a numerator of 1 and a denominator of 2. These attributes will be implemented as private instance variables.
At this point, it’s important to think about valid and invalid object states. Because division by zero is undefined, a fraction object must have a denominator that is not zero. This condition must always be true and is thus called an invariant.
Your fraction class will need to keep track of the fraction’s
numerator and denominator. Add instance variables to your Fraction
class to represent these values by defining them within a method
called __init__
which receives self
as a
parameter. Use the Python convention to make them private by naming
them _numerator
and _denominator
and make
the default fraction 0/1
.
Just like functions, classes and their methods should be well
documented. Add a docstring for your class, including a one-line
description of what we are modeling and a specification of the class
invariant with respect to the values of the instance variables. Also
add a docstring for the __init__
method making clear
that this method initializes a fraction object.
At this point, it should be possible to create a Fraction object, but it won't do anything interesting yet. Let's add some functionality.
The behavior of class objects is specified using methods, including output methods, accessors, mutators and other utility methods. The remaining exercises address these methods one at a time.
__str__
Method
When python prints out an object, it automatically calls the __str__
method defined for that object. A new class can override the default
implementation of this function to specify exactly how it would like
its object to be printed.
Implement a __str__
method that receives self
and returns a string representing the current state of the fraction
object. For example, if the fraction is representing one-half (i.e.,
self._numerator
is 1 and self._denominator
is 2), then return “1/2”.
To test that your method is working, down at the bottom of your file
(below the comment separating the class code from the main code),
create a new Fraction object named f1
and check that it
can be printed using print(f1)
This program should output 0/1
.
It is likely that programmers will want to create their own fraction objects and to set numerator and denominator themselves. For this, we would like a constructor that accepts initial values as parameters.
Update the __init__ method to use the following (slightly more involved) algorithm:
numerator
by default given the value 0.denominator
by default given the value 1.numerator
and
denominator
satisfy the invariant, then:
self._numerator
= numerator
.self._denominator
= denominator
.sys.stderr
.
You can use this code print('Unable to create invalid
fraction', file=sys.stderr)
.sys.exit(-1)
Since we have modified this method, update the documentation to be accurate.
Now, to test the behavior of this new constructor, add the following code to your driver program at the bottom of the file:
f2 = Fraction(1, 2) #Construct a fraction object for one half. print(f2) #Print the object.
This new code segment should output 1/2
. Plan and
implement a few additional test cases to be sure that your
constructor is working properly. Comment these tests as Exercise 8.4.
In particular, try a fraction with a denominator of zero to see that
it prints an error appropriately. After you have verified that your
program has the desired behavior, comment
out the test case that causes the program to exit.
Note that when the programmer creates a fraction object representing
one-half, they do not pass 1/2
to the explicit-value
constructor, as in:
Thef_bad = Fraction(1/2) # Incorrect - This only has one argument (whose value is 0.5!)
Fraction
class knows very well how to represent and work
with fractions; it does not need the programmer to tell it what to do
with the numerator and denominator, all it needs is the (separate!)
values for the numerator and the denominator, as in:
f_good = Fraction(1, 2) # Correct - This passes the numerator and denominator for proper handling by the class.
Classes generally treat their instance variables as private, but it is nevertheless useful at times for programmers who use the class to be able to get the values of those instance variables for special uses. This behavior is implemented using accessors, which are simple methods that return the value of the instance variable.
Write two accessor methods for your fraction class, one for the numerator and the other for the denominator . Name these methods following the convention for accessors (namely, using the prefix get_). When they are implemented, document them and add lines to your console program to test them.
Some methods make changes to the state of a class object. These
methods are called mutator methods. For the fraction class,
it will prove convenient to have a mutator method that simplifies the
current state of the fraction. For example, if the fraction is 2/4
(i.e., the numerator is 2 and the denominator is 4), then we’d
really like to be able to simplify the fraction to the value 1/2.
You’ll use this method in your constructor to simplify new
fraction objects. For example, if a programmer attempts to create the
fraction Fraction(2, 4)
, we would like the constructor
to automatically simplify this to 1/2.
Simplifying fractions requires that you be able to compute the greatest common divisor of the numerator and the denominator. For example, the numerator 2 and denominator 4 share 2 as their greatest common divisor, which means that we can divide both by 2 to simplify 2/4 to (2/2)/(4/2) = 1/2. The greatest common divisor can be computed using the following function.
def computeGCD(alpha, beta): ''' (int, int) -> int This function finds the greatest common divisor of two integers, using Euclid's algorithm ''' alpha = abs(alpha) beta = abs(beta) remainder = alpha % beta while (remainder != 0): alpha = beta beta = remainder remainder = alpha % beta return beta
This function is not specific to a Fraction object, but needs to be available to a fraction object. Thus, copy this function into your file, putting it above the definition of the class, but below the import statement.
Implement a fraction simplification method by following the algorithm
below to define a method called simplify
. As a method, simplify
receives self, then creates the lowest common form
of the fraction, and puts the negative sign (if present) on the
numerator:
gcd
= the greatest common
divisor of the numerator and denominator. computeGCD(self._numerator,
self._denominator)
.
gcd
is not equal to 0 then:
self._numerator
= self._numerator//gcd
.self._denominator
= self._denominator//gcd
.self._denominator
is less
than 0 then:
self._numerator
= self._numerator
* -1.self._denominator
= self._denominator
* -1.
Note that this method does not return anything. It is a mutator
method that changes the values of self._numerator
and self._denominator
but returns nothing.
When this method is complete, document it properly and add an
invocation for it in your constructor just after you’ve
assigned the values to self._numerator
and self._denominator
. The invocation should look like this:
self._numerator = numerator # These first two statements should already be there. self._denominator = denominator self.simplify() # This simplifies the values provided by the calling program.
With this all in place, test it out by adding the following code segment to the bottom of the file:
f3 = Fraction(2, 4) # Construct a fraction object for two fourths. print(f3) # Print the object.
This new code segment should output 1/2
. Plan and
implement a few additional test cases to be sure that your
constructor is working properly (as usual, comment these tests as
Exercise 8.6). In particular, try a case in which both the numerator
and denominator are set to negative numbers; step 3 in the algorithm
is a clever bit of coding that should handle negatives appropriately.
There are times when fractions must interact with other fractions. The methods in this section are not mutator methods. They create a new fraction with the appropriate value.
Multiplying one fraction with another should produce a new fraction with the appropriate numerator and denominator. For example, 1/2 * 2/3 = 2/6 which simplifies to 1/3.
We would like to use the *
operator for multiplication,
and Python allows for this via operator overloading. We can overload
many built-in operators by defining special methods. The
special method for *
is __mul__
.
Build a fraction multiplication method that implements the following
algorithm for the __mul__
method:
other
by which to multiply myself.self._numerator
* other.get_numerator()
,
and
self._denominator
* other.get_denominator()
.
Note that you can construct this new fraction using the constructor, which automatically simplifies the new fraction’s form.
As usual, document this new method and then include a couple test
cases for your multiplication method in your console application
(document the tests as Exercise 8.7). For example, you can multiply
the f1
and f2
fraction objects created
above by saying f1 * f2
.
Submit all the code and supporting files for the exercises in this lab. We will grade this exercise according to the following criteria:
__str__
method.
If you’re working on a lab computer, don’t forget to log off of your machine when you are finished!