Back to Contents

Answers to Questions

Hints may be found after all the answers.

1. Starting Off

1

The expression 17 is a number and so is a value already – there is no work to do. The expression 1 + 2 * 3 + 4 will evaluate to the value 11, since multiplication has higher precedence than addition. The expression 400 > 200 evaluates to the boolean True since this is result of the comparison operator > on the operands 400 and 200. Similarly, 1 != 1 evaluates to False. The expression True or False evaluates to True since one of the operands is true. Similarly, True and False evaluates to False since one of the operands is false. The expression ’%’ is a string and is already a value.

2

The expression evaluates to 11. The programmer seems to be under the impression that spacing affects precedence. It does not, and so this use of space is misleading.

3

The % operator is of higher precedence than the + operator. So 1 + 2 % 3 and 1 + (2 % 3) are the same expression, evaluating to 1 + 2 which is 3, but (1 + 2) % 3 is the same as 3 % 3, which is 0.

4

The comparison operator < considers the words in dictionary order, so ’bacon’ < ’eggs’. The uppercase letters are all “smaller” than the lowercase characters, so for example ’Bacon’ < ’bacon’ evaluates to True. For booleans, False is considered “less than” True.

5

The first one is, of course entirely as expected:

Python
>>> 1 + 2
3

It turns out that the + operator we have been using on numbers to add them can be used on strings to concatenate them:

Python
>>> 'one' + 'two'
'onetwo'

However, it will not work to mix the two types:

Python
>>> 1 + 'two'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'str'

The * operator can also be used on a string and a number, to concatenate the string multiple times:

Python
>>> 3 * '1'
'111'
>>> '1' * 3
'111'
>>> print('1' * 3)
111

In the last line, we remember the difference between a string and printing a string. When it is (ab)used as a number, True has the value 1, whereas False has the value 0:

Python
>>> True + 1
2
>>> False + 1
1

Did you notice the f before the quotation mark in the last example? This is a format string, which we will discuss in chapter 6:

Python
>>> print(f'One and two is {1 + 2} and that is all.')
One and two is 3 and that is all.

The part between the curly braces {} has been evaluated and then printed.

2. Names and Functions

1

We include the return keyword to make sure the result is returned to us:

Python
>>> def times_ten(x):
...     return x * 10
...
>>> times_ten(50)
500

2

This function will have two arguments. We use the and operator, together with the inequality operator !=.

Python
>>> def both_non_zero(a, b):
...     return a != 0 and b != 0
... 
>>> both_non_zero(1, 2)
True
>>> both_non_zero(1, 0)
False

3

This is a simple function with three arguments. We remember to use return, of course:

Python
>>> def volume(w, h, d):
...    return w * h * d
...
>>> volume(10, 20, 30)
6000

We can now write our volume_ten_deep function:

Python
>>> def volume_ten_deep(w, h):
...     return volume(w, h, 10)
>>> volume_ten_deep(5, 6)
300

Notice that we need return here too: the return in volume will not suffice.

4

If a lower case character in the range ’a’’z’ is not a vowel, it must be a consonant. So we can reuse the is_vowel function we wrote earlier, and negate its result using not:

Python
>>> def is_consonant(s):
...     return not is_vowel(s)
... 
>>> is_consonant('r')
True
>>> is_consonant('e')
False

5

We could simply return 0 for a negative argument. The factorial of 0 is 1, so we can change that too, and say our new function finds the factorial of any non-negative number:

Python
>>> def factorial(x):
...     if x < 0:
...         return 0
...     elif x == 0:
...         return 1
...     else:
...         return x * factorial(x - 1)
...
>>> factorial(-1)
0

6

We can use a recursive function:

Python
>>> def sum_nums(n):
...     if n == 1:
...         return 1
...     else:
...         return n + sum_nums(n - 1)
... 
>>> sum_nums(10)
55

There is a direct mathematical formula too. We use the integer division operator //, which we have not yet seen:

Python
>>> def sum_nums(n):
...     return (n * (n + 1)) // 2
>>> sum_nums(10)
55

Can you see why?

7

A number to the power of 0 is 1. A number to the power of 1 is itself. Otherwise, the answer is the current n multiplied by nx − 1.

Python
>>> def power(x, n):
...     if n == 0:
...         return 1
...     else:
...         if n == 1:
...             return x
...         else:
...             return x * power(x, n - 1)
...
>>> power(2, 5)
32

