= "bureaucrats"
s = s.upper()
s 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 modified
v: 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 modified
v: 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:
class
Grid
– this is a name we choose, similar to a function name
__init__
is executed whenever a new class is created.
self
, x
, and y
self
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 % x
n
has a y-position given by n // x
A 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 Grid
Grid
, 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 constructorA 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 y
self.x
references class variable y
Class 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 Fakse
(No shared non-zero corner)Modify the SelectableGrid
object from the notes:
Grid
select
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__