Untitled article
SourceNow we write a function red_gamut to draw a slice of the cube at a given position for a given red value:
Now we can show them side by side.
Here is the result:
6
Like any other polygon, we simply use begin_fill and end_fill:
Project 1A
Here is one solution – there are, of course, many others. It has two interesting features. The first is a function which returns a function which evaluates the given formula using eval. This function may be passed to the graph plotter, and evaluated many times for increasing values of x:
def farg(arg):
def f(x): return eval(arg)
return fThe second is simply a method for cycling through a fixed number of colours in the event that the user wants to plot more graphs than we anticipate. Suppose we have a list of colours of length four:
colors = ["black", "red", "green", "blue"]Then we can cycle through them with the % operator:
t.pencolor(colors[n % 4])Here is the full program:
import sys
import turtle
import math
t = turtle.Turtle()
if len(sys.argv) < 2:
print('No formula supplied')
sys.exit(0)
def plot(f):
t.penup()
t.goto(-300, f(-300))
t.pendown()
for x in range(-300, 300, 1):
t.goto(x, f(x))
def farg(arg):
def f(x): return eval(arg)
return f
def key(n, formula_text, color):
t.color(color)
t.penup()
t.goto(-300, -200 - 20 * n)
t.pendown()
t.write(formula_text, font = ("Arial", 16, "normal"))
def line(x0, y0, x1, y1):
t.penup()
t.goto(x0, y0)
t.pendown()
t.goto(x1, y1)
def axes():
t.color("black")
line(-300, 0, 300, 0)
line(0, -300, 0, 300)
for x in range(-300, 301, 50):
if x != 0:
t.penup()
t.goto(x, -20)
t.pendown()
t.write(str(x), font = ("Arial", 12, "normal"))
line(x, -5, x, 5)
for y in range(-300, 301, 50):
if y != 0:
t.penup()
t.goto(-20, y)
t.pendown()
t.write(str(y), font = ("Arial", 12, "normal"))
line(-5, y, 5, y)
colors = ["black", "red", "green", "blue"]
t.speed(0)
axes()
for n, arg in enumerate(sys.argv[1:]):
t.pencolor(colors[n % 4])
plot(farg(arg))
key(n, arg, colors[n % 4])
turtle.mainloop()Project 1B
A clock face is a good example of a structure which is easier to produce with turtle-like commands than by calculating coordinates and using goto.
The main loop checks the time, clears the screen, and then draws the clock face. Then we use turtle.Screen().update to update the screen, and sleep for one second:
while True:
tm = time.localtime()
t.home()
t.clear()
clockface(tm.tm_hour, tm.tm_min, tm.tm_sec)
turtle.Screen().update()
time.sleep(1)Here is the full program:
import turtle
import time
def hand(length, thickness, angle):
t.penup()
t.home()
t.setheading(90)
t.pensize(thickness)
t.pendown()
t.rt(angle)
t.fd(length)
def tickmarks():
t.pensize(1)
for a in range (0, 60):
t.penup()
t.home()
t.setheading(90)
t.rt(360 / 60 * a)
t.fd(295)
t.pendown()
t.fd(5)
def clockface(h, m, s):
t.penup()
t.goto(0, -300)
t.pensize(1)
t.pendown()
t.circle(300)
tickmarks()
hand(200, 3, 360 / 12 * (h % 12))
hand(280, 3, 360 / 60 * m)
hand(295, 1, 360 / 60 * s)
t = turtle.Turtle()
t.hideturtle()
turtle.Screen().tracer(0, 0)
while True:
tm = time.localtime()
t.home()
t.clear()
clockface(tm.tm_hour, tm.tm_min, tm.tm_sec)
turtle.Screen().update()
time.sleep(1)Project 2: Counting Calories
1
We use the function os.path.join to combine the person’s name and the name of the file where we expect to find the weights listed, and load the table with table_of_file. We can then iterate over the resultant table with the items method:
Printing the dates and weights to the screen is then simple.
2
We use the suggested os.listdir function to get the list of filenames. We are not told anything about the order, so we sort it with sorted – owing to the format for dates which we have chosen, they sort correctly.
We must exclude the weight.txt file, of course.
3
We must take account of the possibility that no weight was recorded for a given date. In this case, the table lookup will yield None.
4
We make the new directory, then open a file in it. The file is created, even though we do not write anything to it.
6
Here is the full csvcals.py program:
import sys
import os
import datetime
import csv
def table_of_file(filename):
with open(filename) as c:
r = csv.reader(c)
next(r)
table = {}
for row in r:
table[row[0]] = row[1:]
return table
def list_eaten(name, date):
for k, vs in table_of_file(os.path.join(name, date) + '.csv').items():
print(f'{k} {vs[0]}')
def list_weights(name):
for k, vs in table_of_file(os.path.join(name, 'weight.csv')).items():
print(f'{k} {vs[0]}')
def list_dates(name):
for filename in sorted(os.listdir(name)):
if filename != 'weight.csv': print(filename[:-4])
def list_foods():
for k, vs in table_of_file('calories.csv').items():
print(k, end=' ')
for v in vs: print(v, end=' ')
print('')
def lookup_calories(food):
table = table_of_file('calories.csv')
vs = table[food]
if vs is None:
print(f'Food {food} not found')
else:
if len(vs) > 1:
weight = vs[0]
calories = vs[1]
print(f'There are {calories} calories in {weight}g of {food}')
else:
print(f'Malformed calorie entry for {food} in calories file')
def lookup_weight(name, date):
table = table_of_file(os.path.join(name, 'weight.csv'))
vs = table[date]
if vs is None:
print(f'No weight found for {date}')
elif len(vs) > 0:
print(f'Weight at {date} was {vs[0]}')
def total_date(name, date):
calories = table_of_file('calories.csv')
table = table_of_file(os.path.join(name, date) + '.csv')
total = 0
for k, vs in table.items():
weight_and_calories = calories[k]
reference_weight = int(weight_and_calories[0])
reference_calories = int(weight_and_calories[1])
calories_per_gram = reference_calories / reference_weight
total += int(vs[0]) * calories_per_gram
print(f'Total calories for {date}: {int(total)}')
def new_user(name):
os.mkdir(name)
with open(os.path.join(name, 'weight.csv'), 'w') as f:
print('Date,Weight', file=f)
def date_today():
d = datetime.datetime.now()
return f'{d.day:02}-{d.month:02}-{d.year}'
def eaten(name, food, grams):
filename = os.path.join(name, date_today()) + '.csv'
is_new = not os.path.exists(filename)
with open(filename, 'a') as f:
if is_new: print('Food,Weight', file=f)
print(f'"{food}",{grams}', file=f)
def weighed(name, weight):
filename = os.path.join(name, 'weight.csv')
is_new = not os.path.exists(filename)
with open(filename, 'a') as f:
if is_new: print('Date,Weight', file=f)
print(f'{date_today()},{weight}', file=f)
arg = sys.argv
if len(arg) > 1:
cmd = arg[1]
if cmd == 'list':
if len(arg) > 3 and arg[2] == 'eaten':
list_eaten(arg[3], arg[4])
else:
if arg[2] == 'weights' and len(arg) > 3:
list_weights(arg[3])
elif arg[2] == 'dates' and len(arg) > 3:
list_dates(arg[3])
elif arg[2] == 'foods':
list_foods()
elif cmd == 'lookup':
if len(arg) > 2:
if arg[2] == 'calories':
lookup_calories(arg[3])
elif arg[2] == 'weight' and len(arg) > 3:
lookup_weight(arg[3], arg[4])
elif cmd == 'total':
if len(arg) > 3:
total_date(arg[2], arg[3])
elif cmd == 'newuser':
if len(arg) > 2:
new_user(arg[2])
elif cmd == 'eaten':
if len(arg) > 4:
eaten(arg[2], arg[3], arg[4])
elif cmd == 'weighed':
if len(arg) > 3:
weighed(arg[2], arg[3])
else:
print('Command not understood')7
There are only two functions to change: those that write non-blank CSV files. We use csv.writer to create a CSV writer from the file, then the writerow method to write both column headers and data.
Project 3: Noughts and Crosses
1
A 1x1 game is always won by the first player to play on their first turn. A 2x2 game is always won by the first player to play on their second turn. In some sense, of course, 3x3 is not interesting either because, as every child finds out soon enough, a draw can always be forced. What happens with a 4x4 game?
2
The most important rules are winning when one can, and blocking the other player if they are about to win. When not in either of those situations, you might think about which spaces are better to hold, for example the centre square.
3
To make a random play, we can choose a number between zero and eight. If the space is blank, we play there. If not, we cycle around the positions until we find a blank one.
The function assumes that there is always at least one blank space to find. Now the random_game function is straightforward:
4
We ask for input from the user, in the form of a string. First, we check that it represents a digit, to avoid an error when using int. Then we check it is in range. Finally, we check the space is really blank. Only then can we make the move.
This function can also be written without recursion, with the use of a while loop.
5
Extending the human_move function is simple: we add an extra argument.
There are many ways we might choose to write the main play function. Here is one:
6
The empty corner and empty side tactics are simple (the order we search for a blank space does not matter):
This tactic requires us to check that the opposite corner has been taken by the opposing side, so a single call to try_to_take cannot suffice.
We can update the computer_move function, adding these three new tactics and removing our earlier tactic_first_blank, since the combination of the centre, empty corner and empty side tactics render it unused.
7
The boolean human_goes_first is true if the human player moves first.
8
The fork tactic requires us to look at each pair of intersecting lines, trying to find two such lines each of which have one of our pieces and two blank spaces. If we find such a pair, and if the intersecting space is blank, we play it. Otherwise, the tactic fails. So we shall need a list of the pairs of intersecting lines in a board, together with the space at which they intersect:
intersecting_lines =
[(h1, v1, 0), (h1, v2, 1), (h1, v3, 2),
(h2, v1, 3), (h2, v2, 4), (h2, v3, 5),
(h3, v1, 6), (h3, v2, 7), (h3, v3, 8),
(d1, h1, 0), (d1, h2, 4), (d1, h3, 8),
(d1, v1, 0), (d1, v2, 4), (d1, v3, 8),
(d2, h1, 2), (d2, h2, 4), (d2, h3, 6),
(d2, v1, 2), (d2, v2, 4), (d2, v3, 6),
(d1, d2, 4)]Now for the fork tactic itself, we go through the pairs of intersecting lines. For each one, we look up those positions on the board. Then we can check the count of pieces in each, and check that the intersecting space is blank. If so, we make the play. If not, we return False.
The block fork tactic is somewhat more complicated. Part of the condition (two intersecting lines with one opponent’s piece and two blanks) is similar to the fork tactic, but we do not necessarily move to the intersection space, even if it is blank. First, we check to find a place to move which makes two of our pieces in a row. If so, we take it instead, forcing our opponent to block instead of fork.
The function find_two_in_a_row, given a board and a position, checks to see if the position, if taken, would make us two in a row. If so, we take it.
Now the main function checks the initial conditions, calls two_in_a_row as required and, should it fail each time, deals with the case of the intersecting space:
Here is the complete computer_move function for all our tactics, including printing each one out if it is applied, for debugging purposes:
9
O wins 77904 times, calculated by a similar function to the one we used to find how many times X wins:
The number of drawn games is 46080 can be calculated similarly, counting one for each board in the tree which is full but not won by any player:
To calculate the total number of games, we can look for all boards which are full or won. This comes to 255168.
Another way to find all boards which are full or won is to look for boards with no sub-trees.
Of course, we need only find two of the three outcomes of a game – we can deduce the third by subtraction from the total number of games.
10
We write a function traverses the tree, counting one for each time the function passed to it returns True.
Now, we can write simple little functions to pass to sum_game_tree:
11
We can write the tree out using nested tuples, each consisting of three elements: the current node, the left branch and the right branch. We use ’?’ for nodes which do not correspond to a valid letter or number:
tree = ('?',
('E',
('I',
('S',
('H', '5', '4'),
('V', '?', '3')),
('U',
'F',
('?', '?', '2'))),
('A',
('R', 'L', '?'),
('W', 'P',
('J', '?', '1')))),
('T',
('N',
('D',
('B', '6', '?'), 'X'),
('K', 'C', 'Y')),
('M',
('G',
('Z', '7', '?'), 'Q'),
('O',
('?', '8', '?'), ('?', '9', '0')))))Now we need a function to look through a given code, and traverse the tree, going left for each dot and right for each dash. When we have finished, we check to see if we have a string or a tuple and extract the string if we need to.
Now we must write a function to split a string into individual codes, recognising seven spaces as one space in the output:
Now the main function is simple: we use split_string to get a list of codes, including the spaces we have found, and decode each non-Space code and print it:
Project 4: Photo Finish
1
We will design appropriate functions for brightness and contrast, and then test them on our image.
For some combinations of brightness and contrast factors and pixel values, these functions will return values less than 0 or more than 255. We begin, then, with a function clamp to make sure that does not happen, and the resulting pixel value is in range:
There is no right or wrong formula for brightness or contrast. Here, we have chosen to take brightness values expected to be generally from -2 to 2, -2 meaning very dim, 2 meaning very bright:
For contrast, we use simple multiplication. This assumes inputs of 0 upwards.
Now we can define functions to perform our operation on a whole image, using the process_pixels function we wrote earlier:
Now we can use these functions with some test values for brightness and contrast, and save them.
Here are the results. They are, from right to left: bright.png, dim.png, low_contrast.png, and high_contrast.png:
2
To flip horizontally, we range over the left hand half of the image, swapping pixels with the equivalents on the right hand side.
If the image has an odd width, the middle column is not touched. This is a consequence of the rounding-down behaviour of the // operator. A similar function can be written for the vertical flip:
Rotation by 180 degrees is a simple combination of the two (try it with a piece of paper you have marked the corners of):
3
The changes are simple:
We can test by blurring three times, just like we did with our original blur function. The new (right) and old (left) results are similar but not the same – the in-place blur is blurrier.
4
We need a border of width 1 for each blur operation, to avoid losing content over the edge:
5
This is simple enough – and you could extend it to add a border too.
Here is frame 100:
Hints for Questions
1
Try to work these out on paper, and then check by typing them in. Can you show possible steps of evaluation for each expression?
2
Type it in. What does Python print? Consider the precedence of + and *.
2. Names and Functions
2
What does the function take as arguments? You can use the != operator and the and keyword here.
3
The function will have three arguments i.e. def volume(w, h, d): …
4
Can you define this in terms of the is_vowel function we have already written?
5
When does it not terminate? Can you add a check to see when it might happen, and return 0 instead? What is the factorial of 0 anyway?
6
What is the sum of all the integers from 1…1? Perhaps this is a good start.
7
What happens when you raise a number to the power 0? What about the power 1? What about a higher power?
8
You can use an additional argument to keep track of which number the function is going to try to divide by.
3. Again and Again
1
Make sure to consider how the start and stop arguments are defined at the beginning of this chapter.
3
You will need a local variable to store the count.
6
The input function with an argument, and using the \n newline sequence might look like this:
entered =
input('Please enter the password\n')To remove the entered variable, we can do the input inside the while loop’s test itself.
7
You might need three variables: the chosen secret number, the current guess, and the number of tries so far. Remember the int function can convert a string to an integer.
8
This is just a big if construct. Make sure not to output both a letter space and a word space at the end of a word – just a word space.
4. Making Lists
2
Try making a fresh, empty list, and then putting items from the original list into it one by one. Maybe you could use the insert method to put them in a particular place.
3
What initial value can we use for keeping track of both the maximum number seen and the minimum number seen?
6
Start from a fresh, empty list. Then, looking at each element of the original list, decide whether it should go into the new list or not.
7
You can use the setify function you have already written to find the unique items, and then the count method to find out how many of each appear in the input list.
11
The rotation may be achieved by slicing.
13
The problem may be split into two. First identify “correct numbers in the correct place” then “correct numbers in incorrect place”. In the latter stage, be careful not to use any position in the code identified in the first stage, nor to use a position twice.
5. More with Lists and Strings
1
We already know how to make the list of words with split.
4
Just a function to remove spaces from the beginning of a list is required; the rest can be done with list reversal.
9
Remember that a list comprehension can have an if part as well as a for part.
6. Prettier Printing
1
Remember not to add a comma or space after the final item. We did this once before, in chapter 3 question 4.
3
Recall that print can take multiple values.
5
How will the user signal that they have no more names to type in?
7. Arranging Things
1
Remember that we can assign to a tuple using tuple unpacking: a, b = ...
2
The items method, described in the chapter, can be used to iterate on two variables at once with for k, v = ...
5
Consider the indices when deletion happens.
6
The items method, described in the chapter can be used to iterate on two variables at once with for k, v = ...
7
Remember that set can build a set of the letters in a string.
8. When Things Go Wrong
2
The technique is to use map to build a list (possibly containing None values), then filter it to remove them.
9. More with Files
3
What happens if int cannot proceed because the file is malformed? How will you know when all the entries have been read?
5
The split method works, of course, equally well on numbers as on words.
7
A dictionary is suitable for storing a histogram. When do we need to add a new entry? When do we need to increment an existing entry?
10. The Other Numbers
1
Consider the two functions math.ceil and math.floor.
3
Consider the function math.floor. What should happen in the case of a negative number?
4
Calculate the column number for the asterisk carefully. How can it be printed in the correct column?
5
You will need to call the star function with an appropriate argument at points between the beginning and end of the range, as determined by the step.
11. The Standard Library
2
The string string.digits is ’0123456789’.
5
The use of time.sleep is to provide a count-down for the user.
12. Building Bigger Programs
1
The two cases (a maximum is provided, and is not provided) may be distinguished by testing the length of the sys.argv list.
2
The plot function requires a function to be passed to it. We can build such a function from the argument provided on the command line, using the eval function.
Project 1: Pretty Pictures
3
The number of segments used to approximate a circle ought to be related to its circumference not its radius.
5
There are three dimensions to the gamut: red, green and blue. Since we cannot display these on a 2D screen, you must find a way to ‘flatten‘ the space out.
Project 1A
We need to build a function which can be called repeatedly to evaluate an expression from the command line, in terms of x. Can you write a function to return such a function?
Project 1B
Recall that turtle.Screen().tracer(0, 0) turns off animation, and that you will need turtle.Screen().update() to show the clock face when you have finished drawing it, and that time.sleep may be used to wait for the next time we need to draw a clock.
Project 2: Counting Calories
1
Remember that the items method may be used to iterate over the keys and values of the table resulting from table_of_file.
2
The dates will sort as if they were in alphabetical order – the digits too are ordered.
1
If a table lookup fails, None is returned.
Project 3: Noughts and Crosses
3
The random_move function will need a mechanism to pick a random blank space. You might repeatedly pick spaces until one is found to be blank, though this may be inefficient when few spaces remain.
Another way would be to pick a random place to start, and then cycle through the positions in turn until a blank one is found.
6
Our try_to_take function is useful here.
8
Draw out diagrams of the situations described by the rules. You will need a list of intersecting lines and the points at which they intersect.
9
To calculate the total number of boards, remember only to count ones which are full or won.
10
The function must traverse the whole tree, counting once for each board for which the function passed to it returns True.
11
The tree may be represented by nested tuples of three items each, representing the data, the left branch and the right branch.
Project 4: Photo Finish
1
There is no right or wrong answer for the formulae for brightness and contrast. Design them to produce a sensible result for a sensible input.
2
Rotation may be achieved by a combination of flips.
4
Consider how the blurring operation spreads the colour of a pixel around: how do we make sure we lose none?