Notice that we had to put one if and else inside another here. The indentation helps to show the structure. Remembering that Python allows us to compress this using the elif keyword:

Python
>>> def power(x, n):
...     if n == 0:
...         return 1
...     elif n == 1:
...         return x
...     else:
...         return x * power(x, n - 1)
...
>>> power(2, 5)
32

This is easier to read, partly because all the return keywords line up. In fact, we can remove the case for n = 1 since power(x, 1) will reduce to x * power(x, 0) which is just x.

8

We test each number less than the given number for divisibility using the % modulus operator we learned about in chapter 1, increasing the test divisor by one each time. If it is divisible, we print it.

Python
>>> def factors(n, x):
...  if x % n == 0: print(n)
...  if n < x: factors(n + 1, x)
>>> factors(1, 12)
1
2
3
4
6
12

We can clean the solution up by wrapping it in another function which supplies the starting point of 1:

Python
>>> def factors_simple(x):
...   factors(1, x)
>>> factors_simple(12)
1
2
3
4
6
12

3. Again and Again

1

We set the step to  − 1. We must be careful with the start and stop points. We set the stop point to 0 so that we stop at 1 (i.e. before we get to 0):

image

2

We change the calculation of column_width to take n * (n - 1) as the item with maximum width rather than n * n:

image

Here is the new result for a table of size ten:

Python
>>> times_table(10)
1  2  3  4  5  6  7  8  9  10 
2  4  6  8  10 12 14 16 18 20 
3  6  9  12 15 18 21 24 27 30 
4  8  12 16 20 24 28 32 36 40 
5  10 15 20 25 30 35 40 45 50 
6  12 18 24 30 36 42 48 54 60 
7  14 21 28 35 42 49 56 63 70 
8  16 24 32 40 48 56 64 72 80 
9  18 27 36 45 54 63 72 81 90 
10 20 30 40 50 60 70 80 90 100

It is still possible to have excess space in some columns with this method:

Python
>>> times_table(4)
1  2  3  4  
2  4  6  8  
3  6  9  12 
4  8  12 16 

Can you fix this?

3

We use a for loop to check each letter in the string, adding one to a local variable each time we see a space:

image

We use return to make sure the final count is the result of the function. We can use the += operator to shorten the common operation of adding to a variable:

image

This works for other operators too.

4

This is a classic problem. It sounds easy, and yet requires several changes. We calculate the length of the string with len, so we know how many letters there are left to go. Then, in the for loop itself, we deduct one from that count each time, and print the space only if the count indicates we are not on the last letter.

image

5

A very simple while loop is required:

image

6

We supply the prompt directly to the input function. We must add the newline \n because, unlike print, input does not move to the next line after printing the prompt.

image

We can now remove the entered variable, but we must use pass, otherwise the while statement would be ungrammatical in Python:

image

7

We need three variables: target to hold the secret number between 1 and 100, guess to hold the current guess, and tries to count the number of tries.

image

Inside the while loop, we add one to the number of tries, and keep going until the correct answer is guessed. Then we print the final message with the number of tries. Notice that we have use an if and an elif but no else. Can you simplify the conditional construct further?

8

We use one giant if construct (in the next chapter we shall discuss better ways to do this). The function print_morse_letter prints a single code for the given letter, followed by three spaces:

image

Now the main function, to print a whole string, uses print_morse_letter for each letter which is not a space. For spaces it prints an extra four spaces in the output to add to the three following the previous letter.

image

This implementation has two problems: (1) it prints an extra three spaces at the end of any message not ending with a space; and (2) a space at the beginning of a message will have only four spaces in the output not seven. Can you fix them?

4. Making Lists

1

The first function is simple: we just return the element at index 0. To return the last element we must use the len function to calculate the index. We remember to subtract one, since list indices start at zero.

image

In fact, we can also write l[-1] to retrieve the last element of a list in Python. What happens in each case if the list is empty?

2

We first create a fresh, empty list. Then we can iterate over the input list in order, inserting each element at index 0 in the new list. The effect is to produce a reversed list, which is then returned.

image

Alternatively, we can use a range with a negative step value, in conjunction with the append method. Again, we begin with a fresh, empty list.

image

3

We will use a for loop to look at each element, updating two variables to keep track of the smallest and largest numbers seen so far. The important thing to to properly initialise the minimum and maximum variables. We do so by setting them to be equal to the first element of the list. Can you see why?

image

This function has a minor inefficiency; it looks at the first element of the list twice. Can you fix that?

4

