Homework 3 - The Grid class

Due by 11:59pm on February 26, 2025

Example Input and Output

grid = Grid.build([[5, 4, 3, 3, 4], [4, 1, 5, 2, 2]]) #new method
repr(grid) #updated repr
>>> 'Grid.build([[5, 4, 3, 3, 4], [4, 1, 5, 2, 2]])'
copy_grid = grid.copy() #new method
grid2 = [[1, 1, 1], [2, 3, 5]]
grid == grid2 #updated
>>> False
Grid.check_list_malformed([[2,1],[55,55,55,55]]) #static method
>>> ValueError

Objectives

  • Develop a fully functional class

Introduction

In Lab 11 you implemented some parts of the Grid class. In this Homework you will complete that class in preparation for Project 2.

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

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

In this homework you'll implement the rest of the Grid interface 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 homework03.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 check_list_malformed() first. This method should have the built-in Python @staticmethod decorator added to it. This will allow us to call it without actually having a Grid object, which makes sense because check_list_malformed() has nothing to do with actually managing a grid. Thus the function definition for this method should look like:

@staticmethod
def check_list_malformed(lst):
    """
    Given a list that represents a 2D nested Grid, check that it has the
    right shape. Raise a ValueError if it is malformed.
    >>> Grid.check_list_malformed([[1, 2], [4, 5]])
    >>> Grid.check_list_malformed(1)
    Traceback (most recent call last):
    ...
    ValueError: Input must be a non-empty list of lists.
    >>> Grid.check_list_malformed([[1, 2], [4, 5, 6]])
    Traceback (most recent call last):
    ...
    ValueError: All items in list must be lists of the same length.
    >>> Grid.check_list_malformed([[1, 2], 3])
    Traceback (most recent call last):
    ...
    ValueError: Input must be a list of lists.
    """

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

When we eventually write the build() method, it will take 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

The purpose of check_list_malformed() is to verify that the data is in the correct "nested list" 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. Be wary of nesting your code too much -- if the logic starts to get complicated with lots of levels of indentation, ask yourself there's a simpler way to check these conditions.

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 object and thus don't have one yet:

@staticmethod
def build(lst):
    """
    Given a list that represents a 2D nested Grid construct a Grid object.
    Grid.build([[1, 2, 3], [4, 5 6]])
    >>> Grid.build([[1, 2, 3], [4, 5, 6]]).array
    [[1, 2, 3], [4, 5, 6]]
    """

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 check_list_malformed() method throws an exception, we want it to kill this method as well. Thus, 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 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? Why? 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) or you can 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 build() 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 can be done in 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 11, we wrote 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 11.

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 see that the nested list we pass in provides all three of those essential "ingredients" to construct a grid object.

The __repr__() method should return a string containing Grid.build(), where build() takes as an argument the representation provided by the repr() function of the internal data array (nested list containing the data) 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)

Finally, let's extend the usefulness of our __eq__() method. Right now, it only accepts Grid objects as the items it can compare with "self" (or, the current grid) to see if their array attributes are the same. You are going to add to this function to allow comparison between a current grid object and 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 Lab 11.

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. Make sure you name them exactly as we specified.