After working through the exercises in this notebook you should be familiar with Python variables, indexing, for loops, and if statements.
from ipythonblocks import BlockGrid, colors, show_color, show_color_triple
BlockGrid?
These blocks with In[ ]:
and Out[ ]:
next to them are "cells". Jupyter "echos" whatever is on the last line of a cell.
BlockGrid(4, 5)
Use an =
to assign something (on the right side of the =
) to the name you want it to have (on the left side of the =
).
grid = BlockGrid(5, 5, fill=colors.RosyBrown)
grid
grid.show()
is another way to get a grid displayed, it's especially useful if you can't echo the grid on the last line of a cell.
.show
is a "method" on the grid
instance. It's like a function attached to this particular grid. Note the parentheses used to "call" the method (and to call the BlockGrid
above when making grid
).
grid.show()
The show_color
and show_color_triple
functions are useful for previewing colors. The colors
object/dictionary has a bunch of useful preset colors.
show_color_triple(colors.Bisque)
colors.AliceBlue
Color(red=240, green=248, blue=255)
When pulling an individual block out of a grid use square brackets and specify a row and column:
grid[row_index, column_index
.
Python is zero-indexed.
block = grid[0, 0]
block
.rgb
is an "attribute" of blocks that has the "red, green, blue" values of the color of the block.
These these integers can have values from 0 - 255 and the combination of the three specifies the color of the block.
block.rgb
(188, 143, 143)
We can assign a new triplet of color integers to the .rgb
attribute to change the color of a block.
block.rgb = colors.AliceBlue
block
We can see further information about a block using the print
and type
functions.
print(block)
Block [0, 0] Color: (240, 248, 255)
type(block)
ipythonblocks.Block
Note that changing the color of the block variable above also changed the color of the block in the grid.
When we ran block = grid[0, 0]
above we created a new name (block
) that referenced the top-left cell
of the grid. We did not make a copy of that block.
grid
We could also change the color of the top-left block without first pulling it into a variable.
grid[0, 0].rgb = colors.Cyan
grid
Note also that assigning the grid
variable to a new variable (new_grid
here) there is still no copy. Modifying new_grid
modifies the original grid
as well because they are different names for the same thing.
new_grid = grid
new_grid[1, 1].rgb = colors.DarkCyan
grid
grid[2, 2] = colors.FireBrick
grid
The grid
object doesn't have a .rgb
attribute.
Python lets you create one by assigning something to it, but nothing happens to the appearance of the grid.
grid.rgb = colors.Gold
grid
Exercise 2: "Change the color of the block in the third row and fourth column."
grid[2, 3].rgb = colors.Black
grid
Exercise 3: "Change the color of the block in the lower-right corner of the grid."
grid[4, 4].rgb = colors.Teal
grid
Python also supports negative indexing, allowing you to index into the ends of containers without knowing
how big they are.
Index -1
is the last item, index -2
is the second-to-last item, etc.
grid[-1, -1].rgb = colors.Violet
grid
If you wanted to show two or more grids in the output of a notebook cell you could use the .show()
method.
grid.show()
new_grid.show()
Exercise 4: "Use negative indexing to change the color of the block in the second row and first column."
grid[-4, -5].rgb = colors.Wheat
grid
Python's for thing in container:
syntax lets you iterate over whatever is in container
.
But, note that what exactly this means varies depending on what container
is.
In the case of our grids, each iteration of the loop gives us one of the blocks from the grid.
As an example we could change the color of every block in the grid using a loop.
for block in grid:
block.rgb = colors.HotPink
grid
Exercise 5: "Use a for
loop to change the color of every block in your grid."
The .animate()
method will show you the state of grid as the loop runs.
(But you'll only see the final state of the grid after the loop if you're reading this later.)
for block in grid.animate():
block.rgb = colors.Honeydew
if
statements allow us to only execute code when a given condition is true.
The ==
comparator checks whether two things are equal.
Don't get it confused with the single =
used for assignment!
for block in grid.animate():
if block.row == 2:
block.rgb = colors.PapayaWhip
How would I figure out that blocks had a .row
attribute?
I could use the ?
help feature to read the docstring or use block.<tab>
to see available methods
and attributes.
But, note that block.<tab>
only works if block is an existing variable with something assigned to it
when you press tab. That's not the case when writing the loop above.
block = grid[0, 0]
block?
Exercise 6: "Use a for
loop and an if
statement to change the color of every block
in the fourth column of your grid."
Blocks also have a .col
attribute that has the column index of the block.
for block in grid.animate():
if block.col == 3:
block.rgb = colors.Magenta
elif
and else
allow us to write separate blocks of code that run under different conditions.
Only one of these branches will run for each block
in the grid.
else
applies to anything not matched by an if
or elif
above it.
for block in grid.animate():
if block.row == 0:
block.rgb = colors.Black
elif block.row == 2:
block.rgb = colors.White
else:
block.rgb = colors.Blue
Exercise 7: "Augment your code from Exercise 6 with an elif
and an else
to change the
color of all blocks in your grid. But make the second column red, the
third column green, and all the other blocks gold."
for block in grid.animate():
if block.col == 1:
block.rgb = colors.Red
elif block.col == 2:
block.rgb = colors.Green
else:
block.rgb = colors.Gold
Other Python comparators:
<
less than<=
less than or equal>
greater than>=
greater than or equal!=
not equaland
and or
allow us to construct more complicated if
expressions.
When using or
only one of the conditions needs to be true for the branch to be executed.
for block in grid.animate():
if block.col == 2 or block.col == 4:
block.rgb = colors.Khaki
Exercise 8: "Use an if
with an or
to change the color of the blocks in the first
and fifth rows to black."
for block in grid.animate():
if block.row == 0 or block.row == 4:
block.rgb = colors.Black
If we want to make checks on the color of the blocks here are some different ways to do that.
block = grid[1, 1]
block.rgb
(255, 0, 0)
block.rgb == (255, 0, 0)
True
block.rgb == colors.Red
True
block.red
255
block.blue
0
Exercise 9: "Use an if
with an and
condition to turn every block that is in the
fourth column AND black to blue."
When using and
both of the conditions must be true in order for the branch to be executed.
for block in grid.animate():
if block.col == 3 and block.rgb == colors.Black:
block.rgb = colors.Blue
You can use parentheses to specify an order of operations for if
conditions.
Absent parentheses or
and and
will be combined left to right.
for block in grid.animate():
if (block.col == 1 or block.col == 2) and block.rgb == colors.Black:
block.rgb = colors.Blue
range
¶Sometimes we don't need to loop over every block in a grid because we know ahead of time we're
only interested in some subset of them.
The range
function can help construct a sequence of integer values to use as indices while in the loop.
range
documentation
Note: the stop
argument to range is not included in the result!
for index in range(5):
print(index)
0 1 2 3 4
for index in range(5, 8):
print(index)
5 6 7
for index in range(5, 15, 3):
print(index)
5 8 11 14
Here we want to change the color of every block in the 2nd row, so we loop over a range
that encompasses
all the columns in the grid.
for column_index in range(grid.width):
grid[1, column_index].rgb = colors.Orange
grid
Exercise 10: "Make a new 20 by 20 block grid. Use a for
loop with range
to change the
color of every block in the third, 10th, and 18th rows."
Note the use of grid.flash()
to briefly show the grid at each iteration since we're no longer
using grid.animate()
. We also need to echo grid
on the last line (or use grid.show()
after the loop) in order to see the final result.
grid = BlockGrid(20, 20)
for column_index in range(grid.width):
grid[2, column_index].rgb = colors.YellowGreen
grid[9, column_index].rgb = colors.YellowGreen
grid[17, column_index].rgb = colors.YellowGreen
grid.flash()
grid
An alternate way of doing this is to have nested loops, the outer one for the columns we want to change and
the inner one for the rows we want to change.
Note that the [2, 9, 17]
is a list with the row indices.
(We can't use range
for the row indices because they aren't evenly spaced.)
for column_index in range(grid.width):
for row_index in [2, 9, 17]:
grid[row_index, column_index].rgb = colors.Indigo
grid.flash()
grid
Exercise 11: "Use nested for
loops with range
to change the color of the bottom-left
quadrant of your 20 by 20 grid."
Here we use some math instead of hard-coding the row and column indexes in our calls to range
.
The //
operator does floor division, returning an integer and discarding any fractional component of the result
(needed because range
doesn't work with floating point inputs and /
always returns a float).
for row_index in range(grid.height // 2, grid.height):
for column_index in range(0, grid.width // 2):
grid[row_index, column_index].rgb = colors.LavenderBlush
grid
The int
function will try to convert an input to an integer, it also truncates the input (does not round).
int(2.3)
2
Slicing is an abbreviated, inline way of specifying a subset of a Python container.
The syntax of a slice is start:stop:step
, but not all of those are required.
In this first example 5:15
means "from the 5th index up to but not including the 15th index".
grid[5:15, 5:15]
When the start or stop of a slice is not specified it means "from the beginning" or "until the end", respectively.
grid[:5, 15:]
A lone :
means "include everything along this dimension" (in this case all the columns).
grid[:5, :]
We can assign a color to a slice of a grid to change the color of all the blocks in that slice.
grid[:5, :] = colors.Crimson
grid
Exercise 12: "Try to get the same affects as in Exercises 10 & 11, but using slicing instead
of for
loops."
grid[2, :] = colors.Thistle
grid[9, :] = colors.Tomato
grid[17, :] = colors.Turquoise
grid[-10:, :10] = colors.Blue
grid
Slicing also supports specifying a step-size, very similar to range
.
Here 1::2
means "start at the first index, go through the end, and include every 2nd index".
grid[1::2, :] = colors.Teal
grid
Functions are one way to package code and re-use it again and again.
Note the def
keyword indicating we're writing a function, the name of the function
(here fahr_to_celsius
), the parentheses, and the list of arguments inside the paretheses.
The return
statement is used to pass something back to the code that calls the function.
A function can return multiple things by separating them with commas after the return
:
return 1, 2, 3, 4
.
def fahr_to_celsius(temp):
return ((temp - 32) * (5 / 9))
fahr_to_celsius(63)
17.22222222222222
fahr_to_celsius(90)
32.22222222222222
Exercise 13: "Write a function that takes a grid as input and returns a new grid with the colors inverted (e.g. white becomes black, black becomes white, yellow becomes ???)."
def invert_colors(input_grid):
"""
Returns a new grid the same size as the input grid but with colors "inverted".
Colors in our grids are are integers that can have values between 0 - 255 (inclusive).
To "invert" a color channel (red, green, or blue) we mirror it within
that 0 - 255 range: if it was 0 the output will be 255 and if it was 255 the output will be 0.
A value of 123 will invert to 132.
"""
new_grid = input_grid.copy()
for row_index in range(input_grid.height):
for col_index in range(input_grid.width):
new_grid[row_index, col_index].red = 255 - input_grid[row_index, col_index].red
new_grid[row_index, col_index].blue = 255 - input_grid[row_index, col_index].blue
new_grid[row_index, col_index].green = 255 - input_grid[row_index, col_index].green
return new_grid
invert_colors(grid)
Exercise 14: "Write a function that takes a grid and a color as input returns a new grid that is the input grid with an added border of the given color.
BONUS: Make the color input argument optional."
If the user doesn't supply a value for the border_color
input it will default to colors.Black
.
def add_border(input_grid, border_color=colors.Black):
"""
Add a two-block border to the input grid.
Specify the border color as a tuple of (red, green, blue) integers.
"""
# increase grid size by 4 to accomodate the two-block border on each side
new_width = input_grid.width + 4
new_height = input_grid.height + 4
new_grid = BlockGrid(new_width, new_height, fill=border_color)
for block in input_grid:
# The +2 here offsets the input grid within the new grid so it ends up with a border
new_grid[block.row + 2, block.col + 2].rgb = block.rgb
return new_grid
add_border(grid, border_color=colors.Cyan)
Exercise 15: Write a function that can follow a list of directions
('up'
, 'down'
, 'left'
, 'right'
) along a path, changing the color of
blocks as it goes.
An example function definition:
def path_color(grid, path, starting_point, color):
...
def path_color(input_grid, path, starting_point, color):
"""
Change the color of all the blocks along a path starting at starting_point.
Specify the path as a sequence of 'up', 'down', 'left', 'right' strings.
Specify the starting point as (row, column) tuple.
Specify the color a (red, green, blue) tuple.
"""
new_grid = input_grid.copy()
# pull the location into separate row/col indices we can update individually
current_row = starting_point[0]
current_col = starting_point[1]
# first, change the color of the block we're at
new_grid[current_row, current_col].rgb = color
# then move to the new location based on the path
for direction in path:
if direction == 'up':
# one row higher in the grid
current_row = current_row - 1
elif direction == 'down':
# one row lower
current_row = current_row + 1
elif direction == 'left':
# one column to the left
current_col = current_col - 1
elif direction == 'right':
# one column to the right
current_col = current_col + 1
# and update the new location's color
new_grid[current_row, current_col].rgb = color
return new_grid
path = (
'right', 'right', 'down', 'right', 'down', 'down', 'left', 'down', 'down', 'right', 'right', 'right', 'up'
)
path_color(grid, path, (3, 4), colors.HotPink)