We need a step value of two, and start and stop values encompassing the whole list:

image Of course, such start and stop values are the default, so we may also write:

image

5

We follow the pattern of our second, shorter evens function above, and write simply:

image

6

We begin by making a fresh, empty list. Then, for each element in the original list, we add it to the new list, unless it is already there:

image

7

We use our setify function to make a new list of the unique items in the original list. Then we iterate over this new list, finding the number of each of its elements in the original list using the count method, and print it.

image

8

This is a simple exercise in the use of in and the boolean operator and:

image

9

We create a fresh, empty list and append the elements of the original list one by one:

image

Alternatively, we can use the slice operator with empty start and stop values:

image

10

We make a fresh list with our new copy_list function, remove the value using remove and then return the new list:

image

11

We set up our alphabet and use list slicing to write a function rotate which can rotate it by any number of places from 1 to 25, returning a new string:

image

Now our encoding and decoding functions are simple, taking the text to encode or decode, and the rotated cipher. They are, as you would expect, somewhat symmetrical:

image

We remember to treat spaces specially.

12

Now that we know about Python’s lists, we can dispense with the huge if construct of our previous Morse code solution, and work from two lists: the letters and their codes:

image

Now it is simple to modify our previous solution to look up codes in the list using the index method on lists:

image

13

First, we shall write a function which looks at a single guess and calculates how many are a) the correct number in the correct place and b) the correct number in the incorrect place. This is surprisingly delicate, since we must make sure not to double count anything – for example, if the code is 1441 and the guess is 4444 we should identify two 4s as being correct numbers in the correct places, but the other two 4s are not identified as being correct numbers in the wrong place, because we have already used up all the 4s in our code.

Our function will take two lists of four numbers each, the code and the guess. We copy them, since we will be changing values in the lists to mark them as used. Then we set up two counters, to keep track of how many numbers are correct and in the right place, or correct and in the wrong place.

image

The function returns True if the code is completely correct, and False otherwise. Now we can write the main function which asks the user for a guess repeatedly:

image

There is currently no handling of errors here - what happens if you type in too few or too many numbers, for instance? Can you fix the program to handle this?

5. More with Lists and Strings

1

We split to make a list, then the sort method to sort it in place.

image

2

Using the sorted function removes the need to introduce the intermediate name l as in the previous question:

image

This makes our function a little easier to read.

3

Here is the original from chapter 4:

image

The modification is very simple: we use the sorted function when creating our list of unique values:

image

4

If we write a function to remove any spaces at the front of a list of strings, we can then use it multiple times to deal with spaces at the beginning and end. Here is such a function:

image

For example, strip_leading_spaces([’ ’, ’ ’, ’y’, ’e’, ’s’, ’ ’]) will return [’y’, ’e’, ’s’, ’ ’]. Notice that the and operator does not try its right hand side if its left hand side is false. And so, if len(l) > 0 is True, the first element of l will not be tested for equality, and the function succeeds even when the list is empty (or consists only of spaces).

Now we can write the main function, which uses our stripper twice, to remove the spaces at the beginning and end of the list made from the original string. One final reversal brings it back to the correct order, and we join it back into a string.

image

5

We can (ab)use split and join to make a much simpler definition:

image

This will, however, also remove any excess multiple spaces in between words. Python provides a built-in method strip to remove just the parts at either end, leaving the rest untouched.

6

First, our clip function:

image

Now, we use map with our clip function, not forgetting to use list to get back an ordinary list before returning.

image

7

We have already seen how a slice with a step of -1 may be used to reverse a list. A palindrome is something which equals its own reverse, so it is easy to write out the definition:

image

We can use this is_palindromic function as a filter to return only such strings in a list as are palindromic:

image

Now, we can build only those numbers in a range whose strings are palindromic.

image

We must remember to convert them back to integers before returning. We can achieve this with map, of course. Now we can find all the palindromic numbers up to 500:

>>> palindromic_numbers_in(1, 500)
[1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 22, 33, 44, 55, 66,
77, 88, 99, 101, 111, 121, 131,141, 151, 161, 171,
181, 191, 202, 212, 222, 232, 242, 252, 262, 272,
282, 292,303, 313, 323, 333, 343, 353, 363, 373, 383,
393, 404, 414, 424, 434, 444, 454, 464, 474, 484, 494]

We can remove the instances of list since range, filter, and map are happy to accept iterators:

image

We have to chosen to return an actual list, not a generator, from the final palindromic_numbers_in function, however.

