s = "bureaucrats"
s = s.upper()
print(s)BUREAUCRATS
Reading: Python Docs 9.1-9.3
We’ve seen different built-in Python objects that combine data and functionality: strings, for instance, are more than a collection of characters; they also have methods that return new strings, based on the original contents of the string:
Lists have methods that modify the list (and don’t return anything):
K = [6, 4, 5]
v = K.sort()
print("v:", v) # nothing is returned
print("K:", K) # the list was modifiedv: None
K: [4, 5, 6]
Lists also have methods that modify the list and do return something:
K = [6, 4, 5]
v = K.pop() # pop removes the last element of the list and returns it
print("v:", v)
print("K:", K) # the list was modifiedv: 5
K: [6, 4]
We can define our own classes and create new kinds of objects; this is useful whenever we want to combine data and functionality.
Here is a basic class definition.
Note:
classGrid – this is a name we choose, similar to a function name
__init__ is executed whenever a new class is created.
self, x, and yself
self refers to the instance of the object__init__ never returns anything (its return type is always None)
self
The use of the word “self” is a convention. You could, in theory, choose a different name. Nonetheless, the first positional argument refers to the instance of the object, and you should not deviate from the self convention.
We can create a new object by calling the class name as if it were a function, which uses the constructor to create a new object. Because the constructor __init__ was defined as taking two positional arguments other than self, we must create a new object using two positional arguments. We can then access the x and y values for this Grid object:
We can create multiple different instances of the Grid class. These different objects will have different instance variables x and y:
The visualizer illustrates that there are multiple instances of the Grid class (multiple objects).
Our constructor previously took input arguments (x and y) and created instance variables (self.x and self.y) from them, but the constructor __init__ is a function, which means we can put any functionality we want into it. Here, we also create instance variables self.size and self.shape:
We have collected data (instance variables) in the Grid class, with one method, the constructor __init__. Classes can also include other methods that perform functionality: either arbitrary functionality that we define, or “built in” types of functionality.
First, we will specify a numbering scheme for grids of arbitrary dimensions:
Here are examples of a 3x3 grid and a 4x3 grid:

