Homework 3 - The Grid class

Objectives

  • Develop a fully functional class

Introduction

In Labs 7, 8, and 9 you've been working on parts of the Grid class. In this Homework you will complete that class in preparation for using the class to implement Project 2.

So far you've created the following methods in your Grid class:

  • __init__()
  • __str__()
  • __repr__()
  • __eq__()
  • get()
  • set()
  • in_bounds()

For this homework you implement the rest of the Grid API consisting of the following methods:

  • build() - this method takes a nested list and turns it into a Grid object.
  • copy() - returns a copy of the current Grid object
  • check_list_malformed() - This method verifies that the nest list passed to the build() method is of the correct structure (i.e. rectangular) to properly build a grid.
  • __repr__() - You'll give this the correct functionality
  • __eq__() - You'll expand the functionality of this one.

The hw03.zip file has the test code for this homework and a blank Grid.py file. You can just replace that blank file with the one you've already been working on.

Part 1 - Remaining Functionality

Task 1 - check_list_malformed() (10 pts)

Since the build() method needs the check_list_malformed() method, let's implement that first. This method should have the @staticmethod decorator added to it. This will allow us to call it without actually having a Grid object, which makes sense because it has nothing to do with actually managing a grid. This is a built-in python decorator. Thus the function definition for this method should look like:

@staticmethod
def check_list_malformed(lst):

This is a data sanity check method that verifies that the user's input data is good before we try to use it in our class. One of the points of Object Oriented programming and data encapsulation is to make sure that data is always valid and in a good state. This method will checks to see that the passed in data is in a valid format and throws an exception if it is not.

build() takes as input a nested list where each element in the outer list is a row of the grid and each element in each of the nested list are the values for the columns. So a list that looks like this:

lst = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] 

corresponds to a grid that looks like this:

grid representation of nested list

Our check_list_malformed() method's job is to verify that the data is in that format. The data passed in to build() has to meet the following criteria:

  1. The object passed in should be a list object
  2. The top-level list should not be empty
  3. Each element of the list object should also be a list object
  4. Each element of the top-level list should have the same length

Create the check_list_malformed() method in your Grid class and write code that checks each of the conditions listed above. If the object passed in doesn't pass a test, then the code should throw (raise) a ValueError() exception with an appropriate message.

If all the checks pass, the method should just end and does not need to return anything.

Note: If you want to review static methods, review the Class lab's section about static methods here.

Task 2 - build() (10 pts)

Now that check_list_malformed() is written we are ready to write the build() method. This method should also have the @staticmethod decorator added to it, since we are using it to create a Grid and thus don't have one yet:

@staticmethod
def build(lst):

This method should do the following:

  1. Call check_list_malformed() on the passed in list. Do not do this in a try block. If that method throws an exception we want it to kill this method as well so we don't want to catch the exception here.
  2. Determine height and width of the grid from the len of the list object passed in (the height) and the length of one of the sub lists (the width).
  3. Create a new Grid object with the height and width.
  4. Set the new Grid's array attribute to a deep copy of the list that was passed in. If you remember from your implementation of the __init__() method, that is the format we are using to represent the grid.
  5. Return the newly created Grid object.

In step 4, we need to do a deep copy so that our grid is fully encapsulated by our class object. If we just assign the existing list to the .array attribute, think about what happens if someone changes the original list passed in? Will that change the data in our object? Remember that part of writing classes is so that the class has completely control over its data and the data can only be changed through the class methods.

To do a deep copy, you can copy the values by hand (a set of loops) you use Python's deepcopy() function in the copy library. Just add:

from copy import deepcopy

to your file to access the function.

In the main code block of your file, write some code to test that this is working and verify that you are getting a separate copy of the data and not pointing at the same list object that was passed in.

Task 3 - Copy the Grid (6 pts)

The final bit is to write the copy() method that will return a copy of the Grid object it is invoked on. It takes no parameters and returns a Grid object that is a copy of the current one.

The implementation of this method is straightforward and actually only needs one line of code as you can use methods you've already implemented to create the copy. Remember, the copy should be completely independent of the original.

Once you've implemented the copy() method, write some code in your main block to test it and verify that it is working properly.

Part 2 - Functionality Updates

Task 1 - Update __repr__() (6 pts)

In Lab 8, we had to write a simple __repr__() function that just printed the same thing as the __str__() method so that the function would be present. Now we'll get it fully functional so that it does what __repr__() is supposed to do: return a string that can be evaluated to recreate the original object. To do that, we needed the build() method that you hadn't written in Lab 8.

In order to recreate the class we need three things: the height, the width, and the data. If we look at our build() method, we can get all of those things by passing in a nested list to represent the data.

For your __repr__() method, it should return a string containing a call to Grid.build() with the argument being the representation provided by the repr() function of the internal data array of the current object.

For example, if you create a Grid object using the build() method like so:

>>> grid = Grid.build([[1, 2], [3, 4]])

calling the repr() function on it will return a string formatted like so:

>>> repr(grid)
'Grid.build([[1, 2], [3, 4]])'

Update your __repr__() method and test it to make sure it works.

Task 2 - Extend the __eq__() method (6 pts)

As the final task we will extend the usefulness of our __eq__() method. Right now, it only accepts Grid objects as the item it will compare to. You are going to add the functionality to also accept a nested list.

In the if statement that checks to see if other is an instance of Grid, add an elif clause to check if other is an instance of list. If it is, return self.array == other.

Turn in your work

A note on grading. The alert student may have noticed that the parts of this Homework only add up to 38 points. There are still 50 points possible for the assignment, the other 12 points will come from tests on the parts of the class you wrote in Labs 7 & 8.

You'll submit your Grid.py file on Canvas via Gradescope where it will be checked via the auto grader. We will be testing all of the methods of your class to verify that they are working properly.

© 2023 Brigham Young University, All Rights Reserved