Solutions to End-of-Part Exercises - Appendixes - Learning Python (2013)

Learning Python (2013)

Part IX. Appendixes

Appendix D. Solutions to End-of-Part Exercises

Part I, Getting Started

See Test Your Knowledge: Part I Exercises in Chapter 3 for the exercises.

1. Interaction. Assuming Python is configured properly, the interaction should look something like the following (you can run this any way you like (in IDLE, from a shell prompt, and so on):

2. % python

3. ...copyright information lines...

4. >>> "Hello World!"

5. 'Hello World!'

6. >>> # Use Ctrl-D or Ctrl-Z to exit, or close window

7. Programs. Your code (i.e., module) file module1.py and the operating system shell interactions should look like this:

8. print('Hello module world!')

9.

10.% python module1.py

11.Hello module world!

Again, feel free to run this other ways—by clicking the file’s icon, by using IDLE’s Run→Run Module menu option, and so on.

12.Modules. The following interaction listing illustrates running a module file by importing it:

13.% python

14.>>> import module1

15.Hello module world!

16.>>>

Remember that you will need to reload the module to run it again without stopping and restarting the interpreter. The question about moving the file to a different directory and importing it again is a trick question: if Python generates a module1.pyc file in the original directory, it uses that when you import the module, even if the source code (.py) file has been moved to a directory not in Python’s search path. The .pyc file is written automatically if Python has access to the source file’s directory; it contains the compiled byte code version of a module. See Chapter 3for more on modules.

17.Scripts. Assuming your platform supports the #! trick, your solution will look like the following (although your #! line may need to list another path on your machine). Note that these lines are significant under the Windows launcher shipped and installed with Python 3.3, where they are parsed to select a version of Python to run the script, along with a default setting; see Appendix B for details and examples.

18.#!/usr/local/bin/python (or #!/usr/bin/env python)

19.print('Hello module world!')

20.% chmod +x module1.py

21.

22.% module1.py

23.Hello module world!

24.Errors. The following interaction (run in Python 3.X) demonstrates the sorts of error messages you’ll get when you complete this exercise. Really, you’re triggering Python exceptions; the default exception-handling behavior terminates the running Python program and prints an error message and stack trace on the screen. The stack trace shows where you were in a program when the exception occurred (if function calls are active when the error happens, the “Traceback” section displays all active call levels). In Chapter 10 and Part VII, you will learn that you can catch exceptions using try statements and process them arbitrarily; you’ll also see there that Python includes a full-blown source code debugger for special error-detection requirements. For now, notice that Python gives meaningful messages when programming errors occur, instead of crashing silently:

25.% python

26.>>> 2 ** 500

27.32733906078961418700131896968275991522166420460430647894832913680961337964046745

28.54883270092325904157150886684127560071009217256545885393053328527589376

29.>>>

30.>>> 1 / 0

31.Traceback (most recent call last):

32. File "<stdin>", line 1, in <module>

33.ZeroDivisionError: int division or modulo by zero

34.>>>

35.>>> spam

36.Traceback (most recent call last):

37. File "<stdin>", line 1, in <module>

38.NameError: name 'spam' is not defined

39.Breaks and cycles. When you type this code:

40.L = [1, 2]

41.L.append(L)

you create a cyclic data structure in Python. In Python releases before 1.5.1, the Python printer wasn’t smart enough to detect cycles in objects, and it would print an unending stream of [1, 2, [1, 2, [1, 2, [1, 2, and so on, until you hit the break-key combination on your machine (which, technically, raises a keyboard-interrupt exception that prints a default message). Beginning with Python 1.5.1, the printer is clever enough to detect cycles and prints [[...]] instead to let you know that it has detected a loop in the object’s structure and avoided getting stuck printing forever.

The reason for the cycle is subtle and requires information you will glean in Part II, so this is something of a preview. But in short, assignments in Python always generate references to objects, not copies of them. You can think of objects as chunks of memory and of references as implicitly followed pointers. When you run the first assignment above, the name L becomes a named reference to a two-item list object—a pointer to a piece of memory. Python lists are really arrays of object references, with an append method that changes the array in place by tacking on another object reference at the end. Here, the append call adds a reference to the front of L at the end of L, which leads to the cycle illustrated in Figure D-1: a pointer at the end of the list that points back to the front of the list.

Besides being printed specially, as you’ll learn in Chapter 6 cyclic objects must also be handled specially by Python’s garbage collector, or their space will remain unreclaimed even when they are no longer in use. Though rare in practice, in some programs that traverse arbitrary objects or structures you might have to detect such cycles yourself by keeping track of where you’ve been to avoid looping. Believe it or not, cyclic data structures can sometimes be useful, despite their special-case printing.

A cyclic object, created by appending a list to itself. By default, Python appends a reference to the original list, not a copy of the list.

Figure D-1. A cyclic object, created by appending a list to itself. By default, Python appends a reference to the original list, not a copy of the list.

Part II, Types and Operations

See Test Your Knowledge: Part II Exercises in Chapter 9 for the exercises.

1. The basics. Here are the sorts of results you should get, along with a few comments about their meaning. Again, note that ; is used in a few of these to squeeze more than one statement onto a single line (the ; is a statement separator), and commas build up tuples displayed in parentheses. Also keep in mind that the / division result near the top differs in Python 2.X and 3.X (see Chapter 5 for details), and the list wrapper around dictionary method calls is needed to display results in 3.X, but not 2.X (see Chapter 8):

2. # Numbers

3.

4. >>> 2 ** 16 # 2 raised to the power 16

5. 65536

6. >>> 2 / 5, 2 / 5.0 # Integer / truncates in 2.X, but not 3.X

7. (0.40000000000000002, 0.40000000000000002)

8.

9. # Strings

10.

11.>>> "spam" + "eggs" # Concatenation

12.'spameggs'

13.>>> S = "ham"

14.>>> "eggs " + S

15.'eggs ham'

16.>>> S * 5 # Repetition

17.'hamhamhamhamham'

18.>>> S[:0] # An empty slice at the front -- [0:0]

19.'' # Empty of same type as object sliced

20.

21.>>> "green %s and %s" % ("eggs", S) # Formatting

22.'green eggs and ham'

23.>>> 'green {0} and {1}'.format('eggs', S)

24.'green eggs and ham'

25.

26.# Tuples

27.

28.>>> ('x',)[0] # Indexing a single-item tuple

29.'x'

30.>>> ('x', 'y')[1] # Indexing a two-item tuple

31.'y'

32.

33.# Lists

34.

35.>>> L = [1,2,3] + [4,5,6] # List operations

36.>>> L, L[:], L[:0], L[-2], L[-2:]

37.([1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6], [], 5, [5, 6])

38.>>> ([1,2,3]+[4,5,6])[2:4]

39.[3, 4]

40.>>> [L[2], L[3]] # Fetch from offsets; store in a list

41.[3, 4]

42.>>> L.reverse(); L # Method: reverse list in place

43.[6, 5, 4, 3, 2, 1]

44.>>> L.sort(); L # Method: sort list in place

45.[1, 2, 3, 4, 5, 6]

46.>>> L.index(4) # Method: offset of first four (search)

47.3

48.

49.# Dictionaries

50.

51.>>> {'a':1, 'b':2}['b'] # Index a dictionary by key

52.2

53.>>> D = {'x':1, 'y':2, 'z':3}

54.>>> D['w'] = 0 # Create a new entry

55.>>> D['x'] + D['w']

56.1

57.>>> D[(1,2,3)] = 4 # A tuple used as a key (immutable)

58.

59.>>> D

60.{'w': 0, 'z': 3, 'y': 2, (1, 2, 3): 4, 'x': 1}

61.

62.>>> list(D.keys()), list(D.values()), (1,2,3) in D # Methods, key test

63.(['w', 'z', 'y', (1, 2, 3), 'x'], [0, 3, 2, 4, 1], True)

64.

65.# Empties

66.

67.>>> [[]], ["",[],(),{},None] # Lots of nothings: empty objects

68.([[]], ['', [], (), {}, None])

69.Indexing and slicing. Indexing out of bounds (e.g., L[4]) raises an error; Python always checks to make sure that all offsets are within the bounds of a sequence.

On the other hand, slicing out of bounds (e.g., L[-1000:100]) works because Python scales out-of-bounds slices so that they always fit (the limits are set to zero and the sequence length, if required).

Extracting a sequence in reverse, with the lower bound greater than the higher bound (e.g., L[3:1]), doesn’t really work. You get back an empty slice ([ ]) because Python scales the slice limits to make sure that the lower bound is always less than or equal to the upper bound (e.g.,L[3:1] is scaled to L[3:3], the empty insertion point at offset 3). Python slices are always extracted from left to right, even if you use negative indexes (they are first converted to positive indexes by adding the sequence length). Note that Python 2.3’s three-limit slices modify this behavior somewhat. For instance, L[3:1:-1] does extract from right to left:

>>> L = [1, 2, 3, 4]

>>> L[4]

Traceback (innermost last):

File "<stdin>", line 1, in ?

IndexError: list index out of range

>>> L[-1000:100]

[1, 2, 3, 4]

>>> L[3:1]

[]

>>> L

[1, 2, 3, 4]

>>> L[3:1] = ['?']

>>> L

[1, 2, 3, '?', 4]

70.Indexing, slicing, and del. Your interaction with the interpreter should look something like the following code. Note that assigning an empty list to an offset stores an empty list object there, but assigning an empty list to a slice deletes the slice. Slice assignment expects another sequence, or you’ll get a type error; it inserts items inside the sequence assigned, not the sequence itself:

71.>>> L = [1,2,3,4]

72.>>> L[2] = []

73.>>> L

74.[1, 2, [], 4]

75.>>> L[2:3] = []

76.>>> L

77.[1, 2, 4]

78.>>> del L[0]

79.>>> L

80.[2, 4]

81.>>> del L[1:]

82.>>> L

83.[2]

84.>>> L[1:2] = 1

85.Traceback (innermost last):

86. File "<stdin>", line 1, in ?

87.TypeError: illegal argument type for built-in operation

88.Tuple assignment. The values of X and Y are swapped. When tuples appear on the left and right of an assignment symbol (=), Python assigns objects on the right to targets on the left according to their positions. This is probably easiest to understand by noting that the targets on the left aren’t a real tuple, even though they look like one; they are simply a set of independent assignment targets. The items on the right are a tuple, which gets unpacked during the assignment (the tuple provides the temporary assignment needed to achieve the swap effect):

89.>>> X = 'spam'

90.>>> Y = 'eggs'

91.>>> X, Y = Y, X

92.>>> X

93.'eggs'

94.>>> Y

95.'spam'

96.Dictionary keys. Any immutable object can be used as a dictionary key, including integers, tuples, strings, and so on. This really is a dictionary, even though some of its keys look like integer offsets. Mixed-type keys work fine, too:

97.>>> D = {}

98.>>> D[1] = 'a'

99.>>> D[2] = 'b'

100. >>> D[(1, 2, 3)] = 'c'

101. >>> D

102. {1: 'a', 2: 'b', (1, 2, 3): 'c'}

103. Dictionary indexing. Indexing a nonexistent key (D['d']) raises an error; assigning to a nonexistent key (D['d']='spam') creates a new dictionary entry. On the other hand, out-of-bounds indexing for lists raises an error too, but so do out-of-bounds assignments. Variable names work like dictionary keys; they must have already been assigned when referenced, but they are created when first assigned. In fact, variable names can be processed as dictionary keys if you wish (they’re made visible in module namespace or stack-frame dictionaries):

104. >>> D = {'a':1, 'b':2, 'c':3}

105. >>> D['a']

106. 1

107. >>> D['d']

108. Traceback (innermost last):

109. File "<stdin>", line 1, in ?

110. KeyError: d

111. >>> D['d'] = 4

112. >>> D

113. {'b': 2, 'd': 4, 'a': 1, 'c': 3}

114. >>>

115. >>> L = [0, 1]

116. >>> L[2]

117. Traceback (innermost last):

118. File "<stdin>", line 1, in ?

119. IndexError: list index out of range

120. >>> L[2] = 3

121. Traceback (innermost last):

122. File "<stdin>", line 1, in ?

123. IndexError: list assignment index out of range

124. Generic operations. Question answers:

o The + operator doesn’t work on different/mixed types (e.g., string + list, list + tuple).

o + doesn’t work for dictionaries, as they aren’t sequences.

o The append method works only for lists, not strings, and keys works only on dictionaries. append assumes its target is mutable, since it’s an in-place extension; strings are immutable.

o Slicing and concatenation always return a new object of the same type as the objects processed:

o >>> "x" + 1

o Traceback (innermost last):

o File "<stdin>", line 1, in ?

o TypeError: illegal argument type for built-in operation

o >>>

o >>> {} + {}

o Traceback (innermost last):

o File "<stdin>", line 1, in ?

o TypeError: bad operand type(s) for +

o >>>

o >>> [].append(9)

o >>> "".append('s')

o Traceback (innermost last):

o File "<stdin>", line 1, in ?

o AttributeError: attribute-less object

o >>>

o >>> list({}.keys()) # list() needed in 3.X, not 2.X

o []

o >>> [].keys()

o Traceback (innermost last):

o File "<stdin>", line 1, in ?

o AttributeError: keys

o >>>

o >>> [][:]

o []

o >>> ""[:]

o ''

125. String indexing. This is a bit of a trick question—because strings are collections of one-character strings, every time you index a string, you get back a string that can be indexed again. S[0][0][0][0][0] just keeps indexing the first character over and over. This generally doesn’t work for lists (lists can hold arbitrary objects) unless the list contains strings:

126. >>> S = "spam"

127. >>> S[0][0][0][0][0]

128. 's'

129. >>> L = ['s', 'p']

130. >>> L[0][0][0]

131. 's'

132. Immutable types. Either of the following solutions works. Index assignment doesn’t, because strings are immutable:

133. >>> S = "spam"

134. >>> S = S[0] + 'l' + S[2:]

135. >>> S

136. 'slam'

137. >>> S = S[0] + 'l' + S[2] + S[3]

138. >>> S

139. 'slam'

(See also the Python 3.X and 2.6+ bytearray string type in Chapter 37—it’s a mutable sequence of small integers that is essentially processed the same as a string.)

140. Nesting. Here is a sample:

141. >>> me = {'name':('John', 'Q', 'Doe'), 'age':'?', 'job':'engineer'}

142. >>> me['job']

143. 'engineer'

144. >>> me['name'][2]

145. 'Doe'

146. Files. Here’s one way to create and read back a text file in Python (ls is a Unix command; use dir on Windows):

147. # File: maker.py

148. file = open('myfile.txt', 'w')

149. file.write('Hello file world!\n') # Or: open().write()

150. file.close() # close not always needed

151.

152. # File: reader.py

153. file = open('myfile.txt') # 'r' is default open mode

154. print(file.read()) # Or print(open().read())

155.

156. % python maker.py

157. % python reader.py

158. Hello file world!

159.

160. % ls -l myfile.txt

161. -rwxrwxrwa 1 0 0 19 Apr 13 16:33 myfile.txt

Part III, Statements and Syntax

See Test Your Knowledge: Part III Exercises in Chapter 15 for the exercises.

1. Coding basic loops. As you work through this exercise, you’ll wind up with code that looks like the following:

2. >>> S = 'spam'

3. >>> for c in S:

4. ... print(ord(c))

5. ...

6. 115

7. 112

8. 97

9. 109

10.

11.>>> x = 0

12.>>> for c in S: x += ord(c) # Or: x = x + ord(c)

13....

14.>>> x

15.433

16.

17.>>> x = []

18.>>> for c in S: x.append(ord(c))

19....

20.>>> x

21.[115, 112, 97, 109]

22.

23.>>> list(map(ord, S)) # list() required in 3.X, not 2.X

24.[115, 112, 97, 109]

25.>>> [ord(c) for c in S] # map and listcomps automate list builders

26.[115, 112, 97, 109]

27.Backslash characters. The example prints the bell character (\a) 50 times; assuming your machine can handle it, and when it’s run outside of IDLE, you may get a series of beeps (or one sustained tone, if your machine is fast enough). Hey—I warned you.

28.Sorting dictionaries. Here’s one way to work through this exercise (see Chapter 8 or Chapter 14 if this doesn’t make sense). Remember, you really do have to split up the keys and sort calls like this because sort returns None. In Python 2.2 and later, you can iterate through dictionary keys directly without calling keys (e.g., for key in D:), but the keys list will not be sorted like it is by this code. In more recent Pythons, you can achieve the same effect with the sorted built-in, too:

29.>>> D = {'a':1, 'b':2, 'c':3, 'd':4, 'e':5, 'f':6, 'g':7}

30.>>> D

31.{'f': 6, 'c': 3, 'a': 1, 'g': 7, 'e': 5, 'd': 4, 'b': 2}

32.>>>

33.>>> keys = list(D.keys()) # list() required in 3.X, not in 2.X

34.>>> keys.sort()

35.>>> for key in keys:

36.... print(key, '=>', D[key])

37....

38.a => 1

39.b => 2

40.c => 3

41.d => 4

42.e => 5

43.f => 6

44.g => 7

45.

46.>>> for key in sorted(D): # Better, in more recent Pythons

47.... print(key, '=>', D[key])

48.Program logic alternatives. Here’s some sample code for the solutions. For step e, assign the result of 2 ** X to a variable outside the loops of steps a and b, and use it inside the loop. Your results may vary a bit; this exercise is mostly designed to get you playing with code alternatives, so anything reasonable gets full credit:

49.# a

50.

51.L = [1, 2, 4, 8, 16, 32, 64]

52.X = 5

53.

54.i = 0

55.while i < len(L):

56. if 2 ** X == L[i]:

57. print('at index', i)

58. break

59. i += 1

60.else:

61. print(X, 'not found')

62.

63.# b

64.

65.L = [1, 2, 4, 8, 16, 32, 64]

66.X = 5

67.

68.for p in L:

69. if (2 ** X) == p:

70. print((2 ** X), 'was found at', L.index(p))

71. break

72.else:

73. print(X, 'not found')

74.

75.# c

76.

77.L = [1, 2, 4, 8, 16, 32, 64]

78.X = 5

79.

80.if (2 ** X) in L:

81. print((2 ** X), 'was found at', L.index(2 ** X))

82.else:

83. print(X, 'not found')

84.

85.# d

86.

87.X = 5

88.L = []

89.for i in range(7): L.append(2 ** i)

90.print(L)

91.

92.if (2 ** X) in L:

93. print((2 ** X), 'was found at', L.index(2 ** X))

94.else:

95. print(X, 'not found')

96.

97.

98.# f

99.

100. X = 5

101. L = list(map(lambda x: 2**x, range(7))) # Or [2**x for x in range(7)]

102. print(L) # list() to print all in 3.X, not 2.X

103.

104. if (2 ** X) in L:

105. print((2 ** X), 'was found at', L.index(2 ** X))

106. else:

107. print(X, 'not found')

108. Code maintenance. There is no fixed solution to show here; see mypydoc.py in the book’s examples package for my edits on this code as one example.

Part IV, Functions and Generators

See Test Your Knowledge: Part IV Exercises in Chapter 21 for the exercises.

1. The basics. There’s not much to this one, but notice that using print (and hence your function) is technically a polymorphic operation, which does the right thing for each type of object:

2. % python

3. >>> def func(x): print(x)

4. ...

5. >>> func("spam")

6. spam

7. >>> func(42)

8. 42

9. >>> func([1, 2, 3])

10.[1, 2, 3]

11.>>> func({'food': 'spam'})

12.{'food': 'spam'}

13.Arguments. Here’s a sample solution. Remember that you have to use print to see results in the test calls because a file isn’t the same as code typed interactively; Python doesn’t normally echo the results of expression statements in files:

14.def adder(x, y):

15. return x + y

16.

17.print(adder(2, 3))

18.print(adder('spam', 'eggs'))

19.print(adder(['a', 'b'], ['c', 'd']))

20.

21.% python mod.py

22.5

23.spameggs

24.['a', 'b', 'c', 'd']

25.varargs. Two alternative adder functions are shown in the following file, adders.py. The hard part here is figuring out how to initialize an accumulator to an empty value of whatever type is passed in. The first solution uses manual type testing to look for an integer, and an empty slice of the first argument (assumed to be a sequence) if the argument is determined not to be an integer. The second solution uses the first argument to initialize and scan items 2 and beyond, much like one of the min function variants shown in Chapter 18.

The second solution is better. Both of these assume all arguments are of the same type, and neither works on dictionaries (as we saw in Part II, + doesn’t work on mixed types or dictionaries). You could add a type test and special code to allow dictionaries, too, but that’s extra credit.

def adder1(*args):

print('adder1', end=' ')

if type(args[0]) == type(0): # Integer?

sum = 0 # Init to zero

else: # else sequence:

sum = args[0][:0] # Use empty slice of arg1

for arg in args:

sum = sum + arg

return sum

def adder2(*args):

print('adder2', end=' ')

sum = args[0] # Init to arg1

for next in args[1:]:

sum += next # Add items 2..N

return sum

for func in (adder1, adder2):

print(func(2, 3, 4))

print(func('spam', 'eggs', 'toast'))

print(func(['a', 'b'], ['c', 'd'], ['e', 'f']))

% python adders.py

adder1 9

adder1 spameggstoast

adder1 ['a', 'b', 'c', 'd', 'e', 'f']

adder2 9

adder2 spameggstoast

adder2 ['a', 'b', 'c', 'd', 'e', 'f']

26.Keywords. Here is my solution to the first and second parts of this exercise (coded in the file mod.py). To iterate over keyword arguments, use the **args form in the function header and use a loop (e.g., for x in args.keys(): use args[x]), or use args.values() to make this the same as summing *args positionals:

27.def adder(good=1, bad=2, ugly=3):

28. return good + bad + ugly

29.

30.print(adder())

31.print(adder(5))

32.print(adder(5, 6))

33.print(adder(5, 6, 7))

34.print(adder(ugly=7, good=6, bad=5))

35.

36.% python mod.py

37.6

38.10

39.14

40.18

41.18

42.

43.# Second part solutions

44.

45.def adder1(*args): # Sum any number of positional args

46. tot = args[0]

47. for arg in args[1:]:

48. tot += arg

49. return tot

50.

51.def adder2(**args): # Sum any number of keyword args

52. argskeys = list(args.keys()) # list needed in 3.X!

53. tot = args[argskeys[0]]

54. for key in argskeys[1:]:

55. tot += args[key]

56. return tot

57.

58.def adder3(**args): # Same, but convert to list of values

59. args = list(args.values()) # list needed to index in 3.X!

60. tot = args[0]

61. for arg in args[1:]:

62. tot += arg

63. return tot

64.

65.def adder4(**args): # Same, but reuse positional version

66. return adder1(*args.values())

67.

68.print(adder1(1, 2, 3), adder1('aa', 'bb', 'cc'))

69.print(adder2(a=1, b=2, c=3), adder2(a='aa', b='bb', c='cc'))

70.print(adder3(a=1, b=2, c=3), adder3(a='aa', b='bb', c='cc'))

71.print(adder4(a=1, b=2, c=3), adder4(a='aa', b='bb', c='cc'))

72.(and 6.) Dictionary tools. Here are my solutions to exercises 5 and 6 (file dicts.py). These are just coding exercises, though, because Python 1.5 added the dictionary methods D.copy() and D1.update(D2) to handle things like copying and adding (merging) dictionaries. SeeChapter 8 for dict.update examples, and Python’s library manual or O’Reilly’s Python Pocket Reference for more details. X[:] doesn’t work for dictionaries, as they’re not sequences (see Chapter 8 for details). Also, remember that if you assign (e = d) rather than copying, you generate a reference to a shared dictionary object; changing d changes e, too:

73.def copyDict(old):

74. new = {}

75. for key in old.keys():

76. new[key] = old[key]

77. return new

78.

79.def addDict(d1, d2):

80. new = {}

81. for key in d1.keys():

82. new[key] = d1[key]

83. for key in d2.keys():

84. new[key] = d2[key]

85. return new

86.

87.% python

88.>>> from dicts import *

89.>>> d = {1: 1, 2: 2}

90.>>> e = copyDict(d)

91.>>> d[2] = '?'

92.>>> d

93.{1: 1, 2: '?'}

94.>>> e

95.{1: 1, 2: 2}

96.

97.>>> x = {1: 1}

98.>>> y = {2: 2}

99.>>> z = addDict(x, y)

100. >>> z

101. {1: 1, 2: 2}

102. See #5.

103. More argument-matching examples. Here is the sort of interaction you should get, along with comments that explain the matching that goes on:

104. def f1(a, b): print(a, b) # Normal args

105.

106. def f2(a, *b): print(a, b) # Positional varargs

107.

108. def f3(a, **b): print(a, b) # Keyword varargs

109.

110. def f4(a, *b, **c): print(a, b, c) # Mixed modes

111.

112. def f5(a, b=2, c=3): print(a, b, c) # Defaults

113.

114. def f6(a, b=2, *c): print(a, b, c) # Defaults and positional varargs

115.

116.

117. % python

118. >>> f1(1, 2) # Matched by position (order matters)

119. 1 2

120. >>> f1(b=2, a=1) # Matched by name (order doesn't matter)

121. 1 2

122.

123. >>> f2(1, 2, 3) # Extra positionals collected in a tuple

124. 1 (2, 3)

125.

126. >>> f3(1, x=2, y=3) # Extra keywords collected in a dictionary

127. 1 {'x': 2, 'y': 3}

128.

129. >>> f4(1, 2, 3, x=2, y=3) # Extra of both kinds

130. 1 (2, 3) {'x': 2, 'y': 3}

131.

132. >>> f5(1) # Both defaults kick in

133. 1 2 3

134. >>> f5(1, 4) # Only one default used

135. 1 4 3

136.

137. >>> f6(1) # One argument: matches "a"

138. 1 2 ()

139. >>> f6(1, 3, 4) # Extra positional collected

140. 1 3 (4,)

141. Primes revisited. Here is the primes example, wrapped up in a function and a module (file primes.py) so it can be run multiple times. I added an if test to trap negatives, 0, and 1. I also changed / to // in this edition to make this solution immune to the Python 3.X / true division changes we studied in Chapter 5, and to enable it to support floating-point numbers (uncomment the from statement and change // to / to see the differences in 2.X):

142. #from __future__ import division

143.

144. def prime(y):

145. if y <= 1: # For some y > 1

146. print(y, 'not prime')

147. else:

148. x = y // 2 # 3.X / fails

149. while x > 1:

150. if y % x == 0: # No remainder?

151. print(y, 'has factor', x)

152. break # Skip else

153. x -= 1

154. else:

155. print(y, 'is prime')

156.

157. prime(13); prime(13.0)

158. prime(15); prime(15.0)

159. prime(3); prime(2)

160. prime(1); prime(-3)

Here is the module in action; the // operator allows it to work for floating-point numbers too, even though it perhaps should not:

% python primes.py

13 is prime

13.0 is prime

15 has factor 5

15.0 has factor 5.0

3 is prime

2 is prime

1 not prime

-3 not prime

This function still isn’t very reusable—it could return values, instead of printing—but it’s enough to run experiments. It’s also not a strict mathematical prime (floating points work), and it’s still inefficient. Improvements are left as exercises for more mathematically minded readers. (Hint: a for loop over range(y, 1, −1) may be a bit quicker than the while, but the algorithm is the real bottleneck here.) To time alternatives, use the homegrown timer or standard library timeit modules and coding patterns like those used in Chapter 21’s timing sections (and see Solution 10).

161. Iterations and comprehensions. Here is the sort of code you should write; I may have a preference, but yours may vary:

162. >>> values = [2, 4, 9, 16, 25]

163. >>> import math

164.

165. >>> res = []

166. >>> for x in values: res.append(math.sqrt(x))

167. ...

168. >>> res

169. [1.4142135623730951, 2.0, 3.0, 4.0, 5.0]

170.

171. >>> list(map(math.sqrt, values))

172. [1.4142135623730951, 2.0, 3.0, 4.0, 5.0]

173.

174. >>> [math.sqrt(x) for x in values]

175. [1.4142135623730951, 2.0, 3.0, 4.0, 5.0]

176.

177. >>> list(math.sqrt(x) for x in values)

178. [1.4142135623730951, 2.0, 3.0, 4.0, 5.0]

179. Timing tools. Here is some code I wrote to time the three square root options, along with the results in CPythons 3.3 and 2.7 and PyPy 1.9 (which implements Python 2.7). Each test takes the best of three runs; each run takes the total time required to call the test function 1,000 times; and each test function iterates 1,000 times. The last result of each function is printed to verify that all three do the same work:

180. # File timer2.py (2.X and 3.X)

181. ...same as listed in Chapter 21...

182.

183. # File timesqrt.py

184. import sys, timer2

185. reps = 10000

186. repslist = range(reps) # Pull out range list time for 2.X

187.

188. from math import sqrt # Not math.sqrt: adds attr fetch time

189. def mathMod():

190. for i in repslist:

191. res = sqrt(i)

192. return res

193.

194. def powCall():

195. for i in repslist:

196. res = pow(i, .5)

197. return res

198.

199. def powExpr():

200. for i in repslist:

201. res = i ** .5

202. return res

203.

204. print(sys.version)

205. for test in (mathMod, powCall, powExpr):

206. elapsed, result = timer2.bestoftotal(test, _reps1=3, _reps=1000)

207. print ('%s: %.5f => %s' % (test.__name__, elapsed, result))

Following are the test results for the three Pythons. The 3.3 and 2.7 results are roughly twice as fast as 3.0 and 2.6 in the prior edition, due largely to a faster test machine. For each Python tested, it looks like the math module is quicker than the ** expression, which is quicker than thepow call; however, you should try this with your code and on your own machine and version of Python. Also, note that Python 3.3 is essentially twice as slow as 2.7 on this test, and PyPy is a rough order of magnitude (10X) faster than both CPythons, despite the fact that this is running floating-point math and iterations. Later versions of any of these Pythons might differ, so time this in the future to see for yourself:

c:\code> py −3 timesqrt.py

3.3.0 (v3.3.0:bd8afb90ebf2, Sep 29 2012, 10:57:17) [MSC v.1600 64 bit (AMD64)]

mathMod: 2.04481 => 99.99499987499375

powCall: 3.40973 => 99.99499987499375

powExpr: 2.56458 => 99.99499987499375

c:\code> py −2 timesqrt.py

2.7.3 (default, Apr 10 2012, 23:24:47) [MSC v.1500 64 bit (AMD64)]

mathMod: 1.04337 => 99.994999875

powCall: 2.57516 => 99.994999875

powExpr: 1.89560 => 99.994999875

c:\code> c:\pypy\pypy-1.9\pypy timesqrt.py

2.7.2 (341e1e3821ff, Jun 07 2012, 15:43:00)

[PyPy 1.9.0 with MSC v.1500 32 bit]

mathMod: 0.07491 => 99.994999875

powCall: 0.85678 => 99.994999875

powExpr: 0.85453 => 99.994999875

To time the relative speeds of Python 3.X and 2.7 dictionary comprehensions and equivalent for loops interactively, you can run a session like the following. It appears that the two are roughly the same in this regard under Python 3.3; unlike list comprehensions, though, manual loops are slightly faster than dictionary comprehensions today (though the difference isn’t exactly earth-shattering—at the end we save half a second when making 50 dictionaries of 1,000,000 items each). Again, rather than taking these results as gospel you should investigate further on your own, on your computer and with your Python:

C:\code> c:\python33\python

>>>

>>> def dictcomp(I):

return {i: i for i in range(I)}

>>> def dictloop(I):

new = {}

for i in range(I): new[i] = i

return new

>>> dictcomp(10)

{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}

>>> dictloop(10)

{0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9}

>>>

>>> from timer2 import total, bestof

>>> bestof(dictcomp, 10000)[0] # 10,000-item dict

0.0017095345403959072

>>> bestof(dictloop, 10000)[0]

0.002097576400046819

>>>

>>> bestof(dictcomp, 100000)[0] # 100,000-items: 10X slower

0.012716923463358398

>>> bestof(dictloop, 100000)[0]

0.014129806355413166

>>>

>>> bestof(dictcomp, 1000000)[0] # 1 of 1M-items: 10X time

0.11614425187337929

>>> bestof(dictloop, 1000000)[0]

0.1331144855439561

>>>

>>> total(dictcomp, 1000000, _reps=50)[0] # Total to make 50 1M-item dicts

5.8162020671780965

>>> total(dictloop, 1000000, _reps=50)[0]

6.626680761285343

208. Recursive functions. I coded this function as follows; a simple range, comprehension, or map will do the job here as well, but recursion is useful enough to experiment with here (print is a function in 3.X only, unless you import it from __future__ or code your own equivalent):

209. def countdown(N):

210. if N == 0:

211. print('stop') # 2.X: print 'stop'

212. else:

213. print(N, end=' ') # 2.X: print N,

214. countdown(N-1)

215.

216. >>> countdown(5)

217. 5 4 3 2 1 stop

218. >>> countdown(20)

219. 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 stop

220.

221. # Nonrecursive options:

222. >>> list(range(5, 0, −1))

223. [5, 4, 3, 2, 1]

224.

225. # On 3.X only:

226. >>> t = [print(i, end=' ') for i in range(5, 0, −1)]

227. 5 4 3 2 1

228. >>> t = list(map(lambda x: print(x, end=' '), range(5, 0, −1)))

229. 5 4 3 2 1

I didn’t include a generator-based solution in this exercise on the grounds of merit (and humanity!), but one is listed below; all the other techniques seem much simpler in this case—a good example of cases where generators should probably be avoided. Remember that generators produce no results until iterated, so we need a for or yield from here (yield from works in 3.3 and later only):

def countdown2(N): # Generator function, recursive

if N == 0:

yield 'stop'

else:

yield N

for x in countdown2(N-1): yield x # 3.3+: yield from countdown2(N-1)

>>> list(countdown2(5))

[5, 4, 3, 2, 1, 'stop']

# Nonrecursive options:

>>> def countdown3(): # Generator function, simpler

yield from range(5, 0, −1) # Pre 3.3: for x in range(): yield x

>>> list(countdown3())

[5, 4, 3, 2, 1]

>>> list(x for x in range(5, 0, −1)) # Equivalent generator expression

[5, 4, 3, 2, 1]

>>> list(range(5, 0, −1)) # Equivalent nongenerator form

[5, 4, 3, 2, 1]

230. Computing factorials. The following file shows how I coded this exercise; it runs on Python 3.X and 2.X, and its output on 3.3 is given in a string literal at the end of the file. Naturally, there are many possible variations on its code; its ranges, for instance, could run from 2..N+1 to skip an iteration, and fact2 could use reduce(operator.mul, range(N, 1, −1)) to avoid a lambda.

231. #!python

232. from __future__ import print_function # File factorials.py

233. from functools import reduce

234. from timeit import repeat

235. import math

236.

237. def fact0(N): # Recursive

238. if N == 1: # Fails at 999 by default

239. return N

240. else:

241. return N * fact0(N-1)

242.

243. def fact1(N):

244. return N if N == 1 else N * fact1(N-1) # Recursive, one-liner

245.

246. def fact2(N): # Functional

247. return reduce(lambda x, y: x * y, range(1, N+1))

248.

249. def fact3(N):

250. res = 1

251. for i in range(1, N+1): res *= i # Iterative

252. return res

253.

254. def fact4(N):

255. return math.factorial(N) # Stdlib "batteries"

256.

257. # Tests

258. print(fact0(6), fact1(6), fact2(6), fact3(6), fact4(6)) # 6*5*4*3*2*1: all 720

259. print(fact0(500) == fact1(500) == fact2(500) == fact3(500) == fact4(500)) # True

260.

261. for test in (fact0, fact1, fact2, fact3, fact4):

262. print(test.__name__, min(repeat(stmt=lambda: test(500), number=20, repeat=3)))

263.

264. r"""

265. C:\code> py −3 factorials.py

266. 720 720 720 720 720

267. True

268. fact0 0.003990868798355564

269. fact1 0.003901433457907475

270. fact2 0.002732909419593966

271. fact3 0.002052614370939676

272. fact4 0.0003401475243271501

273. """

Conclusions: recursion is slowest on my Python and machine, and fails once N reaches 999 due to the default stack size setting in sys; per Chapter 19, this limit can be increased, but simple loops or the standard library tool seem the best route here in any event.

This general finding holds true often. For instance, ''.join(reversed(S)) may be the preferred way to reverse a string, even though recursive solutions are possible. Time the following to see how: as for factorials in 3.X, recursion is today an order of magnitude slower in CPython, though these results vary in PyPy:

def rev1(S):

if len(S) == 1:

return S

else:

return S[-1] + rev1(S[:-1]) # Recursive: 10x slower in CPython today

def rev2(S):

return ''.join(reversed(S)) # Nonrecursive iterable: simpler, faster

def rev3(S):

return S[::-1] # Even better?: sequence reversal by slice

Part V, Modules and Packages

See Test Your Knowledge: Part V Exercises in Chapter 25 for the exercises.

1. Import basics. When you’re done, your file (mymod.py) and interaction should look similar to the following; remember that Python can read a whole file into a list of line strings, and the len built-in returns the lengths of strings and lists:

2. def countLines(name):

3. file = open(name)

4. return len(file.readlines())

5.

6. def countChars(name):

7. return len(open(name).read())

8.

9. def test(name): # Or pass file object

10. return countLines(name), countChars(name) # Or return a dictionary

11.

12.% python

13.>>> import mymod

14.>>> mymod.test('mymod.py')

15.(10, 291)

Your counts may vary, as mine may or may not include comments and an extra line at the end. Note that these functions load the entire file in memory all at once, so they won’t work for pathologically large files too big for your machine’s memory. To be more robust, you could read line by line with iterators instead and count as you go:

def countLines(name):

tot = 0

for line in open(name): tot += 1

return tot

def countChars(name):

tot = 0

for line in open(name): tot += len(line)

return tot

A generator expression can have the same effect (though the instructor might take off points for excessive magic!):

def countlines(name): return sum(+1 for line in open(name))

def countchars(name): return sum(len(line) for line in open(name))

On Unix, you can verify your output with a wc command; on Windows, right-click on your file to view its properties. Note that your script may report fewer characters than Windows does—for portability, Python converts Windows \r\n line-end markers to \n, thereby dropping 1 byte (character) per line. To match byte counts with Windows exactly, you must open in binary mode ('rb'), or add the number of bytes corresponding to the number of lines. See Chapter 9 and Chapter 37 for more on end-of-line translations in text files.

The “ambitious” part of this exercise (passing in a file object so you only open the file once), will require you to use the seek method of the built-in file object. It works like C’s fseek call (and may call it behind the scenes): seek resets the current position in the file to a passed-in offset. After a seek, future input/output operations are relative to the new position. To rewind to the start of a file without closing and reopening it, call file.seek(0); the file read methods all pick up at the current position in the file, so you need to rewind to reread. Here’s what this tweak would look like:

def countLines(file):

file.seek(0) # Rewind to start of file

return len(file.readlines())

def countChars(file):

file.seek(0) # Ditto (rewind if needed)

return len(file.read())

def test(name):

file = open(name) # Pass file object

return countLines(file), countChars(file) # Open file only once

>>> import mymod2

>>> mymod2.test("mymod2.py")

(11, 392)

16.from/from *. Here’s the from * part; replace * with countChars to do the rest:

17.% python

18.>>> from mymod import *

19.>>> countChars("mymod.py")

20.291

21.__main__. If you code it properly, this file works in either mode—program run or module import:

22.def countLines(name):

23. file = open(name)

24. return len(file.readlines())

25.

26.def countChars(name):

27. return len(open(name).read())

28.

29.def test(name): # Or pass file object

30. return countLines(name), countChars(name) # Or return a dictionary

31.

32.if __name__ == '__main__':

33. print(test('mymod.py'))

34.

35.% python mymod.py

36.(13, 346)

This is where I would probably begin to consider using command-line arguments or user input to provide the filename to be counted, instead of hardcoding it in the script (see Chapter 25 for more on sys.argv, and Chapter 10 for more on input—and use raw_input instead in 2.X):

if __name__ == '__main__':

print(test(input('Enter file name:')) # Console (raw_input in 2.X)

if __name__ == '__main__':

import sys # Command line

print(test(sys.argv[1]))

37.Nested imports. Here is my solution (file myclient.py):

38.from mymod import countLines, countChars

39.print(countLines('mymod.py'), countChars('mymod.py'))

40.

41.% python myclient.py

42.13 346

As for the rest of this one, mymod’s functions are accessible (that is, importable) from the top level of myclient, since from simply assigns to names in the importer (it works as if mymod’s defs appeared in myclient). For example, another file can say:

import myclient

myclient.countLines(...)

from myclient import countChars

countChars(...)

If myclient used import instead of from, you’d need to use a path to get to the functions in mymod through myclient:

import myclient

myclient.mymod.countLines(...)

from myclient import mymod

mymod.countChars(...)

In general, you can define collector modules that import all the names from other modules so they’re available in a single convenience module. The following partial code, for example, creates three different copies of the name somename—mod1.somename, collector.somename, and __main__.somename; all three share the same integer object initially, and only the name somename exists at the interactive prompt as is:

# File mod1.py

somename = 42

# File collector.py

from mod1 import * # Collect lots of names here

from mod2 import * # from assigns to my names

from mod3 import *

>>> from collector import somename

43.Package imports. For this, I put the mymod.py solution file listed for exercise 3 into a directory package. The following is what I did in a Windows console interface to set up the directory and the __init__.py file that it’s required to have until Python 3.3; you’ll need to interpolate for other platforms (e.g., use cp and vi instead of copy and notepad). This works in any directory (I’m using my own code directory here), and you can do some of this from a file explorer GUI, too.

When I was done, I had a mypkg subdirectory that contained the files __init__.py and mymod.py. Until Python 3.3’s namespace package extension, you need an __init__.py in the mypkg directory, but not in its parent; technically, mypkg is located in the home directory component of the module search path. Notice how a print statement coded in the directory’s initialization file fires only the first time it is imported, not the second; raw strings are also used here to avoid escape issues in the file paths:

C:\code> mkdir mypkg

C:\code> copy mymod.py mypkg\mymod.py

C:\code> notepad mypkg\__init__.py

...coded a print statement...

C:\code> python

>>> import mypkg.mymod

initializing mypkg

>>> mypkg.mymod.countLines(r'mypkg\mymod.py')

13

>>> from mypkg.mymod import countChars

>>> countChars(r'mypkg\mymod.py')

346

44.Reloads. This exercise just asks you to experiment with changing the changer.py example in the book, so there’s nothing to show here.

45.Circular imports. The short story is that importing recur2 first works because the recursive import then happens at the import in recur1, not at a from in recur2.

The long story goes like this: importing recur2 first works because the recursive import from recur1 to recur2 fetches recur2 as a whole, instead of getting specific names. recur2 is incomplete when it’s imported from recur1, but because it uses import instead of from, you’re safe: Python finds and returns the already created recur2 module object and continues to run the rest of recur1 without a glitch. When the recur2 import resumes, the second from finds the name Y in recur1 (it’s been run completely), so no error is reported.

Running a file as a script is not the same as importing it as a module; these cases are the same as running the first import or from in the script interactively. For instance, running recur1 as a script works, because it is the same as importing recur2 interactively, as recur2 is the first module imported in recur1. Running recur2 as a script fails for the same reason—it’s the same as running its first import interactively.

Part VI, Classes and OOP

See Test Your Knowledge: Part VI Exercises in Chapter 32 for the exercises.

1. Inheritance. Here’s the solution code for this exercise (file adder.py), along with some interactive tests. The __add__ overload has to appear only once, in the superclass, as it invokes type-specific add methods in subclasses:

2. class Adder:

3. def add(self, x, y):

4. print('not implemented!')

5. def __init__(self, start=[]):

6. self.data = start

7. def __add__(self, other): # Or in subclasses?

8. return self.add(self.data, other) # Or return type?

9.

10.class ListAdder(Adder):

11. def add(self, x, y):

12. return x + y

13.

14.class DictAdder(Adder):

15. def add(self, x, y):

16. new = {}

17. for k in x.keys(): new[k] = x[k]

18. for k in y.keys(): new[k] = y[k]

19. return new

20.

21.% python

22.>>> from adder import *

23.>>> x = Adder()

24.>>> x.add(1, 2)

25.not implemented!

26.>>> x = ListAdder()

27.>>> x.add([1], [2])

28.[1, 2]

29.>>> x = DictAdder()

30.>>> x.add({1:1}, {2:2})

31.{1: 1, 2: 2}

32.

33.>>> x = Adder([1])

34.>>> x + [2]

35.not implemented!

36.>>>

37.>>> x = ListAdder([1])

38.>>> x + [2]

39.[1, 2]

40.>>> [2] + x

41.In 3.3: TypeError: can only concatenate list (not "ListAdder") to list

42.Earlier: TypeError: __add__ nor __radd__ defined for these operands

Notice in the last test that you get an error for expressions where a class instance appears on the right of a +; if you want to fix this, use __radd__ methods, as described in “Operator Overloading” in Chapter 30.

If you are saving a value in the instance anyhow, you might as well rewrite the add method to take just one argument, in the spirit of other examples in this part of the book (this is adder2.py):

class Adder:

def __init__(self, start=[]):

self.data = start

def __add__(self, other): # Pass a single argument

return self.add(other) # The left side is in self

def add(self, y):

print('not implemented!')

class ListAdder(Adder):

def add(self, y):

return self.data + y

class DictAdder(Adder):

def add(self, y):

d = self.data.copy() # Change to use self.data instead of x

d.update(y) # Or "cheat" by using quicker built-ins

return d

x = ListAdder([1, 2, 3])

y = x + [4, 5, 6]

print(y) # Prints [1, 2, 3, 4, 5, 6]

z = DictAdder(dict(name='Bob')) + {'a':1}

print(z) # Prints {'name': 'Bob', 'a': 1}

Because values are attached to objects rather than passed around, this version is arguably more object-oriented. And, once you’ve gotten to this point, you’ll probably find that you can get rid of add altogether and simply define type-specific __add__ methods in the two subclasses.

43.Operator overloading. The solution code (file mylist.py) uses a handful of operator overloading methods we explored in Chapter 30. Copying the initial value in the constructor is important because it may be mutable; you don’t want to change or have a reference to an object that’s possibly shared somewhere outside the class. The __getattr__ method routes calls to the wrapped list. For hints on an easier way to code this in Python 2.2 and later, see Extending Types by Subclassing in Chapter 32:

44.class MyList:

45. def __init__(self, start):

46. #self.wrapped = start[:] # Copy start: no side effects

47. self.wrapped = list(start) # Make sure it's a list here

48. def __add__(self, other):

49. return MyList(self.wrapped + other)

50. def __mul__(self, time):

51. return MyList(self.wrapped * time)

52. def __getitem__(self, offset): # Also passed a slice in 3.X

53. return self.wrapped[offset] # For iteration if no __iter__

54. def __len__(self):

55. return len(self.wrapped)

56. def __getslice__(self, low, high): # Ignored in 3.X: uses __getitem__

57. return MyList(self.wrapped[low:high])

58. def append(self, node):

59. self.wrapped.append(node)

60. def __getattr__(self, name): # Other methods: sort/reverse/etc

61. return getattr(self.wrapped, name)

62. def __repr__(self): # Catchall display method

63. return repr(self.wrapped)

64.

65.if __name__ == '__main__':

66. x = MyList('spam')

67. print(x)

68. print(x[2])

69. print(x[1:])

70. print(x + ['eggs'])

71. print(x * 3)

72. x.append('a')

73. x.sort()

74. print(' '.join(c for c in x))

75.

76.c:\code> python mylist.py

77.['s', 'p', 'a', 'm']

78.a

79.['p', 'a', 'm']

80.['s', 'p', 'a', 'm', 'eggs']

81.['s', 'p', 'a', 'm', 's', 'p', 'a', 'm', 's', 'p', 'a', 'm']

82.a a m p s

Note that it’s important to copy the start value by calling list instead of slicing here, because otherwise the result may not be a true list and so will not respond to expected list methods, such as append (e.g., slicing a string returns another string, not a list). You would be able to copy a MyList start value by slicing because its class overloads the slicing operation and provides the expected list interface; however, you need to avoid slice-based copying for objects such as strings.

83.Subclassing. My solution (mysub.py) appears as follows. Your solution should be similar:

84.from mylist import MyList

85.

86.class MyListSub(MyList):

87. calls = 0 # Shared by instances

88. def __init__(self, start):

89. self.adds = 0 # Varies in each instance

90. MyList.__init__(self, start)

91.

92. def __add__(self, other):

93. print('add: ' + str(other))

94. MyListSub.calls += 1 # Class-wide counter

95. self.adds += 1 # Per-instance counts

96. return MyList.__add__(self, other)

97.

98. def stats(self):

99. return self.calls, self.adds # All adds, my adds

100.

101. if __name__ == '__main__':

102. x = MyListSub('spam')

103. y = MyListSub('foo')

104. print(x[2])

105. print(x[1:])

106. print(x + ['eggs'])

107. print(x + ['toast'])

108. print(y + ['bar'])

109. print(x.stats())

110.

111. c:\code> python mysub.py

112. a

113. ['p', 'a', 'm']

114. add: ['eggs']

115. ['s', 'p', 'a', 'm', 'eggs']

116. add: ['toast']

117. ['s', 'p', 'a', 'm', 'toast']

118. add: ['bar']

119. ['f', 'o', 'o', 'bar']

120. (3, 2)

121. Attribute methods. I worked through this exercise as follows. Notice that in Python 2.X’s classic classes, operators try to fetch attributes through __getattr__, too; you need to return a value to make them work. As noted in Chapter 32 and elsewhere, __getattr__ is not called for built-in operations in Python 3.X (and in 2.X if new-style classes are used), so the expressions aren’t intercepted at all here; in new-style classes, a class like this must redefine __X__ operator overloading methods explicitly. More on this in Chapter 28, Chapter 31, Chapter 32,Chapter 38, and Chapter 39: it can impact much code!

122. c:\code> py −2

123. >>> class Attrs:

124. def __getattr__(self, name):

125. print('get %s' % name)

126. def __setattr__(self, name, value):

127. print('set %s %s' % (name, value))

128.

129. >>> x = Attrs()

130. >>> x.append

131. get append

132. >>> x.spam = 'pork'

133. set spam pork

134. >>> x + 2

135. get __coerce__

136. TypeError: 'NoneType' object is not callable

137. >>> x[1]

138. get __getitem__

139. TypeError: 'NoneType' object is not callable

140. >>> x[1:5]

141. get __getslice__

142. TypeError: 'NoneType' object is not callable

143.

144. c:\code> py −3

145. >>> ...same startup code...

146. >>> x + 2

147. TypeError: unsupported operand type(s) for +: 'Attrs' and 'int'

148. >>> x[1]

149. TypeError: 'Attrs' object does not support indexing

150. >>> x[1:5]

151. TypeError: 'Attrs' object is not subscriptable

152. Set objects. Here’s the sort of interaction you should get. Comments explain which methods are called. Also, note that sets are a built-in type in Python today, so this is largely just a coding exercise (see Chapter 5 for more on sets).

153. % python

154. >>> from setwrapper import Set

155. >>> x = Set([1, 2, 3, 4]) # Runs __init__

156. >>> y = Set([3, 4, 5])

157.

158. >>> x & y # __and__, intersect, then __repr__

159. Set:[3, 4]

160. >>> x | y # __or__, union, then __repr__

161. Set:[1, 2, 3, 4, 5]

162.

163. >>> z = Set("hello") # __init__ removes duplicates

164. >>> z[0], z[-1], z[2:] # __getitem__

165. ('h', 'o', ['l', 'o'])

166.

167. >>> for c in z: print(c, end=' ') # __iter__ (else __getitem__) [3.X print]

168. ...

169. h e l o

170. >>> ''.join(c.upper() for c in z) # __iter__ (else __getitem__)

171. 'HELO'

172. >>> len(z), z # __len__, __repr__

173. (4, Set:['h', 'e', 'l', 'o'])

174.

175. >>> z & "mello", z | "mello"

176. (Set:['e', 'l', 'o'], Set:['h', 'e', 'l', 'o', 'm'])

My solution to the multiple-operand extension subclass looks like the following class (file multiset.py). It needs to replace only two methods in the original set. The class’s documentation string explains how it works:

from setwrapper import Set

class MultiSet(Set):

"""

Inherits all Set names, but extends intersect and union to support

multiple operands; note that "self" is still the first argument

(stored in the *args argument now); also note that the inherited

& and | operators call the new methods here with 2 arguments, but

processing more than 2 requires a method call, not an expression;

intersect doesn't remove duplicates here: the Set constructor does;

"""

def intersect(self, *others):

res = []

for x in self: # Scan first sequence

for other in others: # For all other args

if x not in other: break # Item in each one?

else: # No: break out of loop

res.append(x) # Yes: add item to end

return Set(res)

def union(*args): # self is args[0]

res = []

for seq in args: # For all args

for x in seq: # For all nodes

if not x in res:

res.append(x) # Add new items to result

return Set(res)

Your interaction with the extension will look something like the following. Note that you can intersect by using & or calling intersect, but you must call intersect for three or more operands; & is a binary (two-sided) operator. Also, note that we could have called MultiSetsimply Set to make this change more transparent if we used setwrapper.Set to refer to the original within multiset (the as clause in an import could rename the class too if desired):

>>> from multiset import *

>>> x = MultiSet([1, 2, 3, 4])

>>> y = MultiSet([3, 4, 5])

>>> z = MultiSet([0, 1, 2])

>>> x & y, x | y # Two operands

(Set:[3, 4], Set:[1, 2, 3, 4, 5])

>>> x.intersect(y, z) # Three operands

Set:[]

>>> x.union(y, z)

Set:[1, 2, 3, 4, 5, 0]

>>> x.intersect([1,2,3], [2,3,4], [1,2,3]) # Four operands

Set:[2, 3]

>>> x.union(range(10)) # Non-MultiSets work, too

Set:[1, 2, 3, 4, 0, 5, 6, 7, 8, 9]

>>> w = MultiSet('spam') # String sets

>>> w

Set:['s', 'p', 'a', 'm']

>>> ''.join(w | 'super')

'spamuer'

>>> (w | 'super') & MultiSet('slots')

Set:['s']

177. Class tree links. Here is the way I changed the lister classes, and a rerun of the test to show its format. Do the same for the dir-based version, and also do this when formatting class objects in the tree climber variant:

178. class ListInstance:

179. def __attrnames(self):

180. ...unchanged...

181.

182. def __str__(self):

183. return '<Instance of %s(%s), address %s:\n%s>' % (

184. self.__class__.__name__, # My class's name

185. self.__supers(), # My class's own supers

186. id(self), # My address

187. self.__attrnames()) # name=value list

188.

189. def __supers(self):

190. names = []

191. for super in self.__class__.__bases__: # One level up from class

192. names.append(super.__name__) # name, not str(super)

193. return ', '.join(names)

194.

195. # Or: ', '.join(super.__name__ for super in self.__class__.__bases__)

196.

197. c:\code> py listinstance-exercise.py

198. <Instance of Sub(Super, ListInstance), address 43671000:

199. data1=spam

200. data2=eggs

201. data3=42

202. >

203. Composition. My solution is as follows (file lunch.py), with comments from the description mixed in with the code. This is one case where it’s probably easier to express a problem in Python than it is in English:

204. class Lunch:

205. def __init__(self): # Make/embed Customer, Employee

206. self.cust = Customer()

207. self.empl = Employee()

208. def order(self, foodName): # Start Customer order simulation

209. self.cust.placeOrder(foodName, self.empl)

210. def result(self): # Ask the Customer about its Food

211. self.cust.printFood()

212.

213. class Customer:

214. def __init__(self): # Initialize my food to None

215. self.food = None

216. def placeOrder(self, foodName, employee): # Place order with Employee

217. self.food = employee.takeOrder(foodName)

218. def printFood(self): # Print the name of my food

219. print(self.food.name)

220.

221. class Employee:

222. def takeOrder(self, foodName): # Return Food, with desired name

223. return Food(foodName)

224.

225. class Food:

226. def __init__(self, name): # Store food name

227. self.name = name

228.

229. if __name__ == '__main__':

230. x = Lunch() # Self-test code

231. x.order('burritos') # If run, not imported

232. x.result()

233. x.order('pizza')

234. x.result()

235.

236. % python lunch.py

237. burritos

238. pizza

239. Zoo animal hierarchy. Here is the way I coded the taxonomy in Python (file zoo.py); it’s artificial, but the general coding pattern applies to many real structures, from GUIs to employee databases to spacecraft. Notice that the self.speak reference in Animal triggers an independent inheritance search, which finds speak in a subclass. Test this interactively per the exercise description. Try extending this hierarchy with new classes, and making instances of various classes in the tree:

240. class Animal:

241. def reply(self): self.speak() # Back to subclass

242. def speak(self): print('spam') # Custom message

243.

244. class Mammal(Animal):

245. def speak(self): print('huh?')

246.

247. class Cat(Mammal):

248. def speak(self): print('meow')

249.

250. class Dog(Mammal):

251. def speak(self): print('bark')

252.

253. class Primate(Mammal):

254. def speak(self): print('Hello world!')

255.

256. class Hacker(Primate): pass # Inherit from Primate

257. The Dead Parrot Sketch. Here’s how I implemented this one (file parrot.py). Notice how the line method in the Actor superclass works: by accessing self attributes twice, it sends Python back to the instance twice, and hence invokes two inheritance searches—self.name andself.says() find information in the specific subclasses:

258. class Actor:

259. def line(self): print(self.name + ':', repr(self.says()))

260.

261. class Customer(Actor):

262. name = 'customer'

263. def says(self): return "that's one ex-bird!"

264.

265. class Clerk(Actor):

266. name = 'clerk'

267. def says(self): return "no it isn't..."

268.

269. class Parrot(Actor):

270. name = 'parrot'

271. def says(self): return None

272.

273. class Scene:

274. def __init__(self):

275. self.clerk = Clerk() # Embed some instances

276. self.customer = Customer() # Scene is a composite

277. self.subject = Parrot()

278.

279. def action(self):

280. self.customer.line() # Delegate to embedded

281. self.clerk.line()

282. self.subject.line()

Part VII, Exceptions and Tools

See Test Your Knowledge: Part VII Exercises in Chapter 36 for the exercises.

1. try/except. My version of the oops function (file oops.py) follows. As for the noncoding questions, changing oops to raise a KeyError instead of an IndexError means that the try handler won’t catch the exception—it “percolates” to the top level and triggers Python’s default error message. The names KeyError and IndexError come from the outermost built-in names scope (the B in “LEGB”). Import builtins in 3.X (and __builtin__ in Python 2.X) and pass it as an argument to the dir function to see this for yourself.

2. def oops():

3. raise IndexError()

4.

5. def doomed():

6. try:

7. oops()

8. except IndexError:

9. print('caught an index error!')

10. else:

11. print('no error caught...')

12.

13.if __name__ == '__main__': doomed()

14.

15.% python oops.py

16.caught an index error!

17.Exception objects and lists. Here’s the way I extended this module for an exception of my own, file oops2.py:

18.from __future__ import print_function # 2.X

19.

20.class MyError(Exception): pass

21.

22.def oops():

23. raise MyError('Spam!')

24.

25.def doomed():

26. try:

27. oops()

28. except IndexError:

29. print('caught an index error!')

30. except MyError as data:

31. print('caught error:', MyError, data)

32. else:

33. print('no error caught...')

34.

35.if __name__ == '__main__':

36. doomed()

37.

38.% python oops2.py

39.caught error: <class '__main__.MyError'> Spam!

Like all class exceptions, the instance is accessible via the as variable data; the error message shows both the class (<...>) and its instance (Spam!). The instance must be inheriting both an __init__ and a __repr__ or __str__ from Python’s Exception class, or it would print much like the class does. See Chapter 35 for details on how this works in built-in exception classes.

40.Error handling. Here’s one way to solve this one (file exctools.py). I did my tests in a file, rather than interactively, but the results are similar enough for full credit. Notice that the empty except and sys.exc_info approach used here will catch exit-related exceptions that listingException with an as variable won’t; that’s probably not ideal in most applications code, but might be useful in a tool like this designed to work as a sort of exceptions firewall.

41.import sys, traceback

42.

43.def safe(callee, *pargs, **kargs):

44. try:

45. callee(*pargs, **kargs) # Catch everything else

46. except: # Or "except Exception as E:"

47. traceback.print_exc()

48. print('Got %s %s' % (sys.exc_info()[0], sys.exc_info()[1]))

49.

50.if __name__ == '__main__':

51. import oops2

52. safe(oops2.oops)

53.

54.c:\code> py −3 exctools.py

55.Traceback (most recent call last):

56. File "C:\code\exctools.py", line 5, in safe

57. callee(*pargs, **kargs) # Catch everything else

58. File "C:\code\oops2.py", line 6, in oops

59. raise MyError('Spam!')

60.oops2.MyError: Spam!

61.Got <class 'oops2.MyError'> Spam!

The following sort of code could turn this into a function decorator that could wrap and catch exceptions raised by any function, using techniques introduced in Chapter 32, but covered more fully in Chapter 39 in the next part of the book—it augments a function, rather than expecting it to be passed in explicitly:

import sys, traceback

def safe(callee):

def callproxy(*pargs, **kargs):

try:

return callee(*pargs, **kargs)

except:

traceback.print_exc()

print('Got %s %s' % (sys.exc_info()[0], sys.exc_info()[1]))

raise

return callproxy

if __name__ == '__main__':

import oops2

@safe

def test():

oops2.oops()

test()

62.Self-study examples. Here are a few examples for you to study as time allows; for more, see follow-up books—such as Programming Python, from which these examples were borrowed or derived—and the Web:

63.# Find the largest Python source file in a single directory

64.

65.import os, glob

66.dirname = r'C:\Python33\Lib'

67.

68.allsizes = []

69.allpy = glob.glob(dirname + os.sep + '*.py')

70.for filename in allpy:

71. filesize = os.path.getsize(filename)

72. allsizes.append((filesize, filename))

73.

74.allsizes.sort()

75.print(allsizes[:2])

76.print(allsizes[-2:])

77.

78.

79.# Find the largest Python source file in an entire directory tree

80.

81.import sys, os, pprint

82.if sys.platform[:3] == 'win':

83. dirname = r'C:\Python33\Lib'

84.else:

85. dirname = '/usr/lib/python'

86.

87.allsizes = []

88.for (thisDir, subsHere, filesHere) in os.walk(dirname):

89. for filename in filesHere:

90. if filename.endswith('.py'):

91. fullname = os.path.join(thisDir, filename)

92. fullsize = os.path.getsize(fullname)

93. allsizes.append((fullsize, fullname))

94.

95.allsizes.sort()

96.pprint.pprint(allsizes[:2])

97.pprint.pprint(allsizes[-2:])

98.

99.

100. # Find the largest Python source file on the module import search path

101.

102. import sys, os, pprint

103. visited = {}

104. allsizes = []

105. for srcdir in sys.path:

106. for (thisDir, subsHere, filesHere) in os.walk(srcdir):

107. thisDir = os.path.normpath(thisDir)

108. if thisDir.upper() in visited:

109. continue

110. else:

111. visited[thisDir.upper()] = True

112. for filename in filesHere:

113. if filename.endswith('.py'):

114. pypath = os.path.join(thisDir, filename)

115. try:

116. pysize = os.path.getsize(pypath)

117. except:

118. print('skipping', pypath)

119. allsizes.append((pysize, pypath))

120.

121. allsizes.sort()

122. pprint.pprint(allsizes[:3])

123. pprint.pprint(allsizes[-3:])

124.

125.

126. # Sum columns in a text file separated by commas

127.

128. filename = 'data.txt'

129. sums = {}

130.

131. for line in open(filename):

132. cols = line.split(',')

133. nums = [int(col) for col in cols]

134. for (ix, num) in enumerate(nums):

135. sums[ix] = sums.get(ix, 0) + num

136.

137. for key in sorted(sums):

138. print(key, '=', sums[key])

139.

140.

141. # Similar to prior, but using lists instead of dictionaries for sums

142.

143. import sys

144. filename = sys.argv[1]

145. numcols = int(sys.argv[2])

146. totals = [0] * numcols

147.

148. for line in open(filename):

149. cols = line.split(',')

150. nums = [int(x) for x in cols]

151. totals = [(x + y) for (x, y) in zip(totals, nums)]

152.

153. print(totals)

154.

155.

156. # Test for regressions in the output of a set of scripts

157.

158. import os

159. testscripts = [dict(script='test1.py', args=''), # Or glob script/args dir

160. dict(script='test2.py', args='spam')]

161.

162. for testcase in testscripts:

163. commandline = '%(script)s %(args)s' % testcase

164. output = os.popen(commandline).read()

165. result = testcase['script'] + '.result'

166. if not os.path.exists(result):

167. open(result, 'w').write(output)

168. print('Created:', result)

169. else:

170. priorresult = open(result).read()

171. if output != priorresult:

172. print('FAILED:', testcase['script'])

173. print(output)

174. else:

175. print('Passed:', testcase['script'])

176.

177.

178. # Build GUI with tkinter (Tkinter in 2.X) with buttons that change color and grow

179.

180. from tkinter import * # Use Tkinter in 2.X

181. import random

182. fontsize = 25

183. colors = ['red', 'green', 'blue', 'yellow', 'orange', 'white', 'cyan', 'purple']

184.

185. def reply(text):

186. print(text)

187. popup = Toplevel()

188. color = random.choice(colors)

189. Label(popup, text='Popup', bg='black', fg=color).pack()

190. L.config(fg=color)

191.

192. def timer():

193. L.config(fg=random.choice(colors))

194. win.after(250, timer)

195.

196. def grow():

197. global fontsize

198. fontsize += 5

199. L.config(font=('arial', fontsize, 'italic'))

200. win.after(100, grow)

201.

202. win = Tk()

203. L = Label(win, text='Spam',

204. font=('arial', fontsize, 'italic'), fg='yellow', bg='navy',

205. relief=RAISED)

206. L.pack(side=TOP, expand=YES, fill=BOTH)

207. Button(win, text='press', command=(lambda: reply('red'))).pack(side=BOTTOM, fill=X)

208. Button(win, text='timer', command=timer).pack(side=BOTTOM, fill=X)

209. Button(win, text='grow', command=grow).pack(side=BOTTOM, fill=X)

210. win.mainloop()

211.

212.

213. # Similar to prior, but use classes so each window has own state information

214.

215. from tkinter import *

216. import random

217.

218. class MyGui:

219. """

220. A GUI with buttons that change color and make the label grow

221. """

222. colors = ['blue', 'green', 'orange', 'red', 'brown', 'yellow']

223.

224. def __init__(self, parent, title='popup'):

225. parent.title(title)

226. self.growing = False

227. self.fontsize = 10

228. self.lab = Label(parent, text='Gui1', fg='white', bg='navy')

229. self.lab.pack(expand=YES, fill=BOTH)

230. Button(parent, text='Spam', command=self.reply).pack(side=LEFT)

231. Button(parent, text='Grow', command=self.grow).pack(side=LEFT)

232. Button(parent, text='Stop', command=self.stop).pack(side=LEFT)

233.

234. def reply(self):

235. "change the button's color at random on Spam presses"

236. self.fontsize += 5

237. color = random.choice(self.colors)

238. self.lab.config(bg=color,

239. font=('courier', self.fontsize, 'bold italic'))

240.

241. def grow(self):

242. "start making the label grow on Grow presses"

243. self.growing = True

244. self.grower()

245.

246. def grower(self):

247. if self.growing:

248. self.fontsize += 5

249. self.lab.config(font=('courier', self.fontsize, 'bold'))

250. self.lab.after(500, self.grower)

251.

252. def stop(self):

253. "stop the button growing on Stop presses"

254. self.growing = False

255.

256. class MySubGui(MyGui):

257. colors = ['black', 'purple'] # Customize to change color choices

258.

259. MyGui(Tk(), 'main')

260. MyGui(Toplevel())

261. MySubGui(Toplevel())

262. mainloop()

263.

264.

265. # Email inbox scanning and maintenance utility

266.

267. """

268. scan pop email box, fetching just headers, allowing

269. deletions without downloading the complete message

270. """

271.

272. import poplib, getpass, sys

273.

274. mailserver = 'your pop email server name here' # pop.server.net

275. mailuser = 'your pop email user name here'

276. mailpasswd = getpass.getpass('Password for %s?' % mailserver)

277.

278. print('Connecting...')

279. server = poplib.POP3(mailserver)

280. server.user(mailuser)

281. server.pass_(mailpasswd)

282.

283. try:

284. print(server.getwelcome())

285. msgCount, mboxSize = server.stat()

286. print('There are', msgCount, 'mail messages, size ', mboxSize)

287. msginfo = server.list()

288. print(msginfo)

289. for i in range(msgCount):

290. msgnum = i+1

291. msgsize = msginfo[1][i].split()[1]

292. resp, hdrlines, octets = server.top(msgnum, 0) # Get hdrs only

293. print('-'*80)

294. print('[%d: octets=%d, size=%s]' % (msgnum, octets, msgsize))

295. for line in hdrlines: print(line)

296.

297. if input('Print?') in ['y', 'Y']:

298. for line in server.retr(msgnum)[1]: print(line) # Get whole msg

299. if input('Delete?') in ['y', 'Y']:

300. print('deleting')

301. server.dele(msgnum) # Delete on srvr

302. else:

303. print('skipping')

304. finally:

305. server.quit() # Make sure we unlock mbox

306. input('Bye.') # Keep window up on Windows

307.

308.

309. # CGI server-side script to interact with a web browser

310.

311. #!/usr/bin/python

312. import cgi

313. form = cgi.FieldStorage() # Parse form data

314. print("Content-type: text/html\n") # hdr plus blank line

315. print("<HTML>")

316. print("<title>Reply Page</title>") # HTML reply page

317. print("<BODY>")

318. if not 'user' in form:

319. print("<h1>Who are you?</h1>")

320. else:

321. print("<h1>Hello <i>%s</i>!</h1>" % cgi.escape(form['user'].value))

322. print("</BODY></HTML>")

323.

324.

325. # Database script to populate a shelve with Python objects

326.

327. # see also Chapter 28 shelve and Chapter 31 pickle examples

328.

329. rec1 = {'name': {'first': 'Bob', 'last': 'Smith'},

330. 'job': ['dev', 'mgr'],

331. 'age': 40.5}

332.

333. rec2 = {'name': {'first': 'Sue', 'last': 'Jones'},

334. 'job': ['mgr'],

335. 'age': 35.0}

336.

337. import shelve

338. db = shelve.open('dbfile')

339. db['bob'] = rec1

340. db['sue'] = rec2

341. db.close()

342.

343.

344. # Database script to print and update shelve created in prior script

345.

346. import shelve

347. db = shelve.open('dbfile')

348. for key in db:

349. print(key, '=>', db[key])

350.

351. bob = db['bob']

352. bob['age'] += 1

353. db['bob'] = bob

354. db.close()

355.

356.

357. # Database script to populate and query a MySql database

358.

359. from MySQLdb import Connect

360. conn = Connect(host='localhost', user='root', passwd='XXXXXXX')

361. curs = conn.cursor()

362. try:

363. curs.execute('drop database testpeopledb')

364. except:

365. pass # Did not exist

366.

367. curs.execute('create database testpeopledb')

368. curs.execute('use testpeopledb')

369. curs.execute('create table people (name char(30), job char(10), pay int(4))')

370.

371. curs.execute('insert people values (%s, %s, %s)', ('Bob', 'dev', 50000))

372. curs.execute('insert people values (%s, %s, %s)', ('Sue', 'dev', 60000))

373. curs.execute('insert people values (%s, %s, %s)', ('Ann', 'mgr', 40000))

374.

375. curs.execute('select * from people')

376. for row in curs.fetchall():

377. print(row)

378.

379. curs.execute('select * from people where name = %s', ('Bob',))

380. print(curs.description)

381. colnames = [desc[0] for desc in curs.description]

382. while True:

383. print('-' * 30)

384. row = curs.fetchone()

385. if not row: break

386. for (name, value) in zip(colnames, row):

387. print('%s => %s' % (name, value))

388.

389. conn.commit() # Save inserted records

390.

391.

392. # Fetch and open/play a file by FTP

393.

394. import webbrowser, sys

395. from ftplib import FTP # Socket-based FTP tools

396. from getpass import getpass # Hidden password input

397. if sys.version[0] == '2': input = raw_input # 2.X compatibility

398.

399. nonpassive = False # Force active mode FTP for server?

400. filename = input('File?') # File to be downloaded

401. dirname = input('Dir? ') or '.' # Remote directory to fetch from

402. sitename = input('Site?') # FTP site to contact

403. user = input('User?') # Use () for anonymous

404. if not user:

405. userinofo = ()

406. else:

407. from getpass import getpass # Hidden password input

408. userinfo = (user, getpass('Pswd?'))

409.

410. print('Connecting...')

411. connection = FTP(sitename) # Connect to FTP site

412. connection.login(*userinfo) # Default is anonymous login

413. connection.cwd(dirname) # Xfer 1k at a time to localfile

414. if nonpassive: # Force active FTP if server requires

415. connection.set_pasv(False)

416.

417. print('Downloading...')

418. localfile = open(filename, 'wb') # Local file to store download

419. connection.retrbinary('RETR ' + filename, localfile.write, 1024)

420. connection.quit()

421. localfile.close()

422.

423. print('Playing...')

424. webbrowser.open(filename)