8

This is a simple list comprehension, just using clip on each item in the list:

image

9

In this example, we use both for and if in a list comprehension to process the list of strings.

image

It is just about readable to put the whole thing in one expression:

image

6. Prettier Printing

1

We must keep a counter to prevent the printing of an extra comma and space:

image

This is a lot more complicated than having print do the work for us, but it does allow us some customisation: for example if we wished to print without commas, or without spaces.

2

In this instance, format strings do not really help use remove any complexity. In fact, this solution is slightly longer than the one without format strings.

image

3

Without format strings, we must use str explicitly on each integer, as a prelude to calling the rjust method. We can use the print function with multiple arguments to print a whole line, though:

image

4

This is a simple substitution of zfill for rjust:

image

In fact, we can use rjust(5, ’0’) to achieve the same effect.

5

We use the with … as construct to safely open and close the file. Then it is a matter of carefully constructing the while condition and the variable it sets to get the right result. We then rearrange the parts of the name the user types in, and print it to file:

image

Can you see why we had to initialise the name variable to a non-empty string?

6

Using a format string allows us to remove the sep=’, ’ argument, but not a lot else:

image

7

For each sentence in the list, we find the position (if any) of the word. Remembering that failure to find the word results in a position of -1, we decide what to print to the screen:

image

8

This is a simple alteration to the previous answer:

image

7. Arranging Things

1

This can be achieved by tuple unpacking:

Python
>>> a = 1
>>> b = 2
>>> a, b = (b, a)
>>> a
2
>>> b
1

Note we do not need parentheses on the tuple when doing multiple assignment of values to names:

Python
>>> a = 1
>>> b = 2
>>> a, b = b, a
>>> a
2
>>> b
1

However, we cannot write this:

image

Why not?

2

We can use the items method on the dictionary, which allows iterating with a for loop using two variables, one for the key and one for the value. We return the result as a tuple.

image

For example:

Python
>>> unzip({1: 'one', 2: 'two'})
([1, 2], ['one', 'two'])

3

We initialise a fresh, empty dictionary. Then, looping over the index positions in the list of keys, we add each key and its value to the dictionary.

image

What happens if the lists ks and vs are of differing lengths?

4

Beginning with an empty dictionary, we loop over the items in each existing dictionary, adding the key and its associated value to the union dictionary.

image

The preference for values from dictionary a is achieved by processing it second. Duplicate entries from dictionary b are thus overwritten.

5

The list is being modified by deletion during the for loop, and so the indices change. Here is a possible working version, which repeatedly uses the remove method which, we remember, removes the first instance of a given element in a list:

image

6

We need to assign values to two names here, so we use the items method. The rest is then simple:

image

The output is not always the same length as the input, because a value may appear multiple times in the input, and so be used multiple times as a key in the output:

Python
>>> reverse_dict({1: 2, 2: 1, 3: 1})
{2: 1, 1: 3}

7

We remember that an empty set is created by set(). We loop over the input words, using set again to build a set of all the letters in each word, and the | operator to add them to our master set, which we then return:

image

For example:

Python
>>> letter_set(['one', 'two', 'three'])
{'w', 'n', 't', 'h', 'o', 'r', 'e'}

To do the inverse, we shall need a set of all the letters. Then we can use the set difference operator.

image

For example:

Python
>>> letters_not_used(['one', 'two', 'three'])
{'m', 'd', 'f', 'q', 'l', 'y', 's', 'k', 'g', 'c',
'v', 'j', 'p', 'a', 'u', 'z', 'x', 'b', 'i'}

8

We can represent sets using dictionaries with the values ignored, for example all set to zero. Here is a function to build such a ‘set’ from a list:

image

Now we can implement the operations. First, for the ‘or’ operation, we add entries to the new dictionary from both input lists:

image

For ‘and’, we must check that the item is in both sets:

image

Set difference is very similar:

image

Finally, exclusive or can be achieved by using our existing functions:

image

9

We use two for portions to iterate over both input sets. Only when x == y do we have a match.

image

This code checks every possible combination of elements of a and b and so is not very efficient.

10

If the type of the input value t is an integer, we return it. Otherwise, we loop over all the items in t, adding up their sums by recursive application of the sum_all function itself.

image

The result works on any tuple containing only number and on numbers themselves:

Python
>>> sum_all((1, 2, 3))
6
>>> sum_all((1, (1, 2), 3))
7
>>> sum_all(10)
10