Now, let’s make a method that prints out a representation of a grid for any size.
class Grid:
def __init__(self, x:int, y:int) -> None:
self.x:int = x
self.y:int = y
self.size:int = x * y
self.shape:tuple[int, int] = (x, y)
def print_grid(self) -> None:
grid_str:str = ""
count:int = 0
for j in range(self.y):
for k in range (self.x):
if count < 10:
grid_str += "[ " + str(count) + "]"
else:
grid_str += "[" + str(count) + "]"
count += 1
grid_str += "\n"
print(grid_str)
A = Grid(4, 6)
A.print_grid()[ 0][ 1][ 2][ 3]
[ 4][ 5][ 6][ 7]
[ 8][ 9][10][11]
[12][13][14][15]
[16][17][18][19]
[20][21][22][23]
Note:
Grid.print_grid method is defined with one argument, self
self is always included in class method definitionsself is not part of the method callsWe wrote a method to print out the grid representation, but what happens when we try to print the grid object itself?
This is not particularly useful: it tells us that A is a Grid object, but we don’t know anything else about it. We can define a string representation for any class, which lets us (the human) inspect the object using the print function.
The default string representation uses a built-in method, __repr__. We “override” this by defining it ourselves:
class Grid:
def __init__(self, x:int, y:int) -> None:
self.x:int = x
self.y:int = y
self.size:int = x * y
self.shape:tuple[int, int] = (x, y)
def __repr__(self) -> str:
grid_str:str = ""
count:int = 0
for j in range(self.y):
for k in range (self.x):
if count < 10:
grid_str += "[ " + str(count) + "]"
else:
grid_str += "[" + str(count) + "]"
count += 1
grid_str += "\n"
return grid_str
A = Grid(3, 4)
print(A)[ 0][ 1][ 2]
[ 3][ 4][ 5]
[ 6][ 7][ 8]
[ 9][10][11]
Let’s add another method: this method will check if a given grid square is at a corner of the grid or not. Looking back at the illustrated diagram above, you’ll see that \(2\) is a corner of the 3x3 grid and not a corner of the 4x3 grid; \(3\) is a corner of the 4x3 grid and not a corner of the 3x3 grid.
First, we’ll need the arithmetic to determine if a grid square is in a corner for any given grid:
n has an x-position given by n % xn has a y-position given by n // xA corner exists wherever:
0 or the width minus 1 and0 or the height minus 1We can now add a is_corner method to our class. Note that self.x and self.y have been renamed self.width and self.height to resolve ambiguity:
class Grid:
def __init__(self, x:int, y:int) -> None:
self.width:int = x
self.height:int = y
self.size:int = self.width * self.height
self.shape:tuple[int, int] = (self.width, self.height)
def __repr__(self) -> str:
grid_str:str = ""
count:int = 0
for j in range(self.height):
for k in range (self.width):
if count < 10:
grid_str += "[ " + str(count) + "]"
else:
grid_str += "[" + str(count) + "]"
count += 1
grid_str += "\n"
return grid_str
def is_corner(self, n) -> bool:
x:int = n % self.width
y:int = n // self.width
if (x == 0 or x == self.width - 1) and (y == 0 or y == self.height - 1):
return True
return False
A = Grid(3, 4)
print(A)
print(2, A.is_corner(2))
print(3, A.is_corner(3))
print(4, A.is_corner(4))
print(9, A.is_corner(9))[ 0][ 1][ 2]
[ 3][ 4][ 5]
[ 6][ 7][ 8]
[ 9][10][11]
2 True
3 False
4 False
9 True
One powerful feature of classes is the ability of a class to inherit from another class. We have already defined a basic Grid class: let’s extend the Grid class to add more functionality.
We will add the ability to “select” certain grid squares, and remember which squares are selected in a list. We will do this in a new SelectableGrid class:
class SelectableGrid(Grid):
def __init__(self, x:int , y:int):
super().__init__(x, y)
self.selected:list = []
A = SelectableGrid(3, 5)
print(A.is_corner(2))True
SelectableGrid includes as argument Grid
SelectableGrid will automatically inherit all of the methods and variables from GridGrid, that method will be present in SelectableGrid and function identically__init__
super().__init__ to invoke the original constructorx and y arguments from our new constructor are passed through to the original constructor
A very simple example of inheritance, visualized:
Note that the B class does not have any constructor defined: if we want to inherit a method from the parent class without changing it, we don’t do anything: all of the parent’s methods are automatically inherited.
We can use the super() function to call parent methods when changing them:
Now we’ll add another method to SelectableGrid to “select” certain squares of the grid:
class SelectableGrid(Grid):
def __init__(self, x:int , y:int):
super().__init__(x, y)
self.selected:list = []
def select(self, grid_sq) -> None:
self.selected.append(grid_sq)
A = SelectableGrid(3, 5)
A.select(5)
A.select(2)
print(A.selected)[5, 2]
This sort of works, but isn’t an ideal implementation, logically. We’ll address the issues later, in the practice problems.
It’s possible to create class variables that persist with the entire class, rather than instances of the class. In the definition below, selected is a class variable rather than an instance variable.
class SelectableGrid(Grid):
selected:list = []
def select(self, grid_sq) -> None:
self.selected.append(grid_sq)
A = SelectableGrid(3, 5)
A.select(5)
A.select(2)
B = SelectableGrid(4, 3)
print(A.selected)
print(B.selected)[5, 2]
[5, 2]
A.selected and B.selected both reference the same class variable – which is why they are the same.
A simple example, visualized:
y is an instance variable: it is distinct between different instances of the classx is a class variable: it is shared by every instance of the class!
x from any member of the class will change it for every member of the classself.y references instance variable yself.x references class variable yClass variables can be useful, but just as frequently, confusing class variables and instance variables can cause problems. Be careful!
Write a function shared_corner that takes two basic Grid objects as arguments. The function should return True if the grids “share” any non-zero corner, otherwise, return False.
shared_corner(Grid(3, 3), Grid(4, 3)) returns True (8 is a shared corner)shared_corner(Grid(3, 3), Grid(3, 6)) returns True (2 is a shared corner)shared_corner(Grid(3, 4), Grid(4, 5)) returns False (No shared non-zero corner)Modify the SelectableGrid object from the notes:
Gridselect method should keep only unique selected grid squares:
SelectableGrid.select is called on a grid square that has already been selected, do nothingselect method should not be able to select grid squares that are not on the grid:
SelectableGrid.select is called on a grid square that doesn’t exist in the grid, do nothingChange the SelectableGrid object’s string representation to include more information about the Grid. Show selected squares using parentheses:
should show
3x4 Grid:
[ 0][ 1][ 2]
[ 3][ 4]( 5)
[ 6]( 7)[ 8]
[ 9][10][11]
Try to use the string returned by super().__repr__
Homework problems should always be your individual work. Please review the collaboration policy and ask the course staff if you have questions. Remember: Put comments at the start of each problem to tell us how you worked on it.
Double check your file names and return values. These need to be exact matches for you to get credit.
For this homework, don’t use any built-in functions that find maximum, find minimum, or sort.
These problems are intended to be completed in sequence. Each builds on the previous.
Write a new class CalculationGrid that extends this Grid class:
class Grid:
def __init__(self, x:int, y:int) -> None:
self.width:int = x
self.height:int = y
self.size:int = self.width * self.height
self.shape:tuple[int, int] = (self.width, self.height)
def __repr__(self) -> str:
grid_str:str = ""
count:int = 0
for j in range(self.height):
for k in range (self.width):
if count < 10:
grid_str += "[ " + str(count) + "]"
else:
grid_str += "[" + str(count) + "]"
count += 1
grid_str += "\n"
return grid_str
def is_corner(self, n) -> bool:
x:int = n % self.width
y:int = n // self.width
if (x == 0 or x == self.width - 1) and (y == 0 or y == self.height - 1):
return True
return Falseselect_points in CalculationGrid that takes two arguments, both ints.CalculationGrid.select_points should store those two ints as instance variables selectedA and selectedB if and only if they are not the same, and they are both valid points on the grid:
X.select_points(1, 2) should result in X.selectedA being 1 and X.selectedB being 2, where X is an instance of CalculationGridX.select_points(4, 3) should result in X.selectedA being 4 and X.selectedB being 3, where X is an instance of CalculationGridX.select_points(2, 2) should result in nothing happening (None is retuned and no instance variables change)X = CalculationGrid(3, 4), X.select_points(5, 13) should result in nothing happening, because both points are not valid points on that grid.Submit as calculation_grid1.py
Add a method shared_edge to the CalculationGrid class:
self)None)TrueFalseIn the 4x3 grid, points 2 and 3 share an edge. In the 3x3 grid, they do not. In the 4x3 grid, points 0 and 4 share an edge. In the 3x3 grid, they do not.

Submit as calculation_grid2.py
Add a method both_corners to the CalculationGrid class:
self)TrueFalseSubmit as calculation_grid3.py
Add a method clear_points to the CalculationGrid class:
self)selectedA and selectedB to None
shared_edge and both_corners should still work (i.e., they should do nothing when called, and not result in errors)Submit as calculation_grid4.py