In this lab, you’ll work with basic class design and implementation to model fractions.

Exercise 8.1

To get started with this lab exercise do the following things.

  1. Create a folder called lab08.
  2. Create a new file named fraction.py.
  3. Add documentation at the top of the file indicating the purpose of the lab, your name(s) and id(s) and the date.
  4. Import the system-specific parameters and functions using import sys
  5. Create an empty class named Fraction.
  6. Add a documentation line at the bottom of your file that separates your class definition from your main code.

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.

Implementing Class Attributes

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.

Exercise 8.2

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.

Implementing Class Behaviors

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.

Exercise 8.3

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 .

Explicit-Value Constructor

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.

Exercise 8.4

Update the __init__ method to use the following (slightly more involved) algorithm:

  1. Update the parameter list to include two default parameters:
    1. numerator by default given the value 0.
    2. denominator by default given the value 1.
  2. If the values of numerator and denominator satisfy the invariant, then:
    1. Set self._numerator = numerator.
    2. Set self._denominator = denominator.
    Otherwise:
    1. Print an error message to sys.stderr. You can use this code print('Unable to create invalid fraction', file=sys.stderr).
    2. Exit the program using 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:

f_bad = Fraction(1/2)    # Incorrect - This only has one argument (whose value is 0.5!)
The 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.

Accessor Methods

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.

Exercise 8.5

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.

Mutator Method

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.

Exercise 8.6

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:

  1. Set gcd = the greatest common divisor of the numerator and denominator.
         Note, you can compute this by calling computeGCD(self._numerator, self._denominator).
  2. If gcd is not equal to 0 then:
    1. Set self._numerator = self._numerator//gcd.
    2. Set self._denominator = self._denominator//gcd.
  3. If self._denominator is less than 0 then:
    1. Set self._numerator = self._numerator * -1.
    2. Set 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.

Object Interaction

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.

Multiplication

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

Exercise 8.7

Build a fraction multiplication method that implements the following algorithm for the __mul__ method:

  1. Receive fraction object other by which to multiply myself.
  2. Return a new fraction object whose:
    • numerator is equal to self._numerator * other.get_numerator(), and
    • denominator is equal to 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.

Checking In

Submit all the code and supporting files for the exercises in this lab. We will grade this exercise according to the following criteria:

If you’re working on a lab computer, don’t forget to log off of your machine when you are finished!