html/ 0000755 0001750 0001750 00000000000 11544706051 011421 5 ustar downey downey html/chap09.html 0000644 0001750 0001750 00000047447 11544706047 013420 0 ustar downey downey
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 9Tuples9.1 Mutability and tuplesSo far, you have seen two compound types: strings, which are made up of characters; and lists, which are made up of elements of any type. One of the differences we noted is that the elements of a list can be modified, but the characters in a string cannot. In other words, strings are immutable and lists are mutable. There is another type in Python called a tuple that is similar to a list except that it is immutable. Syntactically, a tuple is a comma-separated list of values: >>> tuple = 'a', 'b', 'c', 'd', 'e'
Although it is not necessary, it is conventional to enclose tuples in parentheses: >>> tuple = ('a', 'b', 'c', 'd', 'e')
To create a tuple with a single element, we have to include the final comma: >>> t1 = ('a',)
Without the comma, Python treats ('a') as a string in parentheses: >>> t2 = ('a')
Syntax issues aside, the operations on tuples are the same as the operations on lists. The index operator selects an element from a tuple. >>> tuple = ('a', 'b', 'c', 'd', 'e')
And the slice operator selects a range of elements. >>> tuple[1:3]
But if we try to modify one of the elements of the tuple, we get an error: >>> tuple[0] = 'A'
Of course, even if we can't modify the elements of a tuple, we can replace it with a different tuple: >>> tuple = ('A',) + tuple[1:]
9.2 Tuple assignmentOnce in a while, it is useful to swap the values of two variables. With conventional assignment statements, we have to use a temporary variable. For example, to swap a and b: >>> temp = a
If we have to do this often, this approach becomes cumbersome. Python provides a form of tuple assignment that solves this problem neatly: >>> a, b = b, a
The left side is a tuple of variables; the right side is a tuple of values. Each value is assigned to its respective variable. All the expressions on the right side are evaluated before any of the assignments. This feature makes tuple assignment quite versatile. Naturally, the number of variables on the left and the number of values on the right have to be the same: >>> a, b, c, d = 1, 2, 3
9.3 Tuples as return valuesFunctions can return tuples as return values. For example, we could write a function that swaps two parameters: def swap(x, y):
Then we can assign the return value to a tuple with two variables: a, b = swap(a, b)
In this case, there is no great advantage in making swap a function. In fact, there is a danger in trying to encapsulate swap, which is the following tempting mistake: def swap(x, y): # incorrect version
If we call this function like this: swap(a, b)
then a and x are aliases for the same value. Changing x inside swap makes x refer to a different value, but it has no effect on a in __main__. Similarly, changing y has no effect on b. This function runs without producing an error message, but it doesn't do what we intended. This is an example of a semantic error. As an exercise, draw a state diagram for this function so that you can see why it doesn't work. 9.4 Random numbersMost computer programs do the same thing every time they execute, so they are said to be deterministic. Determinism is usually a good thing, since we expect the same calculation to yield the same result. For some applications, though, we want the computer to be unpredictable. Games are an obvious example, but there are more. Making a program truly nondeterministic turns out to be not so easy, but there are ways to make it at least seem nondeterministic. One of them is to generate random numbers and use them to determine the outcome of the program. Python provides a built-in function that generates pseudorandom numbers, which are not truly random in the mathematical sense, but for our purposes they will do. The random module contains a function called random that returns a floating-point number between 0.0 and 1.0. Each time you call random, you get the next number in a long series. To see a sample, run this loop: import random
To generate a random number between 0.0 and an upper bound like high, multiply x by high. As an exercise, generate a random number between low and high. As an additional exercise, generate a random integer between low and high, including both end points. 9.5 List of random numbersThe first step is to generate a list of random values. randomList takes an integer argument and returns a list of random numbers with the given length. It starts with a list of n zeros. Each time through the loop, it replaces one of the elements with a random number. The return value is a reference to the complete list: def randomList(n):
We'll test this function with a list of eight elements. For purposes of debugging, it is a good idea to start small. >>> randomList(8)
The numbers generated by random are supposed to be distributed uniformly, which means that every value is equally likely. If we divide the range of possible values into equal-sized "buckets," and count the number of times a random value falls in each bucket, we should get roughly the same number in each. We can test this theory by writing a program to divide the range into buckets and count the number of values in each. 9.6 CountingA good approach to problems like this is to divide the problem into subproblems and look for subproblems that fit a computational pattern you have seen before. In this case, we want to traverse a list of numbers and count the number of times a value falls in a given range. That sounds familiar. In Section 7.8, we wrote a program that traversed a string and counted the number of times a given letter appeared. So, we can proceed by copying the old program and adapting it for the current problem. The original program was: count = 0
The first step is to replace fruit with t and char with num. That doesn't change the program; it just makes it more readable. The second step is to change the test. We aren't interested in finding letters. We want to see if num is between the given values low and high. count = 0
The last step is to encapsulate this code in a function called inBucket. The parameters are the list and the values low and high. def inBucket(t, low, high):
By copying and modifying an existing program, we were able to write this function quickly and save a lot of debugging time. This development plan is called pattern matching. If you find yourself working on a problem you have solved before, reuse the solution. 9.7 Many bucketsAs the number of buckets increases, inBucket gets a little unwieldy. With two buckets, it's not bad: low = inBucket(a, 0.0, 0.5)
But with four buckets it is getting cumbersome. bucket1 = inBucket(a, 0.0, 0.25)
There are two problems. One is that we have to make up new variable names for each result. The other is that we have to compute the range for each bucket. We'll solve the second problem first. If the number of buckets is numBuckets, then the width of each bucket is 1.0 / numBuckets. We'll use a loop to compute the range of each bucket. The loop variable, i, counts from 0 to numBuckets-1: bucketWidth = 1.0 / numBuckets
To compute the low end of each bucket, we multiply the loop variable by the bucket width. The high end is just a bucketWidth away. With numBuckets = 8, the output is: 0.0 to 0.125
You can confirm that each bucket is the same width, that they don't overlap, and that they cover the entire range from 0.0 to 1.0. Now back to the first problem. We need a way to store eight integers, using the loop variable to indicate one at a time. By now you should be thinking, "List!" We have to create the bucket list outside the loop, because we only want to do it once. Inside the loop, we'll call inBucket repeatedly and update the i-eth element of the list: numBuckets = 8
With a list of 1000 values, this code produces this bucket list: [138, 124, 128, 118, 130, 117, 114, 131]
These numbers are fairly close to 125, which is what we expected. At least, they are close enough that we can believe the random number generator is working. As an exercise, test this function with some longer lists, and see if the number of values in each bucket tends to level off. 9.8 A single-pass solutionAlthough this program works, it is not as efficient as it could be. Every time it calls inBucket, it traverses the entire list. As the number of buckets increases, that gets to be a lot of traversals. It would be better to make a single pass through the list and compute for each value the index of the bucket in which it falls. Then we can increment the appropriate counter. In the previous section we took an index, i, and multiplied it by the bucketWidth to find the lower bound of a given bucket. Now we want to take a value in the range 0.0 to 1.0 and find the index of the bucket where it falls. Since this problem is the inverse of the previous problem, we might guess that we should divide by bucketWidth instead of multiplying. That guess is correct.
Since bucketWidth = 1.0 / numBuckets, dividing by bucketWidth is the same as multiplying by numBuckets. If we
multiply a number in the range 0.0 to 1.0 by numBuckets, we get
a number in the range from 0.0 to numBuckets. If we round that
number to the next lower integer, we get exactly what we are looking
for numBuckets = 8
We used the int function to convert a floating-point number to an integer. Is it possible for this calculation to produce an index that is out of range (either negative or greater than len(buckets)-1)? A list like buckets that contains counts of the number of values in each range is called a histogram. As an exercise, write a function called histogram that takes a list and a number of buckets as arguments and returns a histogram with the given number of buckets. 9.9 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 2Variables, expressions and statements2.1 Values and types
A value is one of the fundamental things These values belong to different types: 2 is an integer, and 'Hello, World!' is a string, so-called because it contains a "string" of letters. You (and the interpreter) can identify strings because they are enclosed in quotation marks. The print statement also works for integers. >>> print 4
If you are not sure what type a value has, the interpreter can tell you. >>> type('Hello, World!')
Not surprisingly, strings belong to the type str and integers belong to the type int. Less obviously, numbers with a decimal point belong to a type called float, because these numbers are represented in a format called floating-point. >>> type(3.2)
What about values like '17' and '3.2'? They look like numbers, but they are in quotation marks like strings. >>> type('17')
They're strings. When you type a large integer, you might be tempted to use commas between groups of three digits, as in 1,000,000. This is not a legal integer in Python, but it is a legal expression: >>> print 1,000,000
Well, that's not what we expected at all! Python interprets 1,000,000 as a comma-separated list of three integers, which it prints consecutively. This is the first example we have seen of a semantic error: the code runs without producing an error message, but it doesn't do the "right" thing. 2.2 VariablesOne of the most powerful features of a programming language is the ability to manipulate variables. A variable is a name that refers to a value. The assignment statement creates new variables and gives them values: >>> message = "What's up, Doc?"
This example makes three assignments. The first assigns the string "What's up, Doc?" to a new variable named message. The second gives the integer 17 to n, and the third gives the floating-point number 3.14159 to pi. Notice that the first statement uses double quotes to enclose the string. In general, single and double quotes do the same thing, but if the string contains a single quote (or an apostrophe, which is the same character), you have to use double quotes to enclose it. A common way to represent variables on paper is to write the name with an arrow pointing to the variable's value. This kind of figure is called a state diagram because it shows what state each of the variables is in (think of it as the variable's state of mind). This diagram shows the result of the assignment statements:
The print statement also works with variables. >>> print message
In each case the result is the value of the variable. Variables also have types; again, we can ask the interpreter what they are. >>> type(message)
The type of a variable is the type of the value it refers to. 2.3 Variable names and keywords
Programmers generally choose names for their variables that
are meaningful Variable names can be arbitrarily long. They can contain both letters and numbers, but they have to begin with a letter. Although it is legal to use uppercase letters, by convention we don't. If you do, remember that case matters. Bruce and bruce are different variables. The underscore character (_) can appear in a name. It is often used in names with multiple words, such as my_name or price_of_tea_in_china. If you give a variable an illegal name, you get a syntax error: >>> 76trombones = 'big parade'
76trombones is illegal because it does not begin with a letter. more$ is illegal because it contains an illegal character, the dollar sign. But what's wrong with class? It turns out that class is one of the Python keywords. Keywords define the language's rules and structure, and they cannot be used as variable names. Python has twenty-nine keywords: and def exec if not return
You might want to keep this list handy. If the interpreter complains about one of your variable names and you don't know why, see if it is on this list. 2.4 StatementsA statement is an instruction that the Python interpreter can execute. We have seen two kinds of statements: print and assignment. When you type a statement on the command line, Python executes it and displays the result, if there is one. The result of a print statement is a value. Assignment statements don't produce a result. A script usually contains a sequence of statements. If there is more than one statement, the results appear one at a time as the statements execute. For example, the script print 1
produces the output 1
Again, the assignment statement produces no output. 2.5 Evaluating expressionsAn expression is a combination of values, variables, and operators. If you type an expression on the command line, the interpreter evaluates it and displays the result: >>> 1 + 1
Although expressions contain values, variables, and operators, not every expression contains all of these elements. A value all by itself is considered an expression, and so is a variable. >>> 17
Confusingly, evaluating an expression is not quite the same thing as printing a value. >>> message = 'Hello, World!'
When the Python interpreter displays the value of an expression, it uses the same format you would use to enter a value. In the case of strings, that means that it includes the quotation marks. But if you use a print statement, Python displays the contents of the string without the quotation marks. In a script, an expression all by itself is a legal statement, but it doesn't do anything. The script 17
produces no output at all. How would you change the script to display the values of these four expressions? 2.6 Operators and operandsOperators are special symbols that represent computations like addition and multiplication. The values the operator uses are called operands. The following are all legal Python expressions whose meaning is more or less clear: 20+32 hour-1 hour*60+minute minute/60 5**2 (5+9)*(15-7)
The symbols +, -, and /, and the use of parenthesis for grouping, mean in Python what they mean in mathematics. The asterisk (*) is the symbol for multiplication, and ** is the symbol for exponentiation. When a variable name appears in the place of an operand, it is replaced with its value before the operation is performed. Addition, subtraction, multiplication, and exponentiation all do what you expect, but you might be surprised by division. The following operation has an unexpected result: >>> minute = 59
The value of minute is 59, and in conventional arithmetic 59 divided by 60 is 0.98333, not 0. The reason for the discrepancy is that Python is performing integer division. When both of the operands are integers, the result must also be an integer, and by convention, integer division always rounds down, even in cases like this where the next integer is very close. A possible solution to this problem is to calculate a percentage rather than a fraction: >>> minute*100/60
Again the result is rounded down, but at least now the answer is approximately correct. Another alternative is to use floating-point division, which we get to in Chapter 3. 2.7 Order of operationsWhen more than one operator appears in an expression, the order of evaluation depends on the rules of precedence. Python follows the same precedence rules for its mathematical operators that mathematics does. The acronym PEMDAS is a useful way to remember the order of operations:
2.8 Operations on stringsIn general, you cannot perform mathematical operations on strings, even if the strings look like numbers. The following are illegal (assuming that message has type string): message-1 'Hello'/123 message*'Hello' '15'+2
Interestingly, the + operator does work with strings, although it does not do exactly what you might expect. For strings, the + operator represents concatenation, which means joining the two operands by linking them end-to-end. For example: fruit = 'banana'
The output of this program is banana nut bread. The space before the word nut is part of the string, and is necessary to produce the space between the concatenated strings. The * operator also works on strings; it performs repetition. For example, 'Fun'*3 is 'FunFunFun'. One of the operands has to be a string; the other has to be an integer. On one hand, this interpretation of + and * makes sense by analogy with addition and multiplication. Just as 4*3 is equivalent to 4+4+4, we expect 'Fun'*3 to be the same as 'Fun'+'Fun'+'Fun', and it is. On the other hand, there is a significant way in which string concatenation and repetition are different from integer addition and multiplication. Can you think of a property that addition and multiplication have that string concatenation and repetition do not? 2.9 Composition
So far, we have looked at the elements of a program One of the most useful features of programming languages is their ability to take small building blocks and compose them. For example, we know how to add numbers and we know how to print; it turns out we can do both at the same time: >>> print 17 + 3
In reality, the addition has to happen before the printing, so the actions aren't actually happening at the same time. The point is that any expression involving numbers, strings, and variables can be used inside a print statement. You've already seen an example of this: print 'Number of minutes since midnight: ', hour*60+minute
You can also put arbitrary expressions on the right-hand side of an assignment statement: percentage = (minute * 100) / 60
This ability may not seem impressive now, but you will see other examples where composition makes it possible to express complex computations neatly and concisely. Warning: There are limits on where you can use certain expressions. For example, the left-hand side of an assignment statement has to be a variable name, not an expression. So, the following is illegal: minute+1 = hour. 2.10 CommentsAs programs get bigger and more complicated, they get more difficult to read. Formal languages are dense, and it is often difficult to look at a piece of code and figure out what it is doing, or why. For this reason, it is a good idea to add notes to your programs to explain in natural language what the program is doing. These notes are called comments, and they are marked with the # symbol: # compute the percentage of the hour that has elapsed
In this case, the comment appears on a line by itself. You can also put comments at the end of a line: percentage = (minute * 100) / 60 # caution: integer division
Everything from the # to the end of the line is ignored This sort of comment is less necessary if you use the integer division operation, //. It has the same effect as the division operator * Note, but it signals that the effect is deliberate. percentage = (minute * 100) // 60
The integer division operator is like a comment that says, "I know this is integer division, and I like it that way!" 2.11 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 8Lists
A list is an ordered set of values, where each value is
identified by an index. The values that make up a list are
called its elements. Lists are similar to strings, which are
ordered sets of characters, except that the elements of a list can
have any type. Lists and strings 8.1 List valuesThere are several ways to create a new list; the simplest is to enclose the elements in square brackets ([ and ]): [10, 20, 30, 40]
The first example is a list of four integers. The second is a list of three strings. The elements of a list don't have to be the same type. The following list contains a string, a float, an integer, and (mirabile dictu) another list: ["hello", 2.0, 5, [10, 20]]
A list within another list is said to be nested. Lists that contain consecutive integers are common, so Python provides a simple way to create them: >>> range(1,5)
The range function takes two arguments and returns a list that contains all the integers from the first to the second, including the first but not including the second! There are two other forms of range. With a single argument, it creates a list that starts at 0: >>> range(10)
If there is a third argument, it specifies the space between successive values, which is called the step size. This example counts from 1 to 10 by steps of 2: >>> range(1, 10, 2)
Finally, there is a special list that contains no elements. It is called the empty list, and it is denoted []. With all these ways to create lists, it would be disappointing if we couldn't assign list values to variables or pass lists as arguments to functions. We can. vocabulary = ["ameliorate", "castigate", "defenestrate"]
8.2 Accessing elements
The syntax for accessing the elements of a list is the same as the
syntax for accessing the characters of a string print numbers[0]
The bracket operator can appear anywhere in an expression. When it appears on the left side of an assignment, it changes one of the elements in the list, so the one-eth element of numbers, which used to be 123, is now 5. Any integer expression can be used as an index: >>> numbers[3-2]
If you try to read or write an element that does not exist, you get a runtime error: >>> numbers[2] = 5
If an index has a negative value, it counts backward from the end of the list: >>> numbers[-1]
numbers[-1] is the last element of the list, numbers[-2] is the second to last, and numbers[-3] doesn't exist. It is common to use a loop variable as a list index. horsemen = ["war", "famine", "pestilence", "death"]
This while loop counts from 0 to 4. When the loop variable i is 4, the condition fails and the loop terminates. So the body of the loop is only executed when i is 0, 1, 2, and 3. Each time through the loop, the variable i is used as an index into the list, printing the i-eth element. This pattern of computation is called a list traversal. 8.3 List lengthThe function len returns the length of a list. It is a good idea to use this value as the upper bound of a loop instead of a constant. That way, if the size of the list changes, you won't have to go through the program changing all the loops; they will work correctly for any size list: horsemen = ["war", "famine", "pestilence", "death"]
The last time the body of the loop is executed, i is len(horsemen) - 1, which is the index of the last element. When i is equal to len(horsemen), the condition fails and the body is not executed, which is a good thing, because len(horsemen) is not a legal index. Although a list can contain another list, the nested list still counts as a single element. The length of this list is four: ['spam!', 1, ['Brie', 'Roquefort', 'Pol le Veq'], [1, 2, 3]]
As an exercise, write a loop that traverses the previous list and prints the length of each element. What happens if you send an integer to len? 8.4 List membershipin is a boolean operator that tests membership in a sequence. We used it in Section 7.10 with strings, but it also works with lists and other sequences: >>> horsemen = ['war', 'famine', 'pestilence', 'death']
Since "pestilence" is a member of the horsemen list, the in operator returns true. Since "debauchery" is not in the list, in returns false. We can use the not in combination with in to test whether an element is not a member of a list: >>> 'debauchery' not in horsemen
8.5 Lists and for loopsThe for loop we saw in Section 7.3 also works with lists. The generalized syntax of a for loop is: for VARIABLE in LIST:
This statement is equivalent to: i = 0
The for loop is more concise because we can eliminate the loop variable, i. Here is the previous loop written with a for loop. for horseman in horsemen:
It almost reads like English: "For (every) horseman in (the list of) horsemen, print (the name of the) horseman." Any list expression can be used in a for loop: for number in range(20):
The first example prints all the even numbers between zero and nineteen. The second example expresses enthusiasm for various fruits. 8.6 List operationsThe + operator concatenates lists: >>> a = [1, 2, 3]
Similarly, the * operator repeats a list a given number of times: >>> [0] * 4
The first example repeats [0] four times. The second example repeats the list [1, 2, 3] three times. 8.7 List slicesThe slice operations we saw in Section 7.4 also work on lists: >>> list = ['a', 'b', 'c', 'd', 'e', 'f']
If you omit the first index, the slice starts at the beginning. If you omit the second, the slice goes to the end. So if you omit both, the slice is really a copy of the whole list. >>> list[:]
8.8 Lists are mutableUnlike strings, lists are mutable, which means we can change their elements. Using the bracket operator on the left side of an assignment, we can update one of the elements: >>> fruit = ["banana", "apple", "quince"]
With the slice operator we can update several elements at once: >>> list = ['a', 'b', 'c', 'd', 'e', 'f']
We can also remove elements from a list by assigning the empty list to them: >>> list = ['a', 'b', 'c', 'd', 'e', 'f']
And we can add elements to a list by squeezing them into an empty slice at the desired location: >>> list = ['a', 'd', 'f']
8.9 List deletionUsing slices to delete list elements can be awkward, and therefore error-prone. Python provides an alternative that is more readable. del removes an element from a list: >>> a = ['one', 'two', 'three']
As you might expect, del handles negative indices and causes a runtime error if the index is out of range. You can use a slice as an index for del: >>> list = ['a', 'b', 'c', 'd', 'e', 'f']
As usual, slices select all the elements up to, but not including, the second index. 8.10 Objects and valuesIf we execute these assignment statements, a = "banana"
we know that a and b will refer to a string with the letters "banana". But we can't tell whether they point to the same string. There are two possible states:
In one case, a and b refer to two different things that
have the same value. In the second case, they refer to the same
thing. These "things" have names Every object has a unique identifier, which we can obtain with the id function. By printing the identifier of a and b, we can tell whether they refer to the same object. >>> id(a)
In fact, we get the same identifier twice, which means that Python only created one string, and both a and b refer to it. Interestingly, lists behave differently. When we create two lists, we get two objects: >>> a = [1, 2, 3]
So the state diagram looks like this:
a and b have the same value but do not refer to the same object. 8.11 AliasingSince variables refer to objects, if we assign one variable to another, both variables refer to the same object: >>> a = [1, 2, 3]
In this case, the state diagram looks like this:
Because the same list has two different names, a and b, we say that it is aliased. Changes made with one alias affect the other: >>> b[0] = 5
Although this behavior can be useful, it is sometimes unexpected or undesirable. In general, it is safer to avoid aliasing when you are working with mutable objects. Of course, for immutable objects, there's no problem. That's why Python is free to alias strings when it sees an opportunity to economize. 8.12 Cloning listsIf we want to modify a list and also keep a copy of the original, we need to be able to make a copy of the list itself, not just the reference. This process is sometimes called cloning, to avoid the ambiguity of the word "copy." The easiest way to clone a list is to use the slice operator: >>> a = [1, 2, 3]
Taking any slice of a creates a new list. In this case the slice happens to consist of the whole list. Now we are free to make changes to b without worrying about a: >>> b[0] = 5
As an exercise, draw a state diagram for a and b before and after this change. 8.13 List parametersPassing a list as an argument actually passes a reference to the list, not a copy of the list. For example, the function head takes a list as an argument and returns the first element: def head(list):
Here's how it is used: >>> numbers = [1, 2, 3]
The parameter list and the variable numbers are aliases for the same object. The state diagram looks like this:
Since the list object is shared by two frames, we drew it between them. If a function modifies a list parameter, the caller sees the change. For example, deleteHead removes the first element from a list: def deleteHead(list):
Here's how deleteHead is used: >>> numbers = [1, 2, 3]
If a function returns a list, it returns a reference to the list. For example, tail returns a list that contains all but the first element of the given list: def tail(list):
Here's how tail is used: >>> numbers = [1, 2, 3]
Because the return value was created with the slice operator, it is a new list. Creating rest, and any subsequent changes to rest, have no effect on numbers. 8.14 Nested listsA nested list is a list that appears as an element in another list. In this list, the three-eth element is a nested list: >>> list = ["hello", 2.0, 5, [10, 20]]
If we print list[3], we get [10, 20]. To extract an element from the nested list, we can proceed in two steps: >>> elt = list[3]
Or we can combine them: >>> list[3][1]
Bracket operators evaluate from left to right, so this expression gets the three-eth element of list and extracts the one-eth element from it. 8.15 MatricesNested lists are often used to represent matrices. For example, the matrix:
might be represented as: >>> matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
matrix is a list with three elements, where each element is a row of the matrix. We can select an entire row from the matrix in the usual way: >>> matrix[1]
Or we can extract a single element from the matrix using the double-index form: >>> matrix[1][1]
The first index selects the row, and the second index selects the column. Although this way of representing matrices is common, it is not the only possibility. A small variation is to use a list of columns instead of a list of rows. Later we will see a more radical alternative using a dictionary. 8.16 Strings and listsTwo of the most useful functions in the string module involve lists of strings. The split function breaks a string into a list of words. By default, any number of whitespace characters is considered a word boundary: >>> import string
An optional argument called a delimiter can be used to specify which characters to use as word boundaries. The following example uses the string ai as the delimiter: >>> string.split(song, 'ai')
Notice that the delimiter doesn't appear in the list. The join function is the inverse of split. It takes a list of strings and concatenates the elements with a space between each pair: >>> list = ['The', 'rain', 'in', 'Spain...']
Like split, join takes an optional delimiter that is inserted between elements: >>> string.join(list, '_')
As an exercise, describe the relationship between string.join(string.split(song)) and song. Are they the same for all strings? When would they be different? 8.17 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 18Stacks18.1 Abstract data typesThe data types you have seen so far are all concrete, in the sense that we have completely specified how they are implemented. For example, the Card class represents a card using two integers. As we discussed at the time, that is not the only way to represent a card; there are many alternative implementations. An abstract data type, or ADT, specifies a set of operations (or methods) and the semantics of the operations (what they do), but it does not specify the implementation of the operations. That's what makes it abstract. Why is that useful?
When we talk about ADTs, we often distinguish the code that uses the ADT, called the client code, from the code that implements the ADT, called the provider code. 18.2 The Stack ADTIn this chapter, we will look at one common ADT, the stack. A stack is a collection, meaning that it is a data structure that contains multiple elements. Other collections we have seen include dictionaries and lists. An ADT is defined by the operations that can be performed on it, which is called an interface. The interface for a stack consists of these operations:
A stack is sometimes called a "last in, first out" or LIFO data structure, because the last item added is the first to be removed. 18.3 Implementing stacks with Python listsThe list operations that Python provides are similar to the operations that define a stack. The interface isn't exactly what it is supposed to be, but we can write code to translate from the Stack ADT to the built-in operations. This code is called an implementation of the Stack ADT. In general, an implementation is a set of methods that satisfy the syntactic and semantic requirements of an interface. Here is an implementation of the Stack ADT that uses a Python list: class Stack :
A Stack object contains an attribute named items that is a list of items in the stack. The initialization method sets items to the empty list. To push a new item onto the stack, push appends it onto items. To pop an item off the stack, pop uses the homonymous * Note list method to remove and return the last item on the list. Finally, to check if the stack is empty, isEmpty compares items to the empty list. An implementation like this, in which the methods consist of simple invocations of existing methods, is called a veneer. In real life, veneer is a thin coating of good quality wood used in furniture-making to hide lower quality wood underneath. Computer scientists use this metaphor to describe a small piece of code that hides the details of an implementation and provides a simpler, or more standard, interface. 18.4 Pushing and poppingA stack is a generic data structure, which means that we can add any type of item to it. The following example pushes two integers and a string onto the stack: >>> s = Stack()
We can use isEmpty and pop to remove and print all of the items on the stack: while not s.isEmpty() :
The output is + 45 54. In other words, we just used a stack to print the items backward! Granted, it's not the standard format for printing a list, but by using a stack, it was remarkably easy to do. You should compare this bit of code to the implementation of printBackward in Section 17.4. There is a natural parallel between the recursive version of printBackward and the stack algorithm here. The difference is that printBackward uses the runtime stack to keep track of the nodes while it traverses the list, and then prints them on the way back from the recursion. The stack algorithm does the same thing, except that it uses a Stack object instead of the runtime stack. 18.5 Using a stack to evaluate postfixIn most programming languages, mathematical expressions are written with the operator between the two operands, as in 1+2. This format is called infix. An alternative used by some calculators is called postfix. In postfix, the operator follows the operands, as in 1 2 +. The reason postfix is sometimes useful is that there is a natural way to evaluate a postfix expression using a stack:
As an exercise, apply this algorithm to the expression 1 2 + 3 *.
This example demonstrates one of the advantages of postfix As an exercise, write a postfix expression that is equivalent to 1 + 2 * 3. 18.6 Parsing
To implement the previous algorithm, we need
to be able to traverse a string and break it into operands and
operators. This process is an example of parsing, and the
results Python provides a split method in both the string and re (regular expression) modules. The function string.split splits a string into a list using a single character as a delimiter. For example: >>> import string
In this case, the delimiter is the space character, so the string is split at each space. The function re.split is more powerful, allowing us to provide a regular expression instead of a delimiter. A regular expression is a way of specifying a set of strings. For example, [A-z] is the set of all letters and [0-9] is the set of all digits. The ^ operator negates a set, so [^0-9] is the set of every character that is not a digit, which is exactly the set we want to use to split up postfix expressions: >>> import re
Notice that the order of the arguments is different from string.split; the delimiter comes before the string. The resulting list includes the operands 123 and 456 and the operators * and /. It also includes two empty strings that are inserted as "phantom operands," whenever an operator appears without a number before or after it. 18.7 Evaluating postfixTo evaluate a postfix expression, we will use the parser from the previous section and the algorithm from the section before that. To keep things simple, we'll start with an evaluator that only implements the operators + and *: def evalPostfix(expr):
The first condition takes care of spaces and empty strings. The next two conditions handle operators. We assume, for now, that anything else must be an operand. Of course, it would be better to check for erroneous input and report an error message, but we'll get to that later. Let's test it by evaluating the postfix form of (56+47)*2: >>> print evalPostfix ("56 47 + 2 *")
18.8 Clients and providers
One of the fundamental goals of an ADT is to separate the
interests of the provider, who writes the code that implements
the ADT, and the client, who uses the ADT.
The provider only has to worry
about whether the implementation is correct Conversely, the client assumes that the implementation of the ADT is correct and doesn't worry about the details. When you are using one of Python's built-in types, you have the luxury of thinking exclusively as a client. Of course, when you implement an ADT, you also have to write client code to test it. In that case, you play both roles, which can be confusing. You should make some effort to keep track of which role you are playing at any moment. 18.9 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 10Dictionaries
The compound types you have learned about Dictionaries are similar to other compound types except that they can use any immutable type as an index. As an example, we will create a dictionary to translate English words into Spanish. For this dictionary, the indices are strings. One way to create a dictionary is to start with the empty dictionary and add elements. The empty dictionary is denoted {}: >>> eng2sp = {}
The first assignment creates a dictionary named eng2sp; the other assignments add new elements to the dictionary. We can print the current value of the dictionary in the usual way: >>> print eng2sp
The elements of a dictionary appear in a comma-separated list. Each entry contains an index and a value separated by a colon. In a dictionary, the indices are called keys, so the elements are called key-value pairs. Another way to create a dictionary is to provide a list of key-value pairs using the same syntax as the previous output: >>> eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
If we print the value of eng2sp again, we get a surprise: >>> print eng2sp
The key-value pairs are not in order! Fortunately, there is no reason to care about the order, since the elements of a dictionary are never indexed with integer indices. Instead, we use the keys to look up the corresponding values: >>> print eng2sp['two']
The key 'two' yields the value 'dos' even though it appears in the third key-value pair. 10.1 Dictionary operationsThe del statement removes a key-value pair from a dictionary. For example, the following dictionary contains the names of various fruits and the number of each fruit in stock: >>> inventory = {'apples': 430, 'bananas': 312, 'oranges': 525,
If someone buys all of the pears, we can remove the entry from the dictionary: >>> del inventory['pears']
Or if we're expecting more pears soon, we might just change the value associated with pears: >>> inventory['pears'] = 0
The len function also works on dictionaries; it returns the number of key-value pairs: >>> len(inventory)
10.2 Dictionary methods
A method is similar to a function >>> eng2sp.keys()
This form of dot notation specifies the name of the function, keys, and the name of the object to apply the function to, eng2sp. The parentheses indicate that this method has no parameters. A method call is called an invocation; in this case, we would say that we are invoking keys on the object eng2sp. The values method is similar; it returns a list of the values in the dictionary: >>> eng2sp.values()
The items method returns both, in the form of a
list of tuples >>> eng2sp.items()
The syntax provides useful type information. The square brackets indicate that this is a list. The parentheses indicate that the elements of the list are tuples. If a method takes an argument, it uses the same syntax as a function call. For example, the method has_key takes a key and returns true (1) if the key appears in the dictionary: >>> eng2sp.has_key('one')
If you try to call a method without specifying an object, you get an error. In this case, the error message is not very helpful: >>> has_key('one')
10.3 Aliasing and copyingBecause dictionaries are mutable, you need to be aware of aliasing. Whenever two variables refer to the same object, changes to one affect the other. If you want to modify a dictionary and keep a copy of the original, use the copy method. For example, opposites is a dictionary that contains pairs of opposites: >>> opposites = {'up': 'down', 'right': 'wrong', 'true': 'false'}
alias and opposites refer to the same object; copy refers to a fresh copy of the same dictionary. If we modify alias, opposites is also changed: >>> alias['right'] = 'left'
If we modify copy, opposites is unchanged: >>> copy['right'] = 'privilege'
10.4 Sparse matricesIn Section 8.14, we used a list of lists to represent a matrix. That is a good choice for a matrix with mostly nonzero values, but consider a sparse matrix like this one:
The list representation contains a lot of zeroes: matrix = [ [0,0,0,1,0],
An alternative is to use a dictionary. For the keys, we can use tuples that contain the row and column numbers. Here is the dictionary representation of the same matrix: matrix = {(0,3): 1, (2, 1): 2, (4, 3): 3}
We only need three key-value pairs, one for each nonzero element of the matrix. Each key is a tuple, and each value is an integer. To access an element of the matrix, we could use the [] operator: matrix[0,3]
Notice that the syntax for the dictionary representation is not the same as the syntax for the nested list representation. Instead of two integer indices, we use one index, which is a tuple of integers. There is one problem. If we specify an element that is zero, we get an error, because there is no entry in the dictionary with that key: >>> matrix[1,3]
The get method solves this problem: >>> matrix.get((0,3), 0)
The first argument is the key; the second argument is the value get should return if the key is not in the dictionary: >>> matrix.get((1,3), 0)
get definitely improves the semantics of accessing a sparse matrix. Shame about the syntax. 10.5 HintsIf you played around with the fibonacci function from Section 5.7, you might have noticed that the bigger the argument you provide, the longer the function takes to run. Furthermore, the run time increases very quickly. On one of our machines, fibonacci(20) finishes instantly, fibonacci(30) takes about a second, and fibonacci(40) takes roughly forever. To understand why, consider this call graph for fibonacci with n=4:
A call graph shows a set function frames, with lines connecting each frame to the frames of the functions it calls. At the top of the graph, fibonacci with n=4 calls fibonacci with n=3 and n=2. In turn, fibonacci with n=3 calls fibonacci with n=2 and n=1. And so on. Count how many times fibonacci(0) and fibonacci(1) are called. This is an inefficient solution to the problem, and it gets far worse as the argument gets bigger. A good solution is to keep track of values that have already been computed by storing them in a dictionary. A previously computed value that is stored for later use is called a hint. Here is an implementation of fibonacci using hints: previous = {0:1, 1:1}
The dictionary named previous keeps track of the Fibonacci numbers we already know. We start with only two pairs: 0 maps to 1; and 1 maps to 1. Whenever fibonacci is called, it checks the dictionary to determine if it contains the result. If it's there, the function can return immediately without making any more recursive calls. If not, it has to compute the new value. The new value is added to the dictionary before the function returns. Using this version of fibonacci, our machines can compute fibonacci(40) in an eyeblink. But when we try to compute fibonacci(50), we see the following: >>> fibonacci(50)
The L at the end of the result indicates that the answer +(20,365,011,074) is too big to fit into a Python integer. Python has automatically converted the result to a long integer. 10.6 Long integersPython provides a type called long that can handle any size integer. There are two ways to create a long value. One is to write an integer with a capital L at the end: >>> type(1L)
The other is to use the long function to convert a value to a long. long can accept any numerical type and even strings of digits: >>> long(1)
All of the math operations work on longs, so in general any code that works with integers will also work with long integers. Any time the result of a computation is too big to be represented with an integer, Python detects the overflow and returns the result as a long integer. For example: >>> 1000 * 1000
In the first case the result has type int; in the second case it is long. 10.7 Counting lettersIn Chapter 7, we wrote a function that counted the number of occurrences of a letter in a string. A more general version of this problem is to form a histogram of the letters in the string, that is, how many times each letter appears. Such a histogram might be useful for compressing a text file. Because different letters appear with different frequencies, we can compress a file by using shorter codes for common letters and longer codes for letters that appear less frequently. Dictionaries provide an elegant way to generate a histogram: >>> letterCounts = {}
We start with an empty dictionary. For each letter in the string, we find the current count (possibly zero) and increment it. At the end, the dictionary contains pairs of letters and their frequencies. It might be more appealing to display the histogram in alphabetical order. We can do that with the items and sort methods: >>> letterItems = letterCounts.items()
You have seen the items method before, but sort is the first method you have encountered that applies to lists. There are several other list methods, including append, extend, and reverse. Consult the Python documentation for details. 10.8 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 1The way of the programThe goal of this book is to teach you to think like a computer scientist. This way of thinking combines some of the best features of mathematics, engineering, and natural science. Like mathematicians, computer scientists use formal languages to denote ideas (specifically computations). Like engineers, they design things, assembling components into systems and evaluating tradeoffs among alternatives. Like scientists, they observe the behavior of complex systems, form hypotheses, and test predictions. The single most important skill for a computer scientist is problem solving. Problem solving means the ability to formulate problems, think creatively about solutions, and express a solution clearly and accurately. As it turns out, the process of learning to program is an excellent opportunity to practice problem-solving skills. That's why this chapter is called, "The way of the program." On one level, you will be learning to program, a useful skill by itself. On another level, you will use programming as a means to an end. As we go along, that end will become clearer. 1.1 The Python programming languageThe programming language you will be learning is Python. Python is an example of a high-level language; other high-level languages you might have heard of are C, C++, Perl, and Java. As you might infer from the name "high-level language," there are also low-level languages, sometimes referred to as "machine languages" or "assembly languages." Loosely speaking, computers can only execute programs written in low-level languages. Thus, programs written in a high-level language have to be processed before they can run. This extra processing takes some time, which is a small disadvantage of high-level languages. But the advantages are enormous. First, it is much easier to program in a high-level language. Programs written in a high-level language take less time to write, they are shorter and easier to read, and they are more likely to be correct. Second, high-level languages are portable, meaning that they can run on different kinds of computers with few or no modifications. Low-level programs can run on only one kind of computer and have to be rewritten to run on another. Due to these advantages, almost all programs are written in high-level languages. Low-level languages are used only for a few specialized applications. Two kinds of programs process high-level languages into low-level languages: interpreters and compilers. An interpreter reads a high-level program and executes it, meaning that it does what the program says. It processes the program a little at a time, alternately reading lines and performing computations.
A compiler reads the program and translates it completely before the program starts running. In this case, the high-level program is called the source code, and the translated program is called the object code or the executable. Once a program is compiled, you can execute it repeatedly without further translation.
Python is considered an interpreted language because Python programs are executed by an interpreter. There are two ways to use the interpreter: command-line mode and script mode. In command-line mode, you type Python programs and the interpreter prints the result: $ python
The first line of this example is the command that starts the Python interpreter. The next two lines are messages from the interpreter. The third line starts with >>>, which is the prompt the interpreter uses to indicate that it is ready. We typed print 1 + 1, and the interpreter replied 2. Alternatively, you can write a program in a file and use the interpreter to execute the contents of the file. Such a file is called a script. For example, we used a text editor to create a file named latoya.py with the following contents: print 1 + 1
By convention, files that contain Python programs have names that end with .py. To execute the program, we have to tell the interpreter the name of the script: $ python latoya.py
In other development environments, the details of executing programs may differ. Also, most programs are more interesting than this one. Most of the examples in this book are executed on the command line. Working on the command line is convenient for program development and testing, because you can type programs and execute them immediately. Once you have a working program, you should store it in a script so you can execute or modify it in the future. 1.2 What is a program?A program is a sequence of instructions that specifies how to perform a computation. The computation might be something mathematical, such as solving a system of equations or finding the roots of a polynomial, but it can also be a symbolic computation, such as searching and replacing text in a document or (strangely enough) compiling a program. The details look different in different languages, but a few basic instructions appear in just about every language:
Believe it or not, that's pretty much all there is to it. Every program you've ever used, no matter how complicated, is made up of instructions that look more or less like these. Thus, we can describe programming as the process of breaking a large, complex task into smaller and smaller subtasks until the subtasks are simple enough to be performed with one of these basic instructions. That may be a little vague, but we will come back to this topic later when we talk about algorithms. 1.3 What is debugging?Programming is a complex process, and because it is done by human beings, it often leads to errors. For whimsical reasons, programming errors are called bugs and the process of tracking them down and correcting them is called debugging. Three kinds of errors can occur in a program: syntax errors, runtime errors, and semantic errors. It is useful to distinguish between them in order to track them down more quickly. Syntax errorsPython can only execute a program if the program is syntactically correct; otherwise, the process fails and returns an error message. Syntax refers to the structure of a program and the rules about that structure. For example, in English, a sentence must begin with a capital letter and end with a period. this sentence contains a syntax error. So does this one For most readers, a few syntax errors are not a significant problem, which is why we can read the poetry of e. e. cummings without spewing error messages. Python is not so forgiving. If there is a single syntax error anywhere in your program, Python will print an error message and quit, and you will not be able to run your program. During the first few weeks of your programming career, you will probably spend a lot of time tracking down syntax errors. As you gain experience, though, you will make fewer errors and find them faster. Runtime errorsThe second type of error is a runtime error, so called because the error does not appear until you run the program. These errors are also called exceptions because they usually indicate that something exceptional (and bad) has happened. Runtime errors are rare in the simple programs you will see in the first few chapters, so it might be a while before you encounter one. Semantic errorsThe third type of error is the semantic error. If there is a semantic error in your program, it will run successfully, in the sense that the computer will not generate any error messages, but it will not do the right thing. It will do something else. Specifically, it will do what you told it to do. The problem is that the program you wrote is not the program you wanted to write. The meaning of the program (its semantics) is wrong. Identifying semantic errors can be tricky because it requires you to work backward by looking at the output of the program and trying to figure out what it is doing. Experimental debuggingOne of the most important skills you will acquire is debugging. Although it can be frustrating, debugging is one of the most intellectually rich, challenging, and interesting parts of programming. In some ways, debugging is like detective work. You are confronted with clues, and you have to infer the processes and events that led to the results you see. Debugging is also like an experimental science. Once you have an idea what is going wrong, you modify your program and try again. If your hypothesis was correct, then you can predict the result of the modification, and you take a step closer to a working program. If your hypothesis was wrong, you have to come up with a new one. As Sherlock Holmes pointed out, "When you have eliminated the impossible, whatever remains, however improbable, must be the truth." (A. Conan Doyle, The Sign of Four) For some people, programming and debugging are the same thing. That is, programming is the process of gradually debugging a program until it does what you want. The idea is that you should start with a program that does something and make small modifications, debugging them as you go, so that you always have a working program. For example, Linux is an operating system that contains thousands of lines of code, but it started out as a simple program Linus Torvalds used to explore the Intel 80386 chip. According to Larry Greenfield, "One of Linus's earlier projects was a program that would switch between printing AAAA and BBBB. This later evolved to Linux." (The Linux Users' Guide Beta Version 1) Later chapters will make more suggestions about debugging and other programming practices. 1.4 Formal and natural languagesNatural languages are the languages that people speak, such as English, Spanish, and French. They were not designed by people (although people try to impose some order on them); they evolved naturally. Formal languages are languages that are designed by people for specific applications. For example, the notation that mathematicians use is a formal language that is particularly good at denoting relationships among numbers and symbols. Chemists use a formal language to represent the chemical structure of molecules. And most importantly: Programming languages are formal languages that have been designed to express computations. Formal languages tend to have strict rules about syntax. For example, 3+3=6 is a syntactically correct mathematical statement, but 3=+6$ is not. H2O is a syntactically correct chemical name, but 2Zz is not. Syntax rules come in two flavors, pertaining to tokens and structure. Tokens are the basic elements of the language, such as words, numbers, and chemical elements. One of the problems with 3=+6$ is that $ is not a legal token in mathematics (at least as far as we know). Similarly, 2Zz is not legal because there is no element with the abbreviation Zz.
The second type of syntax error pertains to the structure of a
statement As an exercise, create what appears to be a well-structured English sentence with unrecognizable tokens in it. Then write another sentence with all valid tokens but with invalid structure. When you read a sentence in English or a statement in a formal language, you have to figure out what the structure of the sentence is (although in a natural language you do this subconsciously). This process is called parsing. For example, when you hear the sentence, "The other shoe fell," you understand that "the other shoe" is the subject and "fell" is the predicate. Once you have parsed a sentence, you can figure out what it means, or the semantics of the sentence. Assuming that you know what a shoe is and what it means to fall, you will understand the general implication of this sentence.
Although formal and natural languages have many features in
common
People who grow up speaking a natural language
Here are some suggestions for reading programs (and other formal languages). First, remember that formal languages are much more dense than natural languages, so it takes longer to read them. Also, the structure is very important, so it is usually not a good idea to read from top to bottom, left to right. Instead, learn to parse the program in your head, identifying the tokens and interpreting the structure. Finally, the details matter. Little things like spelling errors and bad punctuation, which you can get away with in natural languages, can make a big difference in a formal language. 1.5 The first programTraditionally, the first program written in a new language is called "Hello, World!" because all it does is display the words, "Hello, World!" In Python, it looks like this: print "Hello, World!"
This is an example of a print statement, which doesn't actually print anything on paper. It displays a value on the screen. In this case, the result is the words Hello, World!
The quotation marks in the program mark the beginning and end of the value; they don't appear in the result. Some people judge the quality of a programming language by the simplicity of the "Hello, World!" program. By this standard, Python does about as well as possible. 1.6 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Contributor ListTo paraphrase the philosophy of the Free Software Foundation, this book is free like free speech, but not necessarily free like free pizza. It came about because of a collaboration that would not have been possible without the GNU Free Documentation License. So we thank the Free Software Foundation for developing this license and, of course, making it available to us. We also thank the more than 100 sharp-eyed and thoughtful readers who have sent us suggestions and corrections over the past few years. In the spirit of free software, we decided to express our gratitude in the form of a contributor list. Unfortunately, this list is not complete, but we are doing our best to keep it up to date. If you have a chance to look through the list, you should realize that each person here has spared you and all subsequent readers from the confusion of a technical error or a less-than-transparent explanation, just by sending us a note. Impossible as it may seem after so many corrections, there may still be errors in this book. If you should stumble across one, please check the online version of the book at http://thinkpython.com, which is the most up-to-date version. If the error has not been corrected, please take a minute to send us email at feedback@thinkpython.com. If we make a change due to your suggestion, you will appear in the next version of the contributor list (unless you ask to be omitted). Thank you!
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 17Linked lists17.1 Embedded referencesWe have seen examples of attributes that refer to other objects, which we called embedded references (see Section 12.8). A common data structure, the linked list, takes advantage of this feature. Linked lists are made up of nodes, where each node contains a reference to the next node in the list. In addition, each node contains a unit of data called the cargo. A linked list is considered a recursive data structure because it has a recursive definition. A linked list is either: Recursive data structures lend themselves to recursive methods. 17.2 The Node classAs usual when writing a new class, we'll start with the initialization and __str__ methods so that we can test the basic mechanism of creating and displaying the new type: class Node:
As usual, the parameters for the initialization method are optional. By default, both the cargo and the link, next, are set to None. The string representation of a node is just the string representation of the cargo. Since any value can be passed to the str function, we can store any value in a list. To test the implementation so far, we can create a Node and print it: >>> node = Node("test")
To make it interesting, we need a list with more than one node: >>> node1 = Node(1)
This code creates three nodes, but we don't have a list yet because the nodes are not linked. The state diagram looks like this:
To link the nodes, we have to make the first node refer to the second and the second node refer to the third: >>> node1.next = node2
The reference of the third node is None, which indicates that it is the end of the list. Now the state diagram looks like this:
Now you know how to create nodes and link them into lists. What might be less clear at this point is why. 17.3 Lists as collectionsLists are useful because they provide a way to assemble multiple objects into a single entity, sometimes called a collection. In the example, the first node of the list serves as a reference to the entire list. To pass the list as an argument, we only have to pass a reference to the first node. For example, the function printList takes a single node as an argument. Starting with the head of the list, it prints each node until it gets to the end: def printList(node):
To invoke this function, we pass a reference to the first node: >>> printList(node1)
Inside printList we have a reference to the first node of the list, but there is no variable that refers to the other nodes. We have to use the next value from each node to get to the next node. To traverse a linked list, it is common to use a loop variable like node to refer to each of the nodes in succession. This diagram shows the nodes in the list and the values that node takes on:
By convention, lists are often printed in brackets with commas between the elements, as in [1, 2, 3]. As an exercise, modify printList so that it generates output in this format. 17.4 Lists and recursionIt is natural to express many list operations using recursive methods. For example, the following is a recursive algorithm for printing a list backwards:
Of course, Step 2, the recursive call, assumes that we have a way of
printing a list backward. But if we assume that the recursive
call works All we need are a base case and a way of proving that for any list, we will eventually get to the base case. Given the recursive definition of a list, a natural base case is the empty list, represented by None: def printBackward(list):
The first line handles the base case by doing nothing. The next two lines split the list into head and tail. The last two lines print the list. The comma at the end of the last line keeps Python from printing a newline after each node. We invoke this function as we invoked printList: >>> printBackward(node1)
The result is a backward list. You might wonder why printList and printBackward are functions and not methods in the Node class. The reason is that we want to use None to represent the empty list and it is not legal to invoke a method on None. This limitation makes it awkward to write list-manipulating code in a clean object-oriented style. Can we prove that printBackward will always terminate? In other words, will it always reach the base case? In fact, the answer is no. Some lists will make this function crash. 17.5 Infinite listsThere is nothing to prevent a node from referring back to an earlier node in the list, including itself. For example, this figure shows a list with two nodes, one of which refers to itself:
If we invoke printList on this list, it will loop forever. If we invoke printBackward, it will recurse infinitely. This sort of behavior makes infinite lists difficult to work with. Nevertheless, they are occasionally useful. For example, we might represent a number as a list of digits and use an infinite list to represent a repeating fraction. Regardless, it is problematic that we cannot prove that printList and printBackward terminate. The best we can do is the hypothetical statement, "If the list contains no loops, then these functions will terminate." This sort of claim is called a precondition. It imposes a constraint on one of the arguments and describes the behavior of the function if the constraint is satisfied. You will see more examples soon. 17.6 The fundamental ambiguity theoremOne part of printBackward might have raised an eyebrow: head = list
After the first assignment, head and list have the same type and the same value. So why did we create a new variable? The reason is that the two variables play different roles. We think of head as a reference to a single node, and we think of list as a reference to the first node of a list. These "roles" are not part of the program; they are in the mind of the programmer. In general we can't tell by looking at a program what role a variable plays. This ambiguity can be useful, but it can also make programs difficult to read. We often use variable names like node and list to document how we intend to use a variable and sometimes create additional variables to disambiguate. We could have written printBackward without head and tail, which makes it more concise but possibly less clear: def printBackward(list) :
Looking at the two function calls, we have to remember that printBackward treats its argument as a collection and print treats its argument as a single object. The fundamental ambiguity theorem describes the ambiguity that is inherent in a reference to a node: A variable that refers to a node might treat the node as a single object or as the first in a list of nodes. 17.7 Modifying listsThere are two ways to modify a linked list. Obviously, we can change the cargo of one of the nodes, but the more interesting operations are the ones that add, remove, or reorder the nodes. As an example, let's write a function that removes the second node in the list and returns a reference to the removed node: def removeSecond(list):
Again, we are using temporary variables to make the code more readable. Here is how to use this function: >>> printList(node1)
This state diagram shows the effect of the operation:
What happens if you invoke this function and pass a list with only one element (a singleton)? What happens if you pass the empty list as an argument? Is there a precondition for this function? If so, fix the function to handle a violation of the precondition in a reasonable way. 17.8 Wrappers and helpersIt is often useful to divide a list operation into two functions. For example, to print a list backward in the format [3 2 1] we can use the printBackward function to print 3 2 1 but we need a separate function to print the brackets. Let's call it printBackwardNicely: def printBackwardNicely(list) :
Again, it is a good idea to check functions like this to see if they work with special cases like an empty list or a singleton. When we use this function elsewhere in the program, we invoke printBackwardNicely directly, and it invokes printBackward on our behalf. In that sense, printBackwardNicely acts as a wrapper, and it uses printBackward as a helper. 17.9 The LinkedList classThere are some subtle problems with the way we have been implementing lists. In a reversal of cause and effect, we'll propose an alternative implementation first and then explain what problems it solves. First, we'll create a new class called LinkedList. Its attributes are an integer that contains the length of the list and a reference to the first node. LinkedList objects serve as handles for manipulating lists of Node objects: class LinkedList :
One nice thing about the LinkedList class is that it provides a natural place to put wrapper functions like printBackwardNicely, which we can make a method of the LinkedList class: class LinkedList:
Just to make things confusing, we renamed printBackwardNicely. Now there are two methods named printBackward: one in the Node class (the helper); and one in the LinkedList class (the wrapper). When the wrapper invokes self.head.printBackward, it is invoking the helper, because self.head is a Node object. Another benefit of the LinkedList class is that it makes it easier to add or remove the first element of a list. For example, addFirst is a method for LinkedLists; it takes an item of cargo as an argument and puts it at the beginning of the list: class LinkedList:
As usual, you should check code like this to see if it handles the special cases. For example, what happens if the list is initially empty? 17.10 InvariantsSome lists are "well formed"; others are not. For example, if a list contains a loop, it will cause many of our methods to crash, so we might want to require that lists contain no loops. Another requirement is that the length value in the LinkedList object should be equal to the actual number of nodes in the list. Requirements like these are called invariants because, ideally, they should be true of every object all the time. Specifying invariants for objects is a useful programming practice because it makes it easier to prove the correctness of code, check the integrity of data structures, and detect errors. One thing that is sometimes confusing about invariants is that there are times when they are violated. For example, in the middle of addFirst, after we have added the node but before we have incremented length, the invariant is violated. This kind of violation is acceptable; in fact, it is often impossible to modify an object without violating an invariant for at least a little while. Normally, we require that every method that violates an invariant must restore the invariant. If there is any significant stretch of code in which the invariant is violated, it is important for the comments to make that clear, so that no operations are performed that depend on the invariant. 17.11 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 19QueuesThis chapter presents two ADTs: the Queue and the Priority Queue. In real life, a queue is a line of customers waiting for service of some kind. In most cases, the first customer in line is the next customer to be served. There are exceptions, though. At airports, customers whose flights are leaving soon are sometimes taken from the middle of the queue. At supermarkets, a polite customer might let someone with only a few items go first. The rule that determines who goes next is called the queueing policy. The simplest queueing policy is called FIFO, for "first-in-first-out." The most general queueing policy is priority queueing, in which each customer is assigned a priority and the customer with the highest priority goes first, regardless of the order of arrival. We say this is the most general policy because the priority can be based on anything: what time a flight leaves; how many groceries the customer has; or how important the customer is. Of course, not all queueing policies are "fair," but fairness is in the eye of the beholder. The Queue ADT and the Priority Queue ADT have the same set of operations. The difference is in the semantics of the operations: a queue uses the FIFO policy; and a priority queue (as the name suggests) uses the priority queueing policy. 19.1 The Queue ADTThe Queue ADT is defined by the following operations:
19.2 Linked QueueThe first implementation of the Queue ADT we will look at is called a linked queue because it is made up of linked Node objects. Here is the class definition: class Queue:
The methods isEmpty and remove are identical to the LinkedList methods isEmpty and removeFirst. The insert method is new and a bit more complicated. We want to insert new items at the end of the list. If the queue is empty, we just set head to refer to the new node. Otherwise, we traverse the list to the last node and tack the new node on the end. We can identify the last node because its next attribute is None. There are two invariants for a properly formed Queue object. The value of length should be the number of nodes in the queue, and the last node should have next equal to None. Convince yourself that this method preserves both invariants. 19.3 Performance characteristics
Normally when we invoke a method, we are not concerned with the
details of its implementation. But there is one "detail"
we might want to know First look at remove. There are no loops or function calls here, suggesting that the runtime of this method is the same every time. Such a method is called a constant time operation. In reality, the method might be slightly faster when the list is empty since it skips the body of the conditional, but that difference is not significant. The performance of insert is very different. In the general case, we have to traverse the list to find the last element. This traversal takes time proportional to the length of the list. Since the runtime is a linear function of the length, this method is called linear time. Compared to constant time, that's very bad. 19.4 Improved Linked QueueWe would like an implementation of the Queue ADT that can perform all operations in constant time. One way to do that is to modify the Queue class so that it maintains a reference to both the first and the last node, as shown in the figure:
The ImprovedQueue implementation looks like this: class ImprovedQueue:
So far, the only change is the attribute last. It is used in insert and remove methods: class ImprovedQueue:
Since last keeps track of the last node, we don't have to search for it. As a result, this method is constant time. There is a price to pay for that speed. We have to add a special case to remove to set last to None when the last node is removed: class ImprovedQueue:
This implementation is more complicated than the
Linked Queue implementation, and it is more difficult to demonstrate
that it is correct. The advantage is that we have achieved
the goal As an exercise, write an implementation of the Queue ADT using a Python list. Compare the performance of this implementation to the ImprovedQueue for a range of queue lengths. 19.5 Priority queueThe Priority Queue ADT has the same interface as the Queue ADT, but different semantics. Again, the interface is:
The semantic difference is that the item that is removed from the queue is not necessarily the first one that was added. Rather, it is the item in the queue that has the highest priority. What the priorities are and how they compare to each other are not specified by the Priority Queue implementation. It depends on which items are in the queue. For example, if the items in the queue have names, we might choose them in alphabetical order. If they are bowling scores, we might go from highest to lowest, but if they are golf scores, we would go from lowest to highest. As long as we can compare the items in the queue, we can find and remove the one with the highest priority. This implementation of Priority Queue has as an attribute a Python list that contains the items in the queue. class PriorityQueue:
The initialization method, isEmpty, and insert are all veneers on list operations. The only interesting method is remove: class PriorityQueue:
At the beginning of each iteration, maxi holds the index of the biggest item (highest priority) we have seen so far. Each time through the loop, the program compares the i-eth item to the champion. If the new item is bigger, the value of maxi is set to i. When the for statement completes, maxi is the index of the biggest item. This item is removed from the list and returned. Let's test the implementation: >>> q = PriorityQueue()
If the queue contains simple numbers or strings, they are removed in numerical or alphabetical order, from highest to lowest. Python can find the biggest integer or string because it can compare them using the built-in comparison operators. If the queue contains an object type, it has to provide a __cmp__ method. When remove uses the > operator to compare items, it invokes the __cmp__ for one of the items and passes the other as an argument. As long as the __cmp__ method works correctly, the Priority Queue will work. 19.6 The Golfer classAs an example of an object with an unusual definition of priority, let's implement a class called Golfer that keeps track of the names and scores of golfers. As usual, we start by defining __init__ and __str__: class Golfer:
__str__ uses the format operator to put the names and scores in neat columns. Next we define a version of __cmp__ where the lowest score gets highest priority. As always, __cmp__ returns 1 if self is "greater than" other, -1 if self is "less than" other, and 0 if they are equal. class Golfer:
Now we are ready to test the priority queue with the Golfer class: >>> tiger = Golfer("Tiger Woods", 61)
As an exercise, write an implementation of the Priority Queue ADT using a linked list. You should keep the list sorted so that removal is a constant time operation. Compare the performance of this implementation with the Python list implementation. 19.7 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 11Files and exceptionsWhile a program is running, its data is in memory. When the program ends, or the computer shuts down, data in memory disappears. To store data permanently, you have to put it in a file. Files are usually stored on a hard drive, floppy drive, or CD-ROM. When there are a large number of files, they are often organized into directories (also called "folders"). Each file is identified by a unique name, or a combination of a file name and a directory name. By reading and writing files, programs can exchange information with each other and generate printable formats like PDF. Working with files is a lot like working with books. To use a book, you have to open it. When you're done, you have to close it. While the book is open, you can either write in it or read from it. In either case, you know where you are in the book. Most of the time, you read the whole book in its natural order, but you can also skip around. All of this applies to files as well. To open a file, you specify its name and indicate whether you want to read or write. Opening a file creates a file object. In this example, the variable f refers to the new file object. >>> f = open("test.dat","w")
The open function takes two arguments. The first is the name of the file, and the second is the mode. Mode "w" means that we are opening the file for writing. If there is no file named test.dat, it will be created. If there already is one, it will be replaced by the file we are writing. When we print the file object, we see the name of the file, the mode, and the location of the object. To put data in the file we invoke the write method on the file object: >>> f.write("Now is the time")
Closing the file tells the system that we are done writing and makes the file available for reading: >>> f.close()
Now we can open the file again, this time for reading, and read the contents into a string. This time, the mode argument is "r" for reading: >>> f = open("test.dat","r")
If we try to open a file that doesn't exist, we get an error: >>> f = open("test.cat","r")
Not surprisingly, the read method reads data from the file. With no arguments, it reads the entire contents of the file: >>> text = f.read()
There is no space between "time" and "to" because we did not write a space between the strings. read can also take an argument that indicates how many characters to read: >>> f = open("test.dat","r")
If not enough characters are left in the file, read returns the remaining characters. When we get to the end of the file, read returns the empty string: >>> print f.read(1000006)
The following function copies a file, reading and writing up to fifty characters at a time. The first argument is the name of the original file; the second is the name of the new file: def copyFile(oldFile, newFile):
The break statement is new. Executing it breaks out of the loop; the flow of execution moves to the first statement after the loop. In this example, the while loop is infinite because the value True is always true. The only way to get out of the loop is to execute break, which happens when text is the empty string, which happens when we get to the end of the file. 11.1 Text filesA text file is a file that contains printable characters and whitespace, organized into lines separated by newline characters. Since Python is specifically designed to process text files, it provides methods that make the job easy. To demonstrate, we'll create a text file with three lines of text separated by newlines: >>> f = open("test.dat","w")
The readline method reads all the characters up to and including the next newline character: >>> f = open("test.dat","r")
readlines returns all of the remaining lines as a list of strings: >>> print f.readlines()
In this case, the output is in list format, which means that the strings appear with quotation marks and the newline character appears as the escape sequence <br>012. At the end of the file, readline returns the empty string and readlines returns the empty list: >>> print f.readline()
The following is an example of a line-processing program. filterFile makes a copy of oldFile, omitting any lines that begin with #: def filterFile(oldFile, newFile):
The continue statement ends the current iteration of the loop, but continues looping. The flow of execution moves to the top of the loop, checks the condition, and proceeds accordingly. Thus, if text is the empty string, the loop exits. If the first character of text is a hash mark, the flow of execution goes to the top of the loop. Only if both conditions fail do we copy text into the new file. 11.2 Writing variablesThe argument of write has to be a string, so if we want to put other values in a file, we have to convert them to strings first. The easiest way to do that is with the str function: >>> x = 52
An alternative is to use the format operator %. When applied to integers, % is the modulus operator. But when the first operand is a string, % is the format operator. The first operand is the format string, and the second operand is a tuple of expressions. The result is a string that contains the values of the expressions, formatted according to the format string. As a simple example, the format sequence "%d" means that the first expression in the tuple should be formatted as an integer. Here the letter d stands for "decimal": >>> cars = 52
The result is the string '52', which is not to be confused with the integer value 52. A format sequence can appear anywhere in the format string, so we can embed a value in a sentence: >>> cars = 52
The format sequence "%f" formats the next item in the tuple as a floating-point number, and "%s" formats the next item as a string: >>> "In %d days we made %f million %s." % (34,6.1,'dollars')
By default, the floating-point format prints six decimal places. The number of expressions in the tuple has to match the number of format sequences in the string. Also, the types of the expressions have to match the format sequences: >>> "%d %d %d" % (1,2)
In the first example, there aren't enough expressions; in the second, the expression is the wrong type. For more control over the format of numbers, we can specify the number of digits as part of the format sequence: >>> "%6d" % 62
The number after the percent sign is the minimum number of spaces the number will take up. If the value provided takes fewer digits, leading spaces are added. If the number of spaces is negative, trailing spaces are added: >>> "%-6d" % 62
For floating-point numbers, we can also specify the number of digits after the decimal point: >>> "%12.2f" % 6.1
In this example, the result takes up twelve spaces and includes two digits after the decimal. This format is useful for printing dollar amounts with the decimal points aligned. For example, imagine a dictionary that contains student names as keys and hourly wages as values. Here is a function that prints the contents of the dictionary as a formatted report: def report (wages) :
To test this function, we'll create a small dictionary and print the contents: >>> wages = {'mary': 6.23, 'joe': 5.45, 'joshua': 4.25}
By controlling the width of each value, we guarantee that the columns will line up, as long as the names contain fewer than twenty-one characters and the wages are less than one billion dollars an hour. 11.3 DirectoriesWhen you create a new file by opening it and writing, the new file goes in the current directory (wherever you were when you ran the program). Similarly, when you open a file for reading, Python looks for it in the current directory. If you want to open a file somewhere else, you have to specify the path to the file, which is the name of the directory (or folder) where the file is located: >>> f = open("/usr/share/dict/words","r")
This example opens a file named words that resides in a directory named dict, which resides in share, which resides in usr, which resides in the top-level directory of the system, called /. You cannot use / as part of a filename; it is reserved as a delimiter between directory and filenames. The file /usr/share/dict/words contains a list of words in alphabetical order, of which the first is the name of a Danish university. 11.4 PicklingIn order to put values into a file, you have to convert them to strings. You have already seen how to do that with str: >>> f.write (str(12.3))
The problem is that when you read the value back, you get a string. The original type information has been lost. In fact, you can't even tell where one value ends and the next begins: >>> f.readline()
The solution is pickling, so called because it "preserves" data structures. The pickle module contains the necessary commands. To use it, import pickle and then open the file in the usual way: >>> import pickle
To store a data structure, use the dump method and then close the file in the usual way: >>> pickle.dump(12.3, f)
Then we can open the file for reading and load the data structures we dumped: >>> f = open("test.pck","r")
Each time we invoke load, we get a single value from the file, complete with its original type. 11.5 ExceptionsWhenever a runtime error occurs, it creates an exception. Usually, the program stops and Python prints an error message. For example, dividing by zero creates an exception: >>> print 55/0
So does accessing a nonexistent list item: >>> a = []
Or accessing a key that isn't in the dictionary: >>> b = {}
Or trying to open a nonexistent file: >>> f = open("Idontexist", "r")
In each case, the error message has two parts: the type of error before the colon, and specifics about the error after the colon. Normally Python also prints a traceback of where the program was, but we have omitted that from the examples. Sometimes we want to execute an operation that could cause an exception, but we don't want the program to stop. We can handle the exception using the try and except statements. For example, we might prompt the user for the name of a file and then try to open it. If the file doesn't exist, we don't want the program to crash; we want to handle the exception: filename = raw_input('Enter a file name: ')
The try statement executes the statements in the first block. If no exceptions occur, it ignores the except statement. If an exception of type IOError occurs, it executes the statements in the except branch and then continues. We can encapsulate this capability in a function: exists takes a filename and returns true if the file exists, false if it doesn't: def exists(filename):
You can use multiple except blocks to handle different kinds of exceptions. The Python Reference Manual has the details. If your program detects an error condition, you can make it raise an exception. Here is an example that gets input from the user and checks for the value 17. Assuming that 17 is not valid input for some reason, we raise an exception. def inputNumber () :
The raise statement takes two arguments: the exception type and specific information about the error. ValueError is one of the exception types Python provides for a variety of occasions. Other examples include TypeError, KeyError, and my favorite, NotImplementedError. If the function that called inputNumber handles the error, then the program can continue; otherwise, Python prints the error message and exits: >>> inputNumber ()
The error message includes the exception type and the additional information you provided. As an exercise, write a function that uses inputNumber to input a number from the keyboard and that handles the ValueError exception. 11.6 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Appendix BCreating a new data typeObject-oriented programming languages allow programmers to create new data types that behave much like built-in data types. We will explore this capability by building a Fraction class that works very much like the built-in numeric types: integers, longs and floats. Fractions, also known as rational numbers, are values that can be expressed as a ratio of whole numbers, such as 5/6. The top number is called the numerator and the bottom number is called the denominator. We start by defining a Fraction class with an initialization method that provides the numerator and denominator as integers: class Fraction:
The denominator is optional. A Fraction with just one parameter represents a whole number. If the numerator is n, we build the Fraction n/1. The next step is to write a __str__ method that displays fractions in a way that makes sense. The form "numerator/denominator" is natural here: class Fraction:
To test what we have so far, we put it in a file named Fraction.py and import it into the Python interpreter. Then we create a fraction object and print it. >>> from Fraction import Fraction
As usual, the print command invokes the __str__ method implicitly. Fraction multiplicationWe would like to be able to apply the normal addition, subtraction, multiplication, and division operations to fractions. To do this, we can overload the mathematical operators for Fraction objects. We'll start with multiplication because it is the easiest to implement. To multiply fractions, we create a new fraction with a numerator that is the product of the original numerators and a denominator that is a product of the original denominators. __mul__ is the name Python uses for a method that overloads the * operator: class Fraction:
We can test this method by computing the product of two fractions: >>> print Fraction(5,6) * Fraction(3,4)
It works, but we can do better! We can extend the method to handle multiplication by an integer. We use the isinstance function to test if other is an integer and convert it to a fraction if it is. class Fraction:
Multiplying fractions and integers now works, but only if the fraction is the left operand: >>> print Fraction(5,6) * 4
To evaluate a binary operator like multiplication, Python checks the left operand first to see if it provides a __mul__ that supports the type of the second operand. In this case, the built-in integer operator doesn't support fractions. Next, Python checks the right operand to see if it provides an __rmul__ method that supports the first type. In this case, we haven't provided __rmul__, so it fails. On the other hand, there is a simple way to provide __rmul__: class Fraction:
This assignment says that the __rmul__ is the same as __mul__. Now if we evaluate 4 * Fraction(5,6), Python invokes __rmul__ on the Fraction object and passes 4 as a parameter: >>> print 4 * Fraction(5,6)
Since __rmul__ is the same as __mul__, and __mul__ can handle an integer parameter, we're all set. Fraction additionAddition is more complicated than multiplication, but still not too bad. The sum of a/b and c/d is the fraction (a*d+c*b)/(b*d). Using the multiplication code as a model, we can write __add__ and __radd__: class Fraction:
We can test these methods with Fractions and integers. >>> print Fraction(5,6) + Fraction(5,6)
The first two examples invoke __add__; the last invokes __radd__. Euclid's algorithmIn the previous example, we computed the sum 5/6 + 5/6 and got 60/36. That is correct, but it's not the best way to represent the answer. To reduce the fraction to its simplest terms, we have to divide the numerator and denominator by their greatest common divisor (GCD), which is 12. The result is 5/3. In general, whenever we create a new Fraction object, we should reduce it by dividing the numerator and denominator by their GCD. If the fraction is already reduced, the GCD is 1. Euclid of Alexandria (approx. 325--265 BCE) presented an algorithm to find the GCD for two integers m and n: If n divides m evenly, then n is the GCD. Otherwise the GCD is the GCD of n and the remainder of m divided by n. This recursive definition can be expressed concisely as a function: def gcd (m, n):
In the first line of the body, we use the modulus operator to check divisibility. On the last line, we use it to compute the remainder after division. Since all the operations we've written create new Fractions for the result, we can reduce all results by modifying the initialization method. class Fraction:
Now whenever we create a Fraction, it is reduced to its simplest form: >>> Fraction(100,-36)
A nice feature of gcd is that if the fraction is negative, the minus sign is always moved to the numerator. Comparing fractionsSuppose we have two Fraction objects, a and b, and we evaluate a == b. The default implementation of == tests for shallow equality, so it only returns true if a and b are the same object.
More likely, we want to return true if a and b have
the same value We have to teach fractions how to compare themselves. As we saw in Section 15.4, we can overload all the comparison operators at once by supplying a __cmp__ method. By convention, the __cmp__ method returns a negative number if self is less than other, zero if they are the same, and a positive number if self is greater than other. The simplest way to compare fractions is to cross-multiply. If a/b > c/d, then ad > bc. With that in mind, here is the code for __cmp__: class Fraction:
If self is greater than other, then diff will be positive. If other is greater, then diff will be negative. If they are the same, diff is zero. Taking it furtherOf course, we are not done. We still have to implement subtraction by overriding __sub__ and division by overriding __div__. One way to handle those operations is to implement negation by overriding __neg__ and inversion by overriding __invert__. Then we can subtract by negating the second operand and adding, and we can divide by inverting the second operand and multiplying. Next, we have to provide __rsub__ and __rdiv__. Unfortunately, we can't use the same trick we used for addition and multiplication, because subtraction and division are not commutative. We can't just set __rsub__ and __rdiv__ equal to __sub__ and __div__. In these operations, the order of the operands makes a difference. To handle unary negation, which is the use of the minus sign with a single operand, we override __neg__. We can compute powers by overriding __pow__, but the implementation is a little tricky. If the exponent isn't an integer, then it may not be possible to represent the result as a Fraction. For example, Fraction(2) ** Fraction(1,2) is the square root of 2, which is an irrational number (it can't be represented as a fraction). So it's not easy to write the most general version of __pow__. There is one other extension to the Fraction class that you might want to think about. So far, we have assumed that the numerator and denominator are integers. As an exercise, finish the implementation of the Fraction class so that it handles subtraction, division and exponentiation. Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 16Inheritance16.1 InheritanceThe language feature most often associated with object-oriented programming is inheritance. Inheritance is the ability to define a new class that is a modified version of an existing class. The primary advantage of this feature is that you can add new methods to a class without modifying the existing class. It is called "inheritance" because the new class inherits all of the methods of the existing class. Extending this metaphor, the existing class is sometimes called the parent class. The new class may be called the child class or sometimes "subclass." Inheritance is a powerful feature. Some programs that would be complicated without inheritance can be written concisely and simply with it. Also, inheritance can facilitate code reuse, since you can customize the behavior of parent classes without having to modify them. In some cases, the inheritance structure reflects the natural structure of the problem, which makes the program easier to understand. On the other hand, inheritance can make programs difficult to read. When a method is invoked, it is sometimes not clear where to find its definition. The relevant code may be scattered among several modules. Also, many of the things that can be done using inheritance can be done as elegantly (or more so) without it. If the natural structure of the problem does not lend itself to inheritance, this style of programming can do more harm than good. In this chapter we will demonstrate the use of inheritance as part of a program that plays the card game Old Maid. One of our goals is to write code that could be reused to implement other card games. 16.2 A hand of cardsFor almost any card game, we need to represent a hand of cards. A hand is similar to a deck, of course. Both are made up of a set of cards, and both require operations like adding and removing cards. Also, we might like the ability to shuffle both decks and hands. A hand is also different from a deck. Depending on the game being played, we might want to perform some operations on hands that don't make sense for a deck. For example, in poker we might classify a hand (straight, flush, etc.) or compare it with another hand. In bridge, we might want to compute a score for a hand in order to make a bid. This situation suggests the use of inheritance. If Hand is a subclass of Deck, it will have all the methods of Deck, and new methods can be added. In the class definition, the name of the parent class appears in parentheses: class Hand(Deck):
This statement indicates that the new Hand class inherits from the existing Deck class. The Hand constructor initializes the attributes for the hand, which are name and cards. The string name identifies this hand, probably by the name of the player that holds it. The name is an optional parameter with the empty string as a default value. cards is the list of cards in the hand, initialized to the empty list: class Hand(Deck):
For just about any card game, it is necessary to add and remove cards from the deck. Removing cards is already taken care of, since Hand inherits removeCard from Deck. But we have to write addCard: class Hand(Deck):
Again, the ellipsis indicates that we have omitted other methods. The list append method adds the new card to the end of the list of cards. 16.3 Dealing cardsNow that we have a Hand class, we want to deal cards from the Deck into hands. It is not immediately obvious whether this method should go in the Hand class or in the Deck class, but since it operates on a single deck and (possibly) several hands, it is more natural to put it in Deck. deal should be fairly general, since different games will have different requirements. We may want to deal out the entire deck at once or add one card to each hand. deal takes three parameters: the deck, a list (or tuple) of hands, and the total number of cards to deal. If there are not enough cards in the deck, the method deals out all of the cards and stops: class Deck :
The last parameter, nCards, is optional; the default is a large number, which effectively means that all of the cards in the deck will get dealt. The loop variable i goes from 0 to nCards-1. Each time through the loop, a card is removed from the deck using the list method pop, which removes and returns the last item in the list. The modulus operator (%) allows us to deal cards in a round robin (one card at a time to each hand). When i is equal to the number of hands in the list, the expression i % nHands wraps around to the beginning of the list (index 0). 16.4 Printing a HandTo print the contents of a hand, we can take advantage of the printDeck and __str__ methods inherited from Deck. For example: >>> deck = Deck()
It's not a great hand, but it has the makings of a straight flush. Although it is convenient to inherit the existing methods, there is additional information in a Hand object we might want to include when we print one. To do that, we can provide a __str__ method in the Hand class that overrides the one in the Deck class: class Hand(Deck)
Initially, s is a string that identifies the hand. If the hand is empty, the program appends the words is empty and returns the result. Otherwise, the program appends the word contains and the string representation of the Deck, computed by invoking the __str__ method in the Deck class on self. It may seem odd to send self, which refers to the current Hand, to a Deck method, until you remember that a Hand is a kind of Deck. Hand objects can do everything Deck objects can, so it is legal to send a Hand to a Deck method. In general, it is always legal to use an instance of a subclass in place of an instance of a parent class. 16.5 The CardGame classThe CardGame class takes care of some basic chores common to all games, such as creating the deck and shuffling it: class CardGame:
This is the first case we have seen where the initialization method performs a significant computation, beyond initializing attributes. To implement specific games, we can inherit from CardGame and add features for the new game. As an example, we'll write a simulation of Old Maid. The object of Old Maid is to get rid of cards in your hand. You do this by matching cards by rank and color. For example, the 4 of Clubs matches the 4 of Spades since both suits are black. The Jack of Hearts matches the Jack of Diamonds since both are red. To begin the game, the Queen of Clubs is removed from the deck so that the Queen of Spades has no match. The fifty-one remaining cards are dealt to the players in a round robin. After the deal, all players match and discard as many cards as possible. When no more matches can be made, play begins. In turn, each player picks a card (without looking) from the closest neighbor to the left who still has cards. If the chosen card matches a card in the player's hand, the pair is removed. Otherwise, the card is added to the player's hand. Eventually all possible matches are made, leaving only the Queen of Spades in the loser's hand. In our computer simulation of the game, the computer plays all hands. Unfortunately, some nuances of the real game are lost. In a real game, the player with the Old Maid goes to some effort to get their neighbor to pick that card, by displaying it a little more prominently, or perhaps failing to display it more prominently, or even failing to fail to display that card more prominently. The computer simply picks a neighbor's card at random. 16.6 OldMaidHand classA hand for playing Old Maid requires some abilities beyond the general abilities of a Hand. We will define a new class, OldMaidHand, that inherits from Hand and provides an additional method called removeMatches: class OldMaidHand(Hand):
We start by making a copy of the list of cards, so that we can traverse the copy while removing cards from the original. Since self.cards is modified in the loop, we don't want to use it to control the traversal. Python can get quite confused if it is traversing a list that is changing! For each card in the hand, we figure out what the matching card is and go looking for it. The match card has the same rank and the other suit of the same color. The expression 3 - card.suit turns a Club (suit 0) into a Spade (suit 3) and a Diamond (suit 1) into a Heart (suit 2). You should satisfy yourself that the opposite operations also work. If the match card is also in the hand, both cards are removed. The following example demonstrates how to use removeMatches: >>> game = CardGame()
Notice that there is no __init__ method for the OldMaidHand class. We inherit it from Hand. 16.7 OldMaidGame classNow we can turn our attention to the game itself. OldMaidGame is a subclass of CardGame with a new method called play that takes a list of players as an argument. Since __init__ is inherited from CardGame, a new OldMaidGame object contains a new shuffled deck: class OldMaidGame(CardGame):
Some of the steps of the game have been separated into methods. removeAllMatches traverses the list of hands and invokes removeMatches on each: class OldMaidGame(CardGame):
As an exercise, write printHands which traverses self.hands and prints each hand. count is an accumulator that adds up the number of matches in each hand and returns the total. When the total number of matches reaches twenty-five, fifty cards have been removed from the hands, which means that only one card is left and the game is over. The variable turn keeps track of which player's turn it is. It starts at 0 and increases by one each time; when it reaches numHands, the modulus operator wraps it back around to 0. The method playOneTurn takes an argument that indicates whose turn it is. The return value is the number of matches made during this turn: class OldMaidGame(CardGame):
If a player's hand is empty, that player is out of the game, so he or she does nothing and returns 0. Otherwise, a turn consists of finding the first player on the left that has cards, taking one card from the neighbor, and checking for matches. Before returning, the cards in the hand are shuffled so that the next player's choice is random. The method findNeighbor starts with the player to the immediate left and continues around the circle until it finds a player that still has cards: class OldMaidGame(CardGame):
If findNeighbor ever went all the way around the circle without finding cards, it would return None and cause an error elsewhere in the program. Fortunately, we can prove that that will never happen (as long as the end of the game is detected correctly). We have omitted the printHands method. You can write that one yourself. The following output is from a truncated form of the game where only the top fifteen cards (tens and higher) were dealt to three players. With this small deck, play stops after seven matches instead of twenty-five. >>> import cards
16.8 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 4Conditionals and recursion4.1 The modulus operatorThe modulus operator works on integers (and integer expressions) and yields the remainder when the first operand is divided by the second. In Python, the modulus operator is a percent sign (%). The syntax is the same as for other operators: >>> quotient = 7 / 3
So 7 divided by 3 is 2 with 1 left over.
The modulus operator turns out to be surprisingly useful. For
example, you can check whether one number is divisible by another Also, you can extract the right-most digit or digits from a number. For example, x % 10 yields the right-most digit of x (in base 10). Similarly x % 100 yields the last two digits. 4.2 Boolean expressionsA boolean expression is an expression that is either true or false. One way to write a boolean expression is to use the operator ==, which compares two values and produces a boolean value: >>> 5 == 5
In the first statement, the two operands are equal, so the value of the expression is True; in the second statement, 5 is not equal to 6, so we get False. True and False are special values that are built into Python. The == operator is one of the comparison operators; the others are: x != y # x is not equal to y
Although these operations are probably familiar to you, the Python symbols are different from the mathematical symbols. A common error is to use a single equal sign (=) instead of a double equal sign (==). Remember that = is an assignment operator and == is a comparison operator. Also, there is no such thing as =< or =>. 4.3 Logical operatorsThere are three logical operators: and, or, and not. The semantics (meaning) of these operators is similar to their meaning in English. For example, x > 0 and x < 10 is true only if x is greater than 0 and less than 10. n%2 == 0 or n%3 == 0 is true if either of the conditions is true, that is, if the number is divisible by 2 or 3. Finally, the not operator negates a boolean expression, so not(x > y) is true if (x > y) is false, that is, if x is less than or equal to y. Strictly speaking, the operands of the logical operators should be boolean expressions, but Python is not very strict. Any nonzero number is interpreted as "true." >>> x = 5
In general, this sort of thing is not considered good style. If you want to compare a value to zero, you should do it explicitly. 4.4 Conditional executionIn order to write useful programs, we almost always need the ability to check conditions and change the behavior of the program accordingly. Conditional statements give us this ability. The simplest form is the if statement: if x > 0:
The boolean expression after the if statement is called the condition. If it is true, then the indented statement gets executed. If not, nothing happens. Like other compound statements, the if statement is made up of a header and a block of statements: HEADER:
The header begins on a new line and ends with a colon (:). The indented statements that follow are called a block. The first unindented statement marks the end of the block. A statement block inside a compound statement is called the body of the statement. There is no limit on the number of statements that can appear in the body of an if statement, but there has to be at least one. Occasionally, it is useful to have a body with no statements (usually as a place keeper for code you haven't written yet). In that case, you can use the pass statement, which does nothing. 4.5 Alternative executionA second form of the if statement is alternative execution, in which there are two possibilities and the condition determines which one gets executed. The syntax looks like this: if x%2 == 0:
If the remainder when x is divided by 2 is 0, then we know that x is even, and the program displays a message to that effect. If the condition is false, the second set of statements is executed. Since the condition must be true or false, exactly one of the alternatives will be executed. The alternatives are called branches, because they are branches in the flow of execution. As an aside, if you need to check the parity (evenness or oddness) of numbers often, you might "wrap" this code in a function: def printParity(x):
For any value of x, printParity displays an appropriate message. When you call it, you can provide any integer expression as an argument. >>> printParity(17)
4.6 Chained conditionalsSometimes there are more than two possibilities and we need more than two branches. One way to express a computation like that is a chained conditional: if x < y:
elif is an abbreviation of "else if." Again, exactly one branch will be executed. There is no limit of the number of elif statements, but the last branch has to be an else statement: if choice == 'A':
Each condition is checked in order. If the first is false, the next is checked, and so on. If one of them is true, the corresponding branch executes, and the statement ends. Even if more than one condition is true, only the first true branch executes. As an exercise, wrap these examples in functions called compare(x, y) and dispatch(choice). 4.7 Nested conditionalsOne conditional can also be nested within another. We could have written the trichotomy example as follows: if x == y:
The outer conditional contains two branches. The first branch contains a simple output statement. The second branch contains another if statement, which has two branches of its own. Those two branches are both output statements, although they could have been conditional statements as well. Although the indentation of the statements makes the structure apparent, nested conditionals become difficult to read very quickly. In general, it is a good idea to avoid them when you can. Logical operators often provide a way to simplify nested conditional statements. For example, we can rewrite the following code using a single conditional: if 0 < x:
The print statement is executed only if we make it past both the conditionals, so we can use the and operator: if 0 < x and x < 10:
These kinds of conditions are common, so Python provides an alternative syntax that is similar to mathematical notation: if 0 < x < 10:
This condition is semantically the same as the compound boolean expression and the nested conditional. 4.8 The return statementThe return statement allows you to terminate the execution of a function before you reach the end. One reason to use it is if you detect an error condition: import math
The function printLogarithm has a parameter named x. The first thing it does is check whether x is less than or equal to 0, in which case it displays an error message and then uses return to exit the function. The flow of execution immediately returns to the caller, and the remaining lines of the function are not executed. Remember that to use a function from the math module, you have to import it. 4.9 RecursionWe mentioned that it is legal for one function to call another, and you have seen several examples of that. We neglected to mention that it is also legal for a function to call itself. It may not be obvious why that is a good thing, but it turns out to be one of the most magical and interesting things a program can do. For example, look at the following function: def countdown(n):
countdown expects the parameter, n, to be a positive
integer. If n is 0, it outputs the word, "Blastoff!"
Otherwise, it outputs n and then calls a function named
countdown What happens if we call this function like this: >>> countdown(3)
The execution of countdown begins with n=3, and since n is not 0, it outputs the value 3, and then calls itself... The execution of countdown begins with n=2, and since n is not 0, it outputs the value 2, and then calls itself...The execution of countdown begins with n=1, and since n is not 0, it outputs the value 1, and then calls itself...The execution of countdown begins with n=0, and since n is 0, it outputs the word, "Blastoff!" and then returns. The countdown that got n=3 returns. And then you're back in __main__ (what a trip). So, the total output looks like this: 3
As a second example, look again at the functions newLine and threeLines: def newline():
Although these work, they would not be much help if we wanted to output 2 newlines, or 106. A better alternative would be this: def nLines(n):
This program is similar to countdown; as long as n is greater than 0, it outputs one newline and then calls itself to output n-1 additional newlines. Thus, the total number of newlines is 1 + (n - 1) which, if you do your algebra right, comes out to n. The process of a function calling itself is recursion, and such functions are said to be recursive. 4.10 Stack diagrams for recursive functionsIn Section 3.11, we used a stack diagram to represent the state of a program during a function call. The same kind of diagram can help interpret a recursive function. Every time a function gets called, Python creates a new function frame, which contains the function's local variables and parameters. For a recursive function, there might be more than one frame on the stack at the same time. This figure shows a stack diagram for countdown called with n = 3:
As usual, the top of the stack is the frame for __main__. It is empty because we did not create any variables in __main__ or pass any arguments to it. The four countdown frames have different values for the parameter n. The bottom of the stack, where n=0, is called the base case. It does not make a recursive call, so there are no more frames. As an exercise, draw a stack diagram for nLines called with n=4. 4.11 Infinite recursionIf a recursion never reaches a base case, it goes on making recursive calls forever, and the program never terminates. This is known as infinite recursion, and it is generally not considered a good idea. Here is a minimal program with an infinite recursion: def recurse():
In most programming environments, a program with infinite recursion does not really run forever. Python reports an error message when the maximum recursion depth is reached: File "<stdin>", line 2, in recurse
This traceback is a little bigger than the one we saw in the previous chapter. When the error occurs, there are 100 recurse frames on the stack! As an exercise, write a function with infinite recursion and run it in the Python interpreter. 4.12 Keyboard inputThe programs we have written so far are a bit rude in the sense that they accept no input from the user. They just do the same thing every time. Python provides built-in functions that get input from the keyboard. The simplest is called raw_input. When this function is called, the program stops and waits for the user to type something. When the user presses Return or the Enter key, the program resumes and raw_input returns what the user typed as a string: >>> input = raw_input ()
Before calling raw_input, it is a good idea to print a message telling the user what to input. This message is called a prompt. We can supply a prompt as an argument to raw_input: >>> name = raw_input ("What...is your name? ")
If we expect the response to be an integer, we can use the input function: prompt = "What...is the airspeed velocity of an unladen swallow?\n"
The sequence \n at the end of the string represents a newline, so the user's input appears below the prompt. If the user types a string of digits, it is converted to an integer and assigned to speed. Unfortunately, if the user types a character that is not a digit, the program crashes: >>> speed = input (prompt)
To avoid this kind of error, it is generally a good idea to use raw_input to get a string and then use conversion functions to convert to other types. 4.13 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Appendix DRecommendations for further readingSo where do you go from here? There are many directions to pursue, extending your knowledge of Python specifically and computer science in general. The examples in this book have been deliberately simple, but they may not have shown off Python's most exciting capabilities. Here is a sampling of extensions to Python and suggestions for projects that use them.
Another popular platform is wxPython, which is essentially a Python veneer over wxWindows, a C++ package which in turn implements windows using native interfaces on Windows and Unix (including Linux) platforms. The windows and controls under wxPython tend to have a more native look and feel than those of Tkinter and are somewhat simpler to program. Any type of GUI programming will lead you into event-driven programming, where the user and not the programmer determines the flow of execution. This style of programming takes some getting used to, sometimes forcing you to rethink the whole structure of a program. Python-related web sites and booksHere are the authors' recommendations for Python resources on the web:
And here are some books that contain more material on the Python language:
Recommended general computer science booksThe following suggestions for further reading include many of the authors' favorite books. They deal with good programming practices and computer science in general.
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 7Strings7.1 A compound data type
So far we have seen three types: int, float, and string. Strings are qualitatively different from the
other two because they are made up of smaller pieces Types that comprise smaller pieces are called compound data types. Depending on what we are doing, we may want to treat a compound data type as a single thing, or we may want to access its parts. This ambiguity is useful. The bracket operator selects a single character from a string. >>> fruit = "banana"
The expression fruit[1] selects character number 1 from fruit. The variable letter refers to the result. When we display letter, we get a surprise: a
The first letter of "banana" is not a. Unless you are a computer scientist. In that case you should think of the expression in brackets as an offset from the beginning of the string, and the offset of the first letter is zero. So b is the 0th letter ("zero-eth") of "banana", a is the 1th letter ("one-eth"), and n is the 2th ("two-eth") letter. To get the first letter of a string, you just put 0, or any expression with the value 0, in the brackets: >>> letter = fruit[0]
The expression in brackets is called an index. An index specifies a member of an ordered set, in this case the set of characters in the string. The index indicates which one you want, hence the name. It can be any integer expression. 7.2 LengthThe len function returns the number of characters in a string: >>> fruit = "banana"
To get the last letter of a string, you might be tempted to try something like this: length = len(fruit)
That won't work. It causes the runtime error IndexError: string
length = len(fruit)
Alternatively, we can use negative indices, which count backward from the end of the string. The expression fruit[-1] yields the last letter, fruit[-2] yields the second to last, and so on. 7.3 Traversal and the for loopA lot of computations involve processing a string one character at a time. Often they start at the beginning, select each character in turn, do something to it, and continue until the end. This pattern of processing is called a traversal. One way to encode a traversal is with a while statement: index = 0
This loop traverses the string and displays each letter on a line by itself. The loop condition is index < len(fruit), so when index is equal to the length of the string, the condition is false, and the body of the loop is not executed. The last character accessed is the one with the index len(fruit)-1, which is the last character in the string. As an exercise, write a function that takes a string as an argument and outputs the letters backward, one per line.
Using an index to
traverse a set of values is so common that
Python provides an alternative, simpler syntax for char in fruit:
Each time through the loop, the next character in the string is assigned to the variable char. The loop continues until no characters are left. The following example shows how to use concatenation and a for loop to generate an abecedarian series. "Abecedarian" refers to a series or list in which the elements appear in alphabetical order. For example, in Robert McCloskey's book Make Way for Ducklings, the names of the ducklings are Jack, Kack, Lack, Mack, Nack, Ouack, Pack, and Quack. This loop outputs these names in order: prefixes = "JKLMNOPQ"
The output of this program is: Jack
Of course, that's not quite right because "Ouack" and "Quack" are misspelled. As an exercise, modify the program to fix this error. 7.4 String slicesA segment of a string is called a slice. Selecting a slice is similar to selecting a character: >>> s = "Peter, Paul, and Mary"
The operator [n:m] returns the part of the string from the "n-eth" character to the "m-eth" character, including the first but excluding the last. This behavior is counterintuitive; it makes more sense if you imagine the indices pointing between the characters, as in the following diagram:
If you omit the first index (before the colon), the slice starts at the beginning of the string. If you omit the second index, the slice goes to the end of the string. Thus: >>> fruit = "banana"
7.5 String comparisonThe comparison operators work on strings. To see if two strings are equal: if word == "banana":
Other comparison operations are useful for putting words in alphabetical order: if word < "banana":
You should be aware, though, that Python does not handle upper- and lowercase letters the same way that people do. All the uppercase letters come before all the lowercase letters. As a result: Your word, Zebra, comes before banana.
A common way to address this problem is to convert strings to a standard format, such as all lowercase, before performing the comparison. A more difficult problem is making the program realize that zebras are not fruit. 7.6 Strings are immutableIt is tempting to use the [] operator on the left side of an assignment, with the intention of changing a character in a string. For example: greeting = "Hello, world!"
Instead of producing the output Jello, world!, this code
produces the runtime error TypeError: object doesn't support item
Strings are immutable, which means you can't change an existing string. The best you can do is create a new string that is a variation on the original: greeting = "Hello, world!"
The solution here is to concatenate a new first letter onto a slice of greeting. This operation has no effect on the original string. 7.7 A find functionWhat does the following function do? def find(str, ch):
In a sense, find is the opposite of the [] operator. Instead of taking an index and extracting the corresponding character, it takes a character and finds the index where that character appears. If the character is not found, the function returns -1. This is the first example we have seen of a return statement inside a loop. If str[index] == ch, the function returns immediately, breaking out of the loop prematurely. If the character doesn't appear in the string, then the program exits the loop normally and returns -1. This pattern of computation is sometimes called a "eureka" traversal because as soon as we find what we are looking for, we can cry "Eureka!" and stop looking. As an exercise, modify the find function so that it has a third parameter, the index in the string where it should start looking. 7.8 Looping and countingThe following program counts the number of times the letter a appears in a string: fruit = "banana"
This program demonstrates another pattern of computation called a counter. The variable count is initialized to 0 and then
incremented each time an a is found. (To increment is to
increase by one; it is the opposite of decrement, and unrelated
to "excrement," which is a noun.) When the loop exits, count
contains the result As an exercise, encapsulate this code in a function named countLetters, and generalize it so that it accepts the string and the letter as arguments. As a second exercise, rewrite this function so that instead of traversing the string, it uses the three-parameter version of find from the previous. 7.9 The string moduleThe string module contains useful functions that manipulate strings. As usual, we have to import the module before we can use it: >>> import string
The string module includes a function named find that does the same thing as the function we wrote. To call it we have to specify the name of the module and the name of the function using dot notation. >>> fruit = "banana"
This example demonstrates one of the benefits of modules Actually, string.find is more general than our version. First, it can find substrings, not just characters: >>> string.find("banana", "na")
Also, it takes an additional argument that specifies the index it should start at: >>> string.find("banana", "na", 3)
Or it can take two additional arguments that specify a range of indices: >>> string.find("bob", "b", 1, 2)
In this example, the search fails because the letter b does not appear in the index range from 1 to 2 (not including 2). 7.10 Character classificationIt is often helpful to examine a character and test whether it is upper- or lowercase, or whether it is a character or a digit. The string module provides several constants that are useful for these purposes. The string string.lowercase contains all of the letters that the system considers to be lowercase. Similarly, string.uppercase contains all of the uppercase letters. Try the following and see what you get: >>> print string.lowercase
We can use these constants and find to classify characters. For example, if find(lowercase, ch) returns a value other than -1, then ch must be lowercase: def isLower(ch):
Alternatively, we can take advantage of the in operator, which determines whether a character appears in a string: def isLower(ch):
As yet another alternative, we can use the comparison operator: def isLower(ch):
If ch is between a and z, it must be a lowercase letter. As an exercise, discuss which version of isLower you think will be fastest. Can you think of other reasons besides speed to prefer one or the other? Another constant defined in the string module may surprise you when you print it: >>> print string.whitespace
Whitespace characters move the cursor without printing anything. They create the white space between visible characters (at least on white paper). The constant string.whitespace contains all the whitespace characters, including space, tab (\t), and newline (\n). There are other useful functions in the string module, but this book isn't intended to be a reference manual. On the other hand, the Python Library Reference is. Along with a wealth of other documentation, it's available from the Python website, www.python.org. 7.11 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 13Classes and functions13.1 TimeAs another example of a user-defined type, we'll define a class called Time that records the time of day. The class definition looks like this: class Time:
We can create a new Time object and assign attributes for hours, minutes, and seconds: time = Time()
The state diagram for the Time object looks like this:
As an exercise, write a function printTime that takes a Time object as an argument and prints it in the form hours:minutes:seconds. As a second exercise, write a boolean function after that takes two Time objects, t1 and t2, as arguments, and returns True if t1 follows t2 chronologically and False otherwise. 13.2 Pure functionsIn the next few sections, we'll write two versions of a function called addTime, which calculates the sum of two Times. They will demonstrate two kinds of functions: pure functions and modifiers. The following is a rough version of addTime: def addTime(t1, t2):
The function creates a new Time object, initializes its attributes, and returns a reference to the new object. This is called a pure function because it does not modify any of the objects passed to it as arguments and it has no side effects, such as displaying a value or getting user input. Here is an example of how to use this function. We'll create two Time objects: currentTime, which contains the current time; and breadTime, which contains the amount of time it takes for a breadmaker to make bread. Then we'll use addTime to figure out when the bread will be done. If you haven't finished writing printTime yet, take a look ahead to Section 14.2 before you try this: >>> currentTime = Time()
The output of this program is 12:49:30, which is correct. On the other hand, there are cases where the result is not correct. Can you think of one? The problem is that this function does not deal with cases where the number of seconds or minutes adds up to more than sixty. When that happens, we have to "carry" the extra seconds into the minutes column or the extra minutes into the hours column. Here's a second corrected version of the function: def addTime(t1, t2):
Although this function is correct, it is starting to get big. Later we will suggest an alternative approach that yields shorter code. 13.3 ModifiersThere are times when it is useful for a function to modify one or more of the objects it gets as arguments. Usually, the caller keeps a reference to the objects it passes, so any changes the function makes are visible to the caller. Functions that work this way are called modifiers. increment, which adds a given number of seconds to a Time object, would be written most naturally as a modifier. A rough draft of the function looks like this: def increment(time, seconds):
The first line performs the basic operation; the remainder deals with the special cases we saw before. Is this function correct? What happens if the parameter seconds is much greater than sixty? In that case, it is not enough to carry once; we have to keep doing it until seconds is less than sixty. One solution is to replace the if statements with while statements: def increment(time, seconds):
This function is now correct, but it is not the most efficient solution. As an exercise, rewrite this function so that it doesn't contain any loops. As a second exercise, rewrite increment as a pure function, and write function calls to both versions. 13.4 Which is better?Anything that can be done with modifiers can also be done with pure functions. In fact, some programming languages only allow pure functions. There is some evidence that programs that use pure functions are faster to develop and less error-prone than programs that use modifiers. Nevertheless, modifiers are convenient at times, and in some cases, functional programs are less efficient. In general, we recommend that you write pure functions whenever it is reasonable to do so and resort to modifiers only if there is a compelling advantage. This approach might be called a functional programming style. 13.5 Prototype development versus planningIn this chapter, we demonstrated an approach to program development that we call prototype development. In each case, we wrote a rough draft (or prototype) that performed the basic calculation and then tested it on a few cases, correcting flaws as we found them.
Although this approach can be effective, it can lead to code that is
unnecessarily complicated An alternative is planned development, in which high-level insight into the problem can make the programming much easier. In this case, the insight is that a Time object is really a three-digit number in base 60! The second component is the "ones column," the minute component is the "sixties column," and the hour component is the "thirty-six hundreds column." When we wrote addTime and increment, we were effectively doing addition in base 60, which is why we had to carry from one column to the next.
This observation suggests another approach to the whole problem def convertToSeconds(t):
Now, all we need is a way to convert from an integer to a Time object: def makeTime(seconds):
You might have to think a bit to convince yourself that this function is correct. Assuming you are convinced, you can use it and convertToSeconds to rewrite addTime: def addTime(t1, t2):
This version is much shorter than the original, and it is much easier to demonstrate that it is correct. As an exercise, rewrite increment the same way. 13.6 GeneralizationIn some ways, converting from base 60 to base 10 and back is harder than just dealing with times. Base conversion is more abstract; our intuition for dealing with times is better. But if we have the insight to treat times as base 60 numbers and make the investment of writing the conversion functions (convertToSeconds and makeTime), we get a program that is shorter, easier to read and debug, and more reliable. It is also easier to add features later. For example, imagine subtracting two Times to find the duration between them. The naïve approach would be to implement subtraction with borrowing. Using the conversion functions would be easier and more likely to be correct. Ironically, sometimes making a problem harder (or more general) makes it easier (because there are fewer special cases and fewer opportunities for error). 13.7 AlgorithmsWhen you write a general solution for a class of problems, as opposed to a specific solution to a single problem, you have written an algorithm. We mentioned this word before but did not define it carefully. It is not easy to define, so we will try a couple of approaches. First, consider something that is not an algorithm. When you learned to multiply single-digit numbers, you probably memorized the multiplication table. In effect, you memorized 100 specific solutions. That kind of knowledge is not algorithmic. But if you were "lazy," you probably cheated by learning a few tricks. For example, to find the product of n and 9, you can write n-1 as the first digit and 10-n as the second digit. This trick is a general solution for multiplying any single-digit number by 9. That's an algorithm! Similarly, the techniques you learned for addition with carrying, subtraction with borrowing, and long division are all algorithms. One of the characteristics of algorithms is that they do not require any intelligence to carry out. They are mechanical processes in which each step follows from the last according to a simple set of rules. In our opinion, it is embarrassing that humans spend so much time in school learning to execute algorithms that, quite literally, require no intelligence. On the other hand, the process of designing algorithms is interesting, intellectually challenging, and a central part of what we call programming. Some of the things that people do naturally, without difficulty or conscious thought, are the hardest to express algorithmically. Understanding natural language is a good example. We all do it, but so far no one has been able to explain how we do it, at least not in the form of an algorithm. 13.8 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 14Classes and methods14.1 Object-oriented featuresPython is an object-oriented programming language, which means that it provides features that support object-oriented programming. It is not easy to define object-oriented programming, but we have already seen some of its characteristics:
For example, the Time class defined in Chapter 13 corresponds to the way people record the time of day, and the functions we defined correspond to the kinds of things people do with times. Similarly, the Point and Rectangle classes correspond to the mathematical concepts of a point and a rectangle. So far, we have not taken advantage of the features Python provides to support object-oriented programming. Strictly speaking, these features are not necessary. For the most part, they provide an alternative syntax for things we have already done, but in many cases, the alternative is more concise and more accurately conveys the structure of the program. For example, in the Time program, there is no obvious connection between the class definition and the function definitions that follow. With some examination, it is apparent that every function takes at least one Time object as an argument. This observation is the motivation for methods. We have already seen some methods, such as keys and values, which were invoked on dictionaries. Each method is associated with a class and is intended to be invoked on instances of that class. Methods are just like functions, with two differences:
In the next few sections, we will take the functions from the previous two chapters and transform them into methods. This transformation is purely mechanical; you can do it simply by following a sequence of steps. If you are comfortable converting from one form to another, you will be able to choose the best form for whatever you are doing. 14.2 printTimeIn Chapter 13, we defined a class named Time and you wrote a function named printTime, which should have looked something like this: class Time:
To call this function, we passed a Time object as an argument: >>> currentTime = Time()
To make printTime a method, all we have to do is move the function definition inside the class definition. Notice the change in indentation. class Time:
Now we can invoke printTime using dot notation. >>> currentTime.printTime()
As usual, the object on which the method is invoked appears before the dot and the name of the method appears after the dot. The object on which the method is invoked is assigned to the first parameter, so in this case currentTime is assigned to the parameter time. By convention, the first parameter of a method is called self. The reason for this is a little convoluted, but it is based on a useful metaphor. The syntax for a function call, printTime(currentTime), suggests that the function is the active agent. It says something like, "Hey printTime! Here's an object for you to print." In object-oriented programming, the objects are the active agents. An invocation like currentTime.printTime() says "Hey currentTime! Please print yourself!" This change in perspective might be more polite, but it is not obvious that it is useful. In the examples we have seen so far, it may not be. But sometimes shifting responsibility from the functions onto the objects makes it possible to write more versatile functions, and makes it easier to maintain and reuse code. 14.3 Another exampleLet's convert increment (from Section 13.3) to a method. To save space, we will leave out previously defined methods, but you should keep them in your version: class Time:
The transformation is purely mechanical Now we can invoke increment as a method. currentTime.increment(500)
Again, the object on which the method is invoked gets assigned to the first parameter, self. The second parameter, seconds gets the value 500. As an exercise, convert convertToSeconds (from Section 13.5) to a method in the Time class. 14.4 A more complicated exampleThe after function is slightly more complicated because it operates on two Time objects, not just one. We can only convert one of the parameters to self; the other stays the same: class Time:
We invoke this method on one object and pass the other as an argument: if doneTime.after(currentTime):
You can almost read the invocation like English: "If the done-time is after the current-time, then..." 14.5 Optional argumentsWe have seen built-in functions that take a variable number of arguments. For example, string.find can take two, three, or four arguments. It is possible to write user-defined functions with optional argument lists. For example, we can upgrade our own version of find to do the same thing as string.find. This is the original version from Section 7.7: def find(str, ch):
This is the new and improved version: def find(str, ch, start=0):
The third parameter, start, is optional because a default value, 0, is provided. If we invoke find with only two arguments, it uses the default value and starts from the beginning of the string: >>> find("apple", "p")
If we provide a third argument, it overrides the default: >>> find("apple", "p", 2)
As an exercise, add a fourth parameter, end, that specifies where to stop looking. 14.6 The initialization methodThe initialization method is a special method that is invoked when an object is created. The name of this method is __init__ (two underscore characters, followed by init, and then two more underscores). An initialization method for the Time class looks like this: class Time:
There is no conflict between the attribute self.hours and the parameter hours. Dot notation specifies which variable we are referring to. When we invoke the Time constructor, the arguments we provide are passed along to init: >>> currentTime = Time(9, 14, 30)
Because the arguments are optional, we can omit them: >>> currentTime = Time()
Or provide only the first: >>> currentTime = Time (9)
Or the first two: >>> currentTime = Time (9, 14)
Finally, we can make assignments to a subset of the parameters by naming them explicitly: >>> currentTime = Time(seconds = 30, hours = 9)
14.7 Points revisitedLet's rewrite the Point class from Section 12.1 in a more object-oriented style: class Point:
The initialization method takes x and y values as optional parameters; the default for either parameter is 0. The next method, __str__, returns a string representation of a Point object. If a class provides a method named __str__, it overrides the default behavior of the Python built-in str function. >>> p = Point(3, 4)
Printing a Point object implicitly invokes __str__ on the object, so defining __str__ also changes the behavior of print: >>> p = Point(3, 4)
When we write a new class, we almost always start by writing __init__, which makes it easier to instantiate objects, and __str__, which is almost always useful for debugging. 14.8 Operator overloadingSome languages make it possible to change the definition of the built-in operators when they are applied to user-defined types. This feature is called operator overloading. It is especially useful when defining new mathematical types. For example, to override the addition operator +, we provide a method named __add__: class Point:
As usual, the first parameter is the object on which the method is invoked. The second parameter is conveniently named other to distinguish it from self. To add two Points, we create and return a new Point that contains the sum of the x coordinates and the sum of the y coordinates. Now, when we apply the + operator to Point objects, Python invokes __add__: >>> p1 = Point(3, 4)
The expression p1 + p2 is equivalent to p1.__add__(p2), but obviously more elegant. As an exercise, add a method __sub__(self, other) that overloads the subtraction operator, and try it out. There are several ways to override the behavior of the multiplication operator: by defining a method named __mul__, or __rmul__, or both. If the left operand of * is a Point, Python invokes __mul__, which assumes that the other operand is also a Point. It computes the dot product of the two points, defined according to the rules of linear algebra: def __mul__(self, other):
If the left operand of * is a primitive type and the right operand is a Point, Python invokes __rmul__, which performs scalar multiplication: def __rmul__(self, other):
The result is a new Point whose coordinates are a multiple of the original coordinates. If other is a type that cannot be multiplied by a floating-point number, then __rmul__ will yield an error. This example demonstrates both kinds of multiplication: >>> p1 = Point(3, 4)
What happens if we try to evaluate p2 * 2? Since the first operand is a Point, Python invokes __mul__ with 2 as the second argument. Inside __mul__, the program tries to access the x coordinate of other, which fails because an integer has no attributes: >>> print p2 * 2
Unfortunately, the error message is a bit opaque. This example demonstrates some of the difficulties of object-oriented programming. Sometimes it is hard enough just to figure out what code is running. For a more complete example of operator overloading, see Appendix B. 14.9 PolymorphismMost of the methods we have written only work for a specific type. When you create a new object, you write methods that operate on that type. But there are certain operations that you will want to apply to many types, such as the arithmetic operations in the previous sections. If many types support the same set of operations, you can write functions that work on any of those types. For example, the multadd operation (which is common in linear algebra) takes three arguments; it multiplies the first two and then adds the third. We can write it in Python like this: def multadd (x, y, z):
This method will work for any values of x and y that can be multiplied and for any value of z that can be added to the product. We can invoke it with numeric values: >>> multadd (3, 2, 1)
Or with Points: >>> p1 = Point(3, 4)
In the first case, the Point is multiplied by a scalar and then added to another Point. In the second case, the dot product yields a numeric value, so the third argument also has to be a numeric value. A function like this that can take arguments with different types is called polymorphic. As another example, consider the method frontAndBack, which prints a list twice, forward and backward: def frontAndBack(front):
Because the reverse method is a modifier, we make a copy of the list before reversing it. That way, this method doesn't modify the list it gets as an argument. Here's an example that applies frontAndBack to a list: >>> myList = [1, 2, 3, 4]
Of course, we intended to apply this function to lists, so it is not surprising that it works. What would be surprising is if we could apply it to a Point. To determine whether a function can be applied to a new type, we apply the fundamental rule of polymorphism: If all of the operations inside the function can be applied to the type, the function can be applied to the type. The operations in the method include copy, reverse, and print. copy works on any object, and we have already written a __str__ method for Points, so all we need is a reverse method in the Point class: def reverse(self):
Then we can pass Points to frontAndBack: >>> p = Point(3, 4)
The best kind of polymorphism is the unintentional kind, where you discover that a function you have already written can be applied to a type for which you never planned. 14.10 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|
Warning: the HTML version of this document is generated from
Latex and may contain translation errors. In
particular, some mathematical expressions are not translated correctly.
Chapter 5Fruitful functions5.1 Return valuesSome of the built-in functions we have used, such as the math functions, have produced results. Calling the function generates a new value, which we usually assign to a variable or use as part of an expression. e = math.exp(1.0)
But so far, none of the functions we have written has returned a value. In this chapter, we are going to write functions that return values, which we will call fruitful functions, for want of a better name. The first example is area, which returns the area of a circle with the given radius: import math
We have seen the return statement before, but in a fruitful function the return statement includes a return value. This statement means: "Return immediately from this function and use the following expression as a return value." The expression provided can be arbitrarily complicated, so we could have written this function more concisely: def area(radius):
On the other hand, temporary variables like temp often make debugging easier. Sometimes it is useful to have multiple return statements, one in each branch of a conditional: def absoluteValue(x):
Since these return statements are in an alternative conditional, only one will be executed. As soon as one is executed, the function terminates without executing any subsequent statements. Code that appears after a return statement, or any other place the flow of execution can never reach, is called dead code. In a fruitful function, it is a good idea to ensure that every possible path through the program hits a return statement. For example: def absoluteValue(x):
This program is not correct because if x happens to be 0, neither condition is true, and the function ends without hitting a return statement. In this case, the return value is a special value called None: >>> print absoluteValue(0)
As an exercise, write a compare function that returns 1 if x > y, 0 if x == y, and -1 if x < y. 5.2 Program developmentAt this point, you should be able to look at complete functions and tell what they do. Also, if you have been doing the exercises, you have written some small functions. As you write larger functions, you might start to have more difficulty, especially with runtime and semantic errors. To deal with increasingly complex programs, we are going to suggest a technique called incremental development. The goal of incremental development is to avoid long debugging sessions by adding and testing only a small amount of code at a time. As an example, suppose you want to find the distance between two points, given by the coordinates (x1, y1) and (x2, y2). By the Pythagorean theorem, the distance is:
The first step is to consider what a distance function should look like in Python. In other words, what are the inputs (parameters) and what is the output (return value)? In this case, the two points are the inputs, which we can represent using four parameters. The return value is the distance, which is a floating-point value. Already we can write an outline of the function: def distance(x1, y1, x2, y2):
Obviously, this version of the function doesn't compute distances; it always returns zero. But it is syntactically correct, and it will run, which means that we can test it before we make it more complicated. To test the new function, we call it with sample values: >>> distance(1, 2, 4, 6)
We chose these values so that the horizontal distance equals 3 and the vertical distance equals 4; that way, the result is 5 (the hypotenuse of a 3-4-5 triangle). When testing a function, it is useful to know the right answer.
At this point we have confirmed that the function is syntactically
correct, and we can start adding lines of code. After each
incremental change, we test the function again. If an error occurs at
any point, we know where it must be A logical first step in the computation is to find the differences x2 - x1 and y2 - y1. We will store those values in temporary variables named dx and dy and print them. def distance(x1, y1, x2, y2):
If the function is working, the outputs should be 3 and 4. If so, we know that the function is getting the right arguments and performing the first computation correctly. If not, there are only a few lines to check. Next we compute the sum of squares of dx and dy: def distance(x1, y1, x2, y2):
Notice that we removed the print statements we wrote in the previous step. Code like that is called scaffolding because it is helpful for building the program but is not part of the final product. Again, we would run the program at this stage and check the output (which should be 25). Finally, if we have imported the math module, we can use the sqrt function to compute and return the result: def distance(x1, y1, x2, y2):
If that works correctly, you are done. Otherwise, you might want to print the value of result before the return statement. When you start out, you should add only a line or two of code at a time. As you gain more experience, you might find yourself writing and debugging bigger chunks. Either way, the incremental development process can save you a lot of debugging time. The key aspects of the process are:
As an exercise, use incremental development to write a function called hypotenuse that returns the length of the hypotenuse of a right triangle given the lengths of the two legs as arguments. Record each stage of the incremental development process as you go. 5.3 CompositionAs you should expect by now, you can call one function from within another. This ability is called composition. As an example, we'll write a function that takes two points, the center of the circle and a point on the perimeter, and computes the area of the circle. Assume that the center point is stored in the variables xc and yc, and the perimeter point is in xp and yp. The first step is to find the radius of the circle, which is the distance between the two points. Fortunately, there is a function, distance, that does that: radius = distance(xc, yc, xp, yp)
The second step is to find the area of a circle with that radius and return it: result = area(radius)
Wrapping that up in a function, we get: def area2(xc, yc, xp, yp):
We called this function area2 to distinguish it from the area function defined earlier. There can only be one function with a given name within a given module. The temporary variables radius and result are useful for development and debugging, but once the program is working, we can make it more concise by composing the function calls: def area2(xc, yc, xp, yp):
As an exercise, write a function slope(x1, y1, x2, y2) that returns the slope of the line through the points (x1, y1) and (x2, y2). Then use this function in a function called intercept(x1, y1, x2, y2) that returns the y-intercept of the line through the points (x1, y1) and (x2, y2). 5.4 Boolean functionsFunctions can return boolean values, which is often convenient for hiding complicated tests inside functions. For example: def isDivisible(x, y):
The name of this function is isDivisible. It is common to give boolean functions names that sound like yes/no questions. isDivisible returns either True or False to indicate whether the x is or is not divisible by y. We can make the function more concise by taking advantage of the fact that the condition of the if statement is itself a boolean expression. We can return it directly, avoiding the if statement altogether: def isDivisible(x, y):
This session shows the new function in action: >>> isDivisible(6, 4)
Boolean functions are often used in conditional statements: if isDivisible(x, y):
It might be tempting to write something like: if isDivisible(x, y) == True:
But the extra comparison is unnecessary. As an exercise, write a function isBetween(x, y, z) that returns True if y le x le z or False otherwise. 5.5 More recursionSo far, you have only learned a small subset of Python, but you might be interested to know that this subset is a complete programming language, which means that anything that can be computed can be expressed in this language. Any program ever written could be rewritten using only the language features you have learned so far (actually, you would need a few commands to control devices like the keyboard, mouse, disks, etc., but that's all). Proving that claim is a nontrivial exercise first accomplished by Alan Turing, one of the first computer scientists (some would argue that he was a mathematician, but a lot of early computer scientists started as mathematicians). Accordingly, it is known as the Turing Thesis. If you take a course on the Theory of Computation, you will have a chance to see the proof. To give you an idea of what you can do with the tools you have learned so far, we'll evaluate a few recursively defined mathematical functions. A recursive definition is similar to a circular definition, in the sense that the definition contains a reference to the thing being defined. A truly circular definition is not very useful:
If you saw that definition in the dictionary, you might be annoyed. On the other hand, if you looked up the definition of the mathematical function factorial, you might get something like this:
This definition says that the factorial of 0 is 1, and the factorial of any other value, n, is n multiplied by the factorial of n-1. So 3! is 3 times 2!, which is 2 times 1!, which is 1 times 0!. Putting it all together, 3! equals 3 times 2 times 1 times 1, which is 6. If you can write a recursive definition of something, you can usually write a Python program to evaluate it. The first step is to decide what the parameters are for this function. With little effort, you should conclude that factorial has a single parameter: def factorial(n):
If the argument happens to be 0, all we have to do is return 1: def factorial(n):
Otherwise, and this is the interesting part, we have to make a recursive call to find the factorial of n-1 and then multiply it by n: def factorial(n):
The flow of execution for this program is similar to the flow of countdown in Section 4.9. If we call factorial with the value 3: Since 3 is not 0, we take the second branch and calculate the factorial of n-1... Since 2 is not 0, we take the second branch and calculate the factorial of n-1... The return value (2) is multiplied by n, which is 3, and the result, 6, becomes the return value of the function call that started the whole process. Here is what the stack diagram looks like for this sequence of function calls:
The return values are shown being passed back up the stack. In each frame, the return value is the value of result, which is the product of n and recurse. Notice that in the last frame, the local variables recurse and result do not exist, because the branch that creates them did not execute. 5.6 Leap of faithFollowing the flow of execution is one way to read programs, but it can quickly become labyrinthine. An alternative is what we call the "leap of faith." When you come to a function call, instead of following the flow of execution, you assume that the function works correctly and returns the appropriate value. In fact, you are already practicing this leap of faith when you use built-in functions. When you call math.cos or math.exp, you don't examine the implementations of those functions. You just assume that they work because the people who wrote the built-in functions were good programmers.
The same is true when you call one of your own functions. For example,
in Section 5.4, we wrote a function called isDivisible
that determines whether one number is divisible by another. Once we
have convinced ourselves that this function is correct The same is true of recursive programs. When you get to the recursive call, instead of following the flow of execution, you should assume that the recursive call works (yields the correct result) and then ask yourself, "Assuming that I can find the factorial of n-1, can I compute the factorial of n?" In this case, it is clear that you can, by multiplying by n. Of course, it's a bit strange to assume that the function works correctly when you haven't finished writing it, but that's why it's called a leap of faith! 5.7 One more exampleIn the previous example, we used temporary variables to spell out the steps and to make the code easier to debug, but we could have saved a few lines: def factorial(n):
From now on, we will tend to use the more concise form, but we recommend that you use the more explicit version while you are developing code. When you have it working, you can tighten it up if you are feeling inspired. After factorial, the most common example of a recursively defined mathematical function is fibonacci, which has the following definition:
Translated into Python, it looks like this: def fibonacci (n):
If you try to follow the flow of execution here, even for fairly small values of n, your head explodes. But according to the leap of faith, if you assume that the two recursive calls work correctly, then it is clear that you get the right result by adding them together. 5.8 Checking typesWhat happens if we call factorial and give it 1.5 as an argument? >>> factorial (1.5)
It looks like an infinite recursion. But how can that be? There is a
base case In the first recursive call, the value of n is 0.5. In the next, it is -0.5. From there, it gets smaller and smaller, but it will never be 0. We have two choices. We can try to generalize the factorial function to work with floating-point numbers, or we can make factorial check the type of its argument. The first option is called the gamma function and it's a little beyond the scope of this book. So we'll go for the second. We can use the built-in function isinstance to verify the type of the argument. While we're at it, we also make sure the argument is positive: def factorial (n):
Now we have three base cases. The first catches nonintegers. The second catches negative integers. In both cases, the program prints an error message and returns a special value, -1, to indicate that something went wrong: >>> factorial ("fred")
If we get past both checks, then we know that n is a positive integer, and we can prove that the recursion terminates. This program demonstrates a pattern sometimes called a guardian. The first two conditionals act as guardians, protecting the code that follows from values that might cause an error. The guardians make it possible to prove the correctness of the code. 5.9 Glossary
Warning: the HTML version of this document is generated from Latex and may contain translation errors. In particular, some mathematical expressions are not translated correctly.
|