8. When Things Go Wrong

1

We handle the ValueError resulting from int being used on a string which cannot reasonably be converted to an integer, and ignore the error by using pass:

image

Since the exception is raised by int, the total variable will not be updated in the case of a bad string. So we need not worry about the += operation receiving a bad input.

2

We write two little functions. First, safe_int, which handles the ValueError exception raised by int and returns None instead. Second, the function not_none which returns True if a value is anything other than None. Then we can apply map and filter to build a list of results from safe_int and filter out the None values.

image

3

We handle the ZeroDivisionError exception, returning 0.

image

4

We begin with a fresh dictionary. Then, we iterate over the keys and values of dictionary a. We try to insert the corresponding value from b into the new dictionary. If it fails, we handle KeyError and simply skip that key.

image

5

It is easy to add all the items from our first dictionary to the new one – the keys are already unique. When we add items from the second, we check to see if the key exists already. If it does, we raise KeyError.

image

6

We check to see if the item is already in the set. If it is, we raise KeyError. If not, we add it as usual.

image

9. More with Files

1

The with … as construct allows us to combine the two statements in the original into a single block:

image

2

We use with … as again, using the optional file argument of the print function to write each key and value:

image

3

This is a good example of the complications of reading from a file, expecting entries in a certain format, and finding data not fitting such a format. We begin with an empty dictionary, and then enter a while True loop. We then try to read keys and values, returning if we have reached the end of the file (or if the line is empty).

image

The ValueError exception which may be raised is caught and a message is printed.

4

We open the two input files, and the output file in ‘append’ mode. Then it is as simple as copying the lines across, being sure not to introduce extra newlines.

image

5

We use read to get the whole contents of the file at once, split it into ‘words’, then convert them to integers with map and sum them:

image

6

This is similar to our append_files function from question 4:

image

7

We introduce a dictionary to store the character histogram, then create or increment an entry in the dictionary for each character encountered.

image

8

For the word histogram, we introduce a function clean_split which splits a line into words, then processes each word to remove punctuation, and convert to lowercase.

image

9

We can reuse clean_split here to get the words in each line. Then, we can use enumerate to iterate over the indices and lists of words for each line. We check for the presence of the search term, and print the line and its number if required.

image

10

We read the lines all at once with readlines. Then, by careful use of slices, we print five at a time, waiting for the user to press Enter.

image

How might this be rewritten to work well on huge files? In that case, reading all the lines at once would be inefficient.

10. The Other Numbers

1

We calculate the ceiling and floor, and return the closer one, being careful to make sure that a point equally far from the ceiling and floor is rounded up.

image

2

The function returns another point, and is simple arithmetic.

image

3

The whole part is calculated using the floor function. We return a tuple, the first number being the whole part, the second being the original number minus the whole part. In the case of a negative number, we must be careful – floor always rounds downward, not toward zero!

image

Notice that we are using the unary operator - to make the number positive.

4

We need to determine at which column the asterisk will be printed. It is important to make sure that the range 0…1 is split into fifty equal sized parts, which requires some careful thought. Then, we just print enough spaces to pad the line, add the asterisk.

image

5

Our function takes another function as one of its argument. We use a variable to hold the current value, starting at the beginning of the range, and then loop until we are outside the range.

image

No allowance has been made here for bad arguments (for example, b smaller than a). Can you extend our program to move the zero-point to the middle of the screen, so that the sine function can be graphed even when its result is less than zero?

11. The Standard Library

1

Here is the documentation for the factorial function from the math module.

image

We can try it out:

Python
>>> import math
>>> math.factorial(5)
120
>>> math.factorial(5.0)
120
>>> math.factorial(-4)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: factorial() not defined for negative values

How does our function differ? Here we have picked the improved factorial function from the questions to chapter 2:

Python
>>> def factorial(x):
...     if x < 0:
...         return 0
...     elif x == 0:
...         return 1
...     else:
...         return x * factorial(x - 1)
>>> factorial(5)
120
>>> factorial(5.0)
120.0
>>> factorial(-4)
0

We return a floating-point number for a floating-point input, unlike math.factorial, and we return zero for a negative input, where math.factorial raises a ValueError exception.

2

We assume the string does represent an integer, then check each potential digit. If it is not in the string string.digits, we unset the is_integer variable. We then return the variable as the result of the function.

image

There is one small problem: string_is_integer with the empty string will return True. Can you fix this?

3

This is a simple modification: we replace the use of random.randint with one of getpass.getpass, passing the prompt as an argument.

image

4

This is simple. We return them as a tuple.

image

What happens when there is no modal value in a list?

5

We use two functions: time.sleep, which does nothing for a given number of seconds, allowing us to give the user a count-down; and time.time which returns a floating-point value representing the number of seconds since an arbitrary point in the past. By measuring the time twice, and subtracting, we get the elapsed time.

image

What happens if the user presses Enter too soon? Can you fix this?

12. Building Bigger Programs

1

The guessing_game function is unaltered. We need simply to check that there are enough arguments in sys.argv. If there are, we pass the string representing the maximum number to guessing_game:

image

If not, we use the default value of ’100’. We could, in fact, pass the number 100 instead of the string ’100’, since the int function does not care if it is passed something which is already an integer. This, however, would make the program as a whole more difficult to read. Better to keep our types consistent.

2

We first write the file draw.py with our existing plotter:

image

Now the main plot.py program can use import to access the plot function from the draw module, passing the fabricated function f built from the command line argument (one function can sit inside another):

image

And so we may write:

image

What errors might occur? The wrong number of arguments is handled in our program, but what if float fails?

3

We write three little functions to list, add, and remove notes:

image

The main part of the program, then, must decode the command line to decide which operation to do, and what parameters it needs. If the command line too short or malformed, we print a message.

image

Project 1: Pretty Pictures

1

We remember square takes the length of the side of the square as an argument:

image

Now we can write many_squares which takes how many square to use for the star, and the length of the sides, and calls square repeatedly.

image

2

The poly function is unaltered. We define a new function many_poly which takes the number of sides, the number of polygons to draw, and the length of the side of each polygon, and calls poly repeatedly, turning between each one.

image

Here is the result of many_poly(7, 16, 100):

image

3

The number of segments used to approximate a circle ought to be related to its circumference not its radius.

image

The smoothness may be fine-tuned by writing int(circumference * 1.5), int(circumference * 0.5) etc.

4

We write a function grid which takes four arguments: the first two to represent the starting point (sx, sy) and the latter two to give the number of circles in the x and y directions:

image

Here is the result of grid(-100, -100, 5, 4):

image

5

There are three dimensions to this data: red, green, and blue. And so we cannot display it directly on a 2D screen – we must flatten it in some way. We have chosen to slice the cube of data into slices based on the red value, and display the slices side by side.

First, we will need a function to draw a filled square of a given size at a given position.

image

Now we write a function red_gamut to draw a slice of the cube at a given position for a given red value:

image

Now we can show them side by side.

image

Here is the result:

image

6

Like any other polygon, we simply use begin_fill and end_fill:

image

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 f

The 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:

image

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.

image

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.

image

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.

image

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.

image

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.

image

The function assumes that there is always at least one blank space to find. Now the random_game function is straightforward:

image

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.

image

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.

image

There are many ways we might choose to write the main play function. Here is one:

image

6

The empty corner and empty side tactics are simple (the order we search for a blank space does not matter):

image

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.

image

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.

image

7

The boolean human_goes_first is true if the human player moves first.

image

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.

image

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.

image

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:

image

Here is the complete computer_move function for all our tactics, including printing each one out if it is applied, for debugging purposes:

image

9

O wins 77904 times, calculated by a similar function to the one we used to find how many times X wins:

image

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:

image

To calculate the total number of games, we can look for all boards which are full or won. This comes to 255168.

image

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.

image

Now, we can write simple little functions to pass to sum_game_tree:

image

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.

image

Now we must write a function to split a string into individual codes, recognising seven spaces as one space in the output:

image

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:

image

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:

image

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:

image

For contrast, we use simple multiplication. This assumes inputs of 0 upwards.

image

Now we can define functions to perform our operation on a whole image, using the process_pixels function we wrote earlier:

image

Now we can use these functions with some test values for brightness and contrast, and save them.

image

Here are the results. They are, from right to left: bright.png, dim.png, low_contrast.png, and high_contrast.png:

image image image image

2

To flip horizontally, we range over the left hand half of the image, swapping pixels with the equivalents on the right hand side.

image

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:

image

Rotation by 180 degrees is a simple combination of the two (try it with a piece of paper you have marked the corners of):

image

3

The changes are simple:

image

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.

image image

4

We need a border of width 1 for each blur operation, to avoid losing content over the edge:

image

5

This is simple enough – and you could extend it to add a border too.

image

Here is frame 100:

image