When reading the following material, I suggest you have Eclipse open, including a project with and empty module, then copy/paste some of the code below into it, to see what it does, and explore it by experimenting with (changing) it. I always have an Eclipse folder/module named "experiment" for this purpose. This lecture is long (it is really two lectures), but the information is not deep (for ICS-31/-32 graduates). I hope this is all review, but there are always a few things that come up new (or a few new perspectives and connections of old material). The voyage of discovery is not in seeking new landscapes but in having new eyes. - M. Proust ------------------------------------------------------------------------------ Python in Three Sentences: 1. Everything we compute with is an object (and each has a namespace). 2. Names are bound to objects 3. There are rules about how things work. In some sense this tells you nothing about Python, but in another sense it is a framework in which to interpret all sorts of Python ideas. Values (like int objects: really objects/instances of the int class), function definitions, class definitions, modules definitions, and generally instances of all classes are all objects. Some objects are bound as the value of a name, typically through importation, assignment, function/class definitions, and argument/parameter matching. Rules determine how argument/parameter bindings work, how control structures work, etc. ------------------------------------------------------------------------------ Binding The process of making a name refer to a value: e.g., x = 1 binds the name x to the value 1 (and object/instance of the int class); later we can bind x to another value in another assignment statement. We speak about "the binding (noun) of a name" to mean the value that the name is associated with. In reality, every module, function, and class has a dictionary that stores its namespace: all its bindings. We will learn much more about namespaces (and how to manipulate them) later in the quarter. ------------------------------------------------------------------------------ None: None is a value (object/instance) of NoneType (the only value of that type). Sometimes None gets used as a default value as a parameter; sometimes it gets used as a return value: a Python function that terminates without executing a returns statement returns the value None. If None shows up in an unexpected place, I'd look for function whose return you forgot to specify (or that somehow didn't get executed). ------------------------------------------------------------------------------ pass: pass is the simplest statement in Python; it means "do nothing". Often, in tiny examples use pass as the meaning of a function. def f(x) : pass or def f(x) : pass If you are ever tempted to write the first if statement below, don't; instead write the second one, which is simpler. if a in s: # DON'T write this code pass else: print('a is not in s') if a not in s: # Write this code print('a is not in s') ------------------------------------------------------------------------------ Importing: There are five import forms; you should know what each does, and start to think about which is appropriate in situations. Note we will soon learn EBNF and see that [...] means option and {...} means repeat 0 or more times, although this is sometimes written (...)* import form: import module{,module} import module [as name] {,module [as name]} from form: from module import name{,name} from module import name [as name] {,name [as name]} from module import * The import forms import just the names of modules (the second line shows how to rename any modules); the from forms don't import the module name at all, but import names defined/bound inside the module (the second line shows renaming; the third line imports all names). Import (like assignment statements, def, and class) creates a name and binds it to an object: the import form binds names to module objects; the from form binds names to objects (values, functions, classes) defined inside modules (the modules have name for this objects). The key idea as to which kind of import to use is to not pollute the name space of the module doing the importing. If a lot of different names in the imported module are to be used, use the import from and then qualify the names: import prompt ... prompt.for_int(...) If only a few names are to be used from a module, use the form form: from random import choice ... choice(...) Use renaming to simplify either form, if the names are too bulky. Such names are often very long to be clear, but awkward to use many times at that length. Generally we should apply the Goldilocks principle: name lengths shouldn't be too short (devoid of meaning) or too long (awkward to readn/type) but "just right". We almost never write the * form of importing, which defines in the current namesspace all the names defined in the module: by definition, name pollution, since we are defining all sorts of names we may or may not use. ------------------------------------------------------------------------------ range: We know that we can iterate (with a for loop) over ranges: e.g. for i in range(0,len(x)) : pass Here i takes on the values 0, 1, ... , len(x)-1 but not x. Often we want to iterate over a list (alist) and don't need to use its indexes at all: e.g., the loop for i in alist : print(i) is much better than the loop for i in range(0,len(alist)) : print(alist[i]) although post produce the same result. In most cases where using range is appropriate, we want to go from the low to high value inclusive. I have written a iterator named irange (for inclusive range) that we can import from the goody module and use like range. from goody import irange for i in irange(1,10) : print(i) prints the values 1 through 10 inclusive. ------------------------------------------------------------------------------ eval/exec: eval is a function that takes a string parameter and evaluates it as an expression (using the current binding of names to compute values) and returns its result. exec is a function that takes a string parameter and evaluates it as a statement (using the current binding of names to compute values) and returns None. In the process of executing an exec, Python might define new names and bind them to values. These are very powerful functions as they allow a programmer/user to interact with a program (specifically with its namespace) and type in code to be evaluated or executed as if it appeared as statements in the program, while the program is running. We will see both uses in the driver programs that I supply. For now, contemplate a module that contains a = 1 b = 2 exec(input('enter a statement:')) # enter: c = 3 print(eval(input('enter an expression:'))) # enter: a+b*c In both cases input returns a string which eval/exec process, as if the contents of that string appeared in the program. For example, writing exec("c = 3") in a Python program is equivalent to executing c = 3. Here is something even more powerful. Type in the script exec('def f(x):\n ' + input('Enter a function definition for def f(x): ')) print(f(3)) If the user enters return 2*x (note the indentation after \n) then the script prints 6, by first defininig the function (exec'ing the string defining the function) and then calling it. So We can write a script that calls a function that doesn't even exist until the user enters it while the script is running. In fact the script shows a syntax error for print(f(3)) because there is no f in this code (but that function gets defined, just in time, in the exec). We can do this for classes just as easily. ------------------------------------------------------------------------------ Aruments and Parameters: Terminology Whenever we define a function, we specify the names of its parameters in its function header (in parentheses). Whenever we call a function we specify the values of its arguments (also in parentheses). def f(x,y) : return x**y defines a function of two parameters: x and y. f(5,2*6) calls the function with two arguments: the values computed by the arguments 5 and 2*6 are evaluated and bound to the parameters (we will discuss the details of argument/parameter binding below). It is important that you understand the distinction between these two technical terms. ------------------------------------------------------------------------------ Function calls ... always include () Any time a reference to an object is followed by (...) it means to perform a function call on that object (some objects will raise an exception if they do not support function calls). While this is trivial for the functions you have been writing and calling, there are some interesting ramifications of this rule. Run the following code def double(x) : return 2*x def triple(x) : return 3*x def magnitude(x) : return 10*x If we wrote f = double then f would be bound to the same (function) object that double is bound to there are no () so there are no function calls). If we then write print(f(5)) Python would print 10, just as it would if we printed double(5), because x and double refer to the same function object, and it makes no difference whether we call it using x or double. Here is a more intersting example, but of exactly the same idea. for f in (double, triple, magnitude) : print(f(5)) Here f is a variable that iterates through the tuple of three functions, printing the value produced by calling each function with the argument 5. We use references to functions to build a tuple (we could have used a list); note the functions are not called when creating the tuple, the tuple is just built to contain references to these three functions: their names are not followed by (). We could also write fs = {'2x' : double, '3x' : triple, '10x' : magnitude} print( fs['3x'](5) ) Here fs is a dictionary that store keys that are str and their associated functions (no calls in the dictionary-building first statement); we then can look up the function associated with any key (here '3x') and then take the functon that is the result and call it with the argument 5. Look at the following function named bigger_than, which is a function of one parameter. Inside, it defines a function named test which also has one parameter, then the bigger_than function returns a reference to the test function it defines. Have you seen functions that return functions before? def bigger_than(c) : def test(x) : return x > c return test Note that the inner function can refer to any information that is in the outer function: here the inner function refers to its own parameter (x) and to the parameter of the bigger_than function (c). Generally when Python looks up the binding of a name, it uses the LEGB rule: (1) Look for a Local name (parameter/local variable) (2) Look for the name in an Enclosing function (3) Look for a Global name (defined in the module outside the functions) (4) Look for a name imorted from Builtins (or any other module) Now, we can do the following: old = bigger_than(60) ancient = bigger_than(90) print (old(10), old(70), old(90), ancient(70), ancient(90)) The first two assignment statements above each bind a name (old and ancient) to he function object that is returned from calling bigger_than. Then we call each function a few times. In fact, we could even have written something like print (bigger_than(60)(70)) We know bigger_than(60) calls that function and returns a reference to its inner function; by writing (70) after that we are again just calling a function, the one bigger_than(60) returned. Note that def f(x): return 2*x is really just creating a name f and binding it to a function object. So return test is returning the function object currently bound to test (and remembering what value c has when test was returned). Note the difference between the following, which both print 6 x = f(3) print(x) and x = f print(x(3)) A large part of this course deals with understanding functions better, including but not restricted to function calls. ------------------------------------------------------------------------------ Functions vs Methods Functions are typically called f(...) while methods are called on objects like o.m(...). In reality, a method call is just a special syntax to write a function call. The special "argument" o (normally arguments are written inside the parentheses) prefixes the method name. Functions and methods are related by what I call "The Fundamental Equation of Object-Oriented Python Programming." o.m(...) = type(o).m(o,...) On the right side 1) type(o) returns a reference to the class object o was constructed from. 2) .m means call the function m declared in that class: look for def m(...) 3) pass o as the first argument to m: recall def m(self, ....) How well do you understand self (or yourself, for that matter)? This equation is the key. I believe a deep understanding of this equation is the key to clarifying object-oriented Python (objects constructed from classes). Just my two cents. I've never seen any books that talk about this equation. Oh, by the way, I must say that the equation is not completely true when you bring inheritance into the discussion (but the equation can be fixed). But this equation is so clear and useful for tons of examples, it is worth memorizing even if not completely accurate. ------------------------------------------------------------------------------ Parallel/Tuple Assignment (aka sequence unpacking) Note that we can write code like the following: x,y and 1,2 are implicit tuples, and we can unpack x,y = 1,2 In fact, you can replace the left hand side of the equal sign by (x,y) or [x,y] and replace the right hand side of the equal sign by (1,2) or [1,2] and x gets assigned 1 and y get assigned 2. In most programming languages, to exchange the values of two variables x and y we write three assignments (do you know why x = y followed by y = x fails?): temp = x x = y y = temp In Python we can write x,y = y,x It computes the values on the right (1 and 2 from the top) and then stores 2 in x and 1 in y. If we define a function that returns a tuple def reverse(a,b) : return (b,a) We can also write x,y = reverse(x,y) to also exchange these values. Finally, we can write the following for loop, to print each triple in the list. for (i,j,k) in [(1,2,3), (4,5,6), (7,8,9)] : print (i,j,k) It is assigning i, j, and k each group of three values as it iterates down the list. ------------------------------------------------------------------------------ lambda: Lambdas are used where we need a very simple function. Instead of defining one (with a def), we just use a lambda: after the word lambda comes its parameters separated by commas, then a colon followed by an expression that computes the value of a lambda (no return needed). Writing ...(lambda x,y : x+y)... Is just like defining def trivial_f(x,y) : return x+y and then writing ...trivial_f... A lambda is like a function name (not a function call). For example, we can also write x = lambda x,y : x+y # lambdas have one expression after : no return is needed print(x(1,3)) and Python will print 4. In my prompt module, there is a function called for_int that has a parameter that specifies a function that the int value must satisfy to be returned by the prompt (otherwise the for_int function prompts again, for a legal value). import prompt x = prompt.for_int('Enter a value in [0,5]', is_legal = (lambda x : 0<=x<=5)) print(x) See below for how lambdas are used simply when sorting. I often put parentheses around lambdas for clarity: see the prompt.for_int. There are certain places where they are required: calling f(1,(lambda x : x),2) because without the parentheses it would read f(1,lambda x : x,2) and Python would realize that 2 was a third argument, it would think the body of the lambda was x,2. ------------------------------------------------------------------------------ sort(a list method)/sorted (an iterator): key and reverse We know the sort function can be applied to lists, and the sorted iterator can apply to any iterable (lists, tuples, sets, dict). If votes is the list of tuples below, we can execute the following code votes = [('Charlie', 20), ('Able', 10), ('Baker', 20), ('Dog', 15)] votes.sort() for (c,v) in votes : print('Candidate',c,'got',v,'votes') print(votes) The call votes.sort() sorts the list (mutates it); then the for loop prints the information in the list (which is now sorted, so it prints sorted); when we print the votes list at the end, we see the list has been sorted. Contrast this with the following. votes = [('Charlie', 20), ('Able', 10), ('Baker' ,20), ('Dog', 15)] for (c,v) in sorted(votes) : print('Candidate',c,'got',v,'votes') print(votes) Here we never sort/mutate the list. Instead we use the sorted iterator to print the information in the list in sorted form, but when we print the votes list at the end, we see the list remained unchanged The sorted iterator makes a copy of its parameter list, sorts the copy, and then iterates over the sorted copy. If we were going to print some list in sorted form many times, it would be more efficient to sort it once and then just just the regular iterator. But if we needed to keep the list in a certain order and/or didn't care too much about time (milliseconds) we would not sort/mutate the list and use the sorted iterator. Note that if we change votes to a dict, we cannot write votes.sort(), but we can still use the sorted iterator on it: sorted(votes) is the same as sorted(votes.keys(). We cannot sort tuples (they are immutable), sets (they have no order, which actually allows them to operate more quickly; we'll learn why later inthe quarter), or dicts (they have no order, which allows them to operate more quickly; ditto) but we can use the sorted iterator on all three. Here is an example of votes as a dict. votes = {'Charlie' : 20, 'Able' : 10, 'Baker' : 20, 'Dog' : 15} for c in sorted(votes) : print('Candidate',c,'got',votes[c],'votes') print(votes) Well, this is the "normal" way to write this. We can also iterate over sorted "items" as follows (the difference is in the for loop and print. We talk much more on Friday about dicts and their different iterators. votes = {'Charlie' : 20, 'Able' : 10, 'Baker' : 20, 'Dog' : 15} for c,v in sorted(votes.items()) : print('Candidate',c,'got',v,'votes') print(votes) Notice the print doesn't access votes[c] to get the votes, that is the second item in each tuple being iterated over using the .items() iterator. So, how does sort/sorted work? How do they know how to compare the values it is sorting? There is a standard way, or we can uses the key and reverse parameters (which must be used with their names, not positionally). reverse is simpler so let's look at it first; writing sorted(votes,reverse=True) sorts but in the reverse order (reverse=False is like not using reverse at all). Try it in the code above. What sort is doing is comparing each value in the list to the others using the standard meaning of <. You should know how Python compares str values, but how does Python compare whether one tuple is less than another? The algorithm in analagous to strings, so let's re-examine strings. The order, by the way, is called lexicographic ordering, and also dictionary ordering (related to the order words appear in dictionaries, unrelated to Python dicts). --------------- String comparison: x to y (high level: I could write code: a good thing for you to try: write an is_less function). Find the first/minimum i such that the ith character in x and y are different. If that character in x is less than y (by the standard ASCII table) then x is less than y; if that character in x is greater than y then x is greater than y; if there is no such character, then x compares to y as x's length compares to y's (either less than, equal or greater than). So how do we compare x = 'aunt' and y = 'anteater'? The first/minimum i such that the characters are different is 1: x[1] is 'u' and y[1] is 'n'; 'n' is less than 'u' so x > y (or y < x). How do we compare x = 'ant' and y = 'anteater'? There is not first/minimum i such that the characters are different; len(x) < len(y) so x < y. The word 'ant' appears in a book dictionary before 'anteater'. See the handout on ASCII ordering because there are some suprises. You should memorize that the digits and lower/upper case letters compare in order, with all digts < all upper-case letters < all lower-case letters. So 'HUGE' < 'tiny' because 'H' is < 't' (allow upper-case letters have smaller ASCII values than any upper-lower case letters).. --------------- Back to tuples (or lists). We basically do the same thing. We look at what is in index 0; if different, then the tuples compare in the same way as the values in index 0 compare; if the same, we keep going until we find a difference and compare the tuples the same way that the differences compare; if there are no differences the tuples compare the same way their lengths compare. So ('UCI', 100) < ('UCSD', 50) because 'UCI' is < 'UCSD' because 'I' < 'S'. Whereas ('UCI', 100) < ('UCI', 200) because 'UCI' is == 'UCI', so we go to the next index, where we find 100 < 200. Finally, ('UCI', 100) < ('UCI', 100, 'extra') because 'UCI' is == 'UCI' and 100 = 100, and the last tuple has a larger length. So, getting back to Python code, the reason the values come out in the order they do in the code above is because the names that are first index in each tuple ensure the tuples are sorted alphabetically. Python never gets to looking at the second value in each tuple, because the first values are always different. To repeat. votes = [('Charlie', 20), ('Able', 10), ('Baker' ,20), ('Dog', 15)] for (c,v) in sorted(votes) : print('Candidate',c,'got',v,'votes') print(votes) Now, what if we don't want to sort by the natural tuple ordering. We can specify a key function that computes a value for every tuple, and the computed values are used for comparison, not the original values themselves. These are the so called "keys" for comparison. So, the vote_count function below takes a tuple parameter and returns only the second value in the tuple (recall indexes start at 0) for the key on which Python will compare. If we give it a tuple like ('Baker' ,20) it returns 20. def vote_count(t): return t[1] # remember t[0] is the first. So, when we sort with key=vote_count, we are telling sort to compare values after calling the vote_count function: so in this call of sorted, we are comparing tuples by their vote part. So writing votes = [('Charlie', 20), ('Able', 10), ('Baker' ,20), ('Dog', 15)] for (c,v) in sorted(votes,key=vote_count) : print('Candidate',c,'got',v,'votes') produces Candidate Able got 10 votes Candidate Dog got 15 votes Candidate Charlie got 20 votes Candidate Baker got 20 votes First, notice I didn't CALL vote_count (no parentheses) I just passed its function object to the key parameter in sort). Notice that the values are printed in ascending votes; generally for elections we want the votes to be descending, so we use reverse=True in sorted. votes = [('Charlie', 20), ('Able', 10), ('Baker' ,20), ('Dog', 15)] for (c,v) in sorted(votes,key=vote_count,reverse=True): print('Candidate',c,'got',v,'votes') which produces Candidate Charlie got 20 votes Candidate Baker got 20 votes Candidate Dog got 15 votes Candidate Able got 10 votes Now, rather than define this simple vote_count function, we can use a lambda instead, and write the following. for (c,v) in sorted(votes, (key=lambda t : t[1]), reverse=True): So, now we don't have to write that extra vote_count function. Of course, if we did write it, we could reuse it wherever we wanted, instead of rewriting the lambda (but the lambdas are pretty small). So, know how to use functions and lambdas; we'll talk more about the judgement needed to determine when to use each later in the quarter. Another way to sort in reverse order (for integers) is as follows. for (c,v) in sorted(votes,key=lambda t : -t[1]): Here we have negated the vote count part of the tuple, and removed reverse=True. So it is sorting from smallest to largest, but by the negative of the vote values (because that is what key says to do). So the biggest vote count will be the smallest negative number (so that tuple will appear first). Even I must admit this is highly confusing. Finally, typically when multiple candidates are tied for votes, we print their names together (because they all have the same number of votes) but also in alphabetical order. We do this in Python by specifying a key function that returns a tuple in which the vote count is checked first and sorted in descending order; and only if they are tied will the names be checked and sorted in ascending order. Because we want the tuples in decreasing vote counts but increasing names, we cannot use reverse=True; we need to resort to the negation trick above and write votes = [('Charlie', 20), ('Able', 10), ('Baker' ,20), ('Dog', 15)] for (c,v) in sorted(votes,key=lambda t : (-t[1],t[0])): print('Candidate',c,'got',v,'votes')which produces Note that the lambda in the key ensures we compare ('Charlie', 20) and ('Dog', 15) as if they were (-20, 'Charlie') and (-15, 'Dog') so the first tuple will be less and appear earlier (the one with the highest votes has the lowest negative votes), so will appear first. And when we compare ('Charlie', 20) and ('Baker' ,20) as if they were (-20, 'Charlie') and (-20, 'Baker') so the second tuple will be less and appear earlier (equal votes and 'Baker' < 'Charlie'). Candidate Baker got 20 votes Candidate Charlie got 20 votes Candidate Dog got 15 votes Candidate Able got 10 votes OK, that is strange. But you now have a good tool bag for sorting. ------------------------------------------------------------------------------ print Notice how the sep and end parameters in print help control how the printed values are separated and what is printed after the last one. Recall that print can have any number of parmeters (we will see how this is done soon), and it prints the str(...) of each parameter. By default, sep=' ' (space) and end='\n' (newline). So the following print(1,2,3,4,sep='--',end='/') print(5,6,7,8,sep='x',end='**') prints 1--2--3--4/5x6x7x8** Sometimes we use sep='' to control spaces more finely. In this case we must put all the spaces in ourselves. Still, other times we can catenate values together into one string (which requires us to explicitly use the str function). x = 10 print('Your answer of '+str(x)+' is too high'+'\nThis is on the next line') Note the use of the "escape" sequence \n to generate a new line. ------------------------------------------------------------------------------ String/List/tuple (SLT) slicing: SLTs are indexed starting at index 0, and going up to but not including the length of the SLT. 1) Indexing: We can index an SLT by writing SLT[i], where i is positive and in the range 0 to len(SLT)-1 inclusive, or i is negative and i is in the range -len(SLT) to -1 inclusive: SLT[0] is the first index, SLT[-1] is the last. 2) Slicing: We can specify a slice by SLT[i:j] which includes SLT[i] followed by SLT[i+1], ... SLT[j-1]. Slicing a string produces another string, slicing a list produces another list, and slicing a tuple produces another tuple. The resulting structures has j-i elements (or 0 if that number is <= 0); this formula works for positive and negative values. s = 'abcde' x = s[1:3] print (x) # prints 'bc' which is 3-1 = 2 values x = s[-4:-1] print (x) # prints 'bcd' which is -1-(-4) = 3 values likewise s = ('a','b','c','d','e') x = s[1:3] print (x) # prints ('b','c') which is 3-1 = 2 values x = s[-4:-1] print (x) # prints ('b','c', 'd') which is -1-(-4) = 3 values s[:i] is index 0 up to but not including i (can be positive or negative) s[i:] is index i up to and including the last index; so s[-2:] is the last two. 3) Slicing with stride: We can specify a slice by SLT[i:j:k] which includes SLT[i] followed by SLT[i+k], SLT[i+2k] ... SLT[j-1]. This follows the rules for slicing too, and allows negative numbers for indexing. s = ('a','b','c','d','e') x = s[::2] print (x) # prints ('a','c','e') s = ('a','b','c','d','e') x = s[1::2] print (x) # prints ('b','d'') ------------------------------------------------------------------------------ Conditional statement vs. Conditional expression Python has an if/else, which is a conditional statement. It also has a conditional expression, which while not as generally useful, sometimes is exactly the right tool to simplify a task. A conditional statement decides which other statements to execute; a conditional expression decides which value the expression evaluates to. The form of a conditional expression is resultT if test else resultF Often I put conditional expresions inside parentheses for clarity (as I did for lambdas). See the example below. This says, the expression evaluates to the value of resultT if test is True and the value of resultF if test is False; first it evaluates test, and then evaluates either resultT or resultF (but only one of these two) as necessary. Like other short-circuit operators in Python (which are they?) it evaluates only the subexpressions it needs to evaluate to determine the result. Here is a simple example. We start with a conditional statement, which always stores a value into min: either x or y depending on whether x <= y if x <= y: min = x else: min = y We can write this using a simpler conditional expression, capturing the fact that we are always storing into min, and just deciding which value to store. min = (x if x <=y else y) There is another example of using a conditional expression in the next section. Not all conditional statements can be converted into conditional expressions; typically only simple ones can, but doing so simplifies the code even more. So attempt to use conditional expression, but use good judgement after you see what the code looks like. Here is another example; it always prints x followed by some message if x % 2 == 0: print(x,'is even') else: print(x,'is odd') We can rewrite it as as calling print with a conditional expression insided deciding what string to print at the end. print(x, ('is even' if x%2 == 0 else 'is odd')) We can also write it as a one argument print: print(str(x) +(' is even' if x%2 == 0 else ' is odd')) ------------------------------------------------------------------------------ Argument/Parameter Matching (leaves out **kargs, talked about later) Let's explore the argument/parameter matching rules. First we classify arguments and parameters, according the options they include Arguments positional argument: an argument not preceded by name= option named argument: an argument preceded by name= option Parameters name-only parameter : a parameter with no default value default-argument parameter: a parameter including a default argument value When Python calls a function, it defines every parameter name in the function's header, and binds to each (just like an assignment statement) the argument value object matching that parameter name. In the rules below, we will learn how Python matches arguments to parameters according to three criteria: positions, parameter names, and default arguments for parameter names. Here are Python's rules for matching arguments to parameters. The rules are applied in this order (e.g., once you reach M3 we cannot go back to M1). M1. Match positional argument values in the call sequentially to the parameters named in the header's corresponding positions (both name-only and default-argument parameters are OK). Stop when reaching any named argument in the call or the * parameter in the header. M2. If matching a * parameter in the header, match all remaining positional argument values to it. Python creates a tuple that stores all these arguments M3. Match named-argument values in the call to their like-named parameters in the header (both name-only and default-argument parameters are OK) M4. Match any remaining default-argument parameters in the header, un- matched by rules M1 and M3, with their specified default arguments. M5. Exceptions: If at any time (a) an argument cannot match a parameter (e.g., a positional-argument follows a named-argument) or (b) a parameter is matched multiple times by arguments; or if at the end of the process (c) any parameter has not been matched, raise an exception: SyntaxError for (a) and TypeError for (b) and (c). These exceptions report that the function call does not correctly match its header. (When we talk about **kargs we will see what happens when there are extra named arguments in a function call: names besides those of parameters). When this argument-parameter matching process if finished, Python defines, (in the function's namespace), a name for every parameter and binds each to the argument it matches using the above rules. Passing parameters is similar to performing a series of assignment statements between parameter names and their argument values. If a function call raises no exception, these rules ensure that each parameter in the function header matches the value of exactly one argument in the function call. After Python binds each parameter name to its argument, it executes the body of the function, which computes and returns the result of calling the function Here are some examples: def f(a,b,c=10,d=None) : pass def g(a=10,b=20,c=30) : pass def h(a,*b,c=10) : pass Call | Parameter/Argument Binding (matching rule) ------------------+-------------------------------------------- f(1,2,3,4) | a=1, b=2, c=3, d=4(M1) f(1,2,3) | a=1, b=2, c=3(M1) d=None(M4) f(1,2) | a=1, b=2(M1) c=10, d=None(M4) f(1) | a=1(M1) c=10, d=None(M4), TypeError(M5c:b) f(1,2,b=3) | a=1, b=2(M1) b=3(M3), TypeError(M5b:b) f(d=1,b=2) | d=1, b=2(M3) c=10(M4), TypeError(M5c:a) f(b=1,a=2) | b=1, a=2(M3) c=10, d=None(M4) f(a=1,d=2,b=3) | a=1, d=2, b=3(M3), c=10(M4) f(c=1,2,3) | c=1(M3), SyntaxError(M5a:2) g() | a=10, b=20, c=30(M4) g(b=1) | b=1(M3), a=10, c=30(M4) g(a=1,2,c=3) | a=1(M3), SyntaxError(M5a:2) h(1,2,3,4,5) | a=1(M1), b=(2,3,4,5)(M2), c=10(M4) h(1,2,3,4,c=5) | a=1(M1), b=(2,3,4)(M2), c=5(M4) h(a=1,2,3,4,c=5) | a=1(M3), SyntaxError(M5a:1) h(1,2,3,4,c=5,a=1)| a=1(M1), b=(2,3,4)(M2), c=5(M3), TypeError(M5b:a) Here is a real but simple example of using *args, showing how the max function is implememented in Python; we dont' really need to write this function because it is in Python already, but here is how Python does it. We will cover raising exceptions below, so don't worry about that code def max(*args) : # Can refer to args inside; a tuple of values if len(args) == 0: raise TypeError('max: expected >=1 arguments, got 0') answer = args[0] for i in args[1:]: if i > answer: answer = i return answer print(max(3,-4, 2, 8)) Here is another real example of using *args, where I have simulated writing a print function. myprint calls print just once at the end, to actually print the string that it builds from args along with sep and end; it prints the same thing the normal print would print with the same arguments. Notice the use of the conditional if in the first line, to initialize s to either '' or the string value of the first argument def myprint(*args,sep=' ',end='\n'): s = (str(args[0]) if len(args) >= 1 else '') # handle first special for a in args[1:]: # all others with sep s += sep + str(a) s += end # end at the end print(s,end='') # print the string myprint('a',1,'x') # prints a line myprint('a',1,'x',sep='*',end='E') # prints a line but stays at end myprint('a',1,'x') # continues at end of previous line These print. a 1 x a*1*xEa 1 x ------------------------------------------------------------------------------ Iterators and Iterable (Data) Iterators (for loops) allow us to iterate through all the components of iterable data. We can even iterate over strings: iterating through their individual characters. We will study iterator protocols in detail, both the special iter/__iter__ and next/__next__ methods in classes, and generators (which are very very similar to functions, with a small but powerful twist), later in the quarter. Certainly we know about using "for" loops and iterable data (as illustrated by lots of code above). What I want to illustrate here is how easy it is to create lists, tuples, and sets from anything that is iterable by using the list, tuple, and set constructors (we'll deal with dict constructors later in this review). For example, in each of the following the constructor for the list/tuple/set objects iterates over the string argument to get the 1-char strings that become the values in the list/tuple/set object. l = list ('radar') then l is ['r', 'a', 'd', 'a', 'r'] t = tuple('radar') then t is ('r', 'a', 'd', 'a', 'r') s = set ('radar') then s is {'a', 'r', 'd'} Note that lists/tuples are ordered, so whatever the iteration order of their parameter is, the values in the list/tuple will be the same order. Contrast this with sets, which have (no duplicates and) no special order. In fact Python will print the set created above in the order shown. Likewise, since tuples/sets are iterable, we could compute a list from a tuple or a list from a set. Using l, t, and s from above... list(t) which is ['r', 'a', 'd', 'a', 'r'] list(s) which is ['a', 'r', 'd'] Likewise we could create a tuple from a list/set, or a set from a list/tuple. All the constructors handle iterable data. Program #1 will give you lots of experience with these data types and when and how to use them. The take-away now is it is trivial to convert from one of these objects to another, because the constructors for these classes all allow iterable values as their arguments, and all these data types (and Strings as well) are iterable. Before leaving, we need to look at how dictionaries fit into the notion of iterable. There are actually three ways to iterate through dictionaries: by keys, by values, and by items (each item a tuple with a key and its value). Each of these is summoned by a method name for dict, and the methds are named the same: keys, values, and items. So if we write the following to bind d to a dict (we will discuss this "magic" constructor soon) d = dict(a=1,b=2,c=3,d=4,e=5) Then we can create lists of three aspects of the dict: list(d.keys ()) is ['a', 'c', 'b', 'e', 'd'] list(d.values()) is [1, 3, 2, 5, 4] list(d.items ()) is [('a', 1), ('c', 3), ('b', 2), ('e', 5), ('d', 4)] Note that like sets, dicts are not ordered: in the first case we get a list of the keys; in the second a list of the values; in the third a list of tuples, where each tuple contains one key and its associate value. Note that the keys in a dict are always unique, but there might be duplicates among the values: try the code with d = dict(a=1,b=2,c=1). Items are unique because they contain keys (which are unique) Also note that if we iterate over a dict without specifying how, it is equivalent to d.keys. That is list(d) is the same as list(d.keys()) which is ['a', 'c', 'b', 'e', 'd'] One way to construct a dict is to give it an iterable, where each value is either a tuple or list of 2: a key followed by its values. So, we could have written any of the following to initialize d: d = dict( [['a', 1], ['b', 2], ['c', 3], ['d', 4], ['e', 5]] ) d = dict( [('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)] ) d = dict( (['a', 1], ['b', 2], ['c', 3], ['d', 4], ['e', 5]) ) d = dict( (('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)) ) or even d = dict( {('a', 1), ('b', 2), ('c', 3), ('d', 4), ('e', 5)} ) d = dict( {['a', 1], ['b', 2], ['c', 3], ['d', 4], ['e', 5]} ) Each is iterable, and each iteration is two values (in a tuple or list) Finally, if we wanted to construct a dict using the keys/values in another dict, one easy way to do it is with the constructor and the .items method. d_copy = dict(d.items()) ------------------------------------------------------------------------------ Sharing/Copying: is vs. == It is important to understand the fundamental difference between two names sharing an object (bound to the same object) and two names referring/bound to two "copies of the same object". Note that if we mutate a shared object, both names "see" the change: both are bound to the same object which has mutated. Not so if they refer to different copies of the same object. Note that difference between the Python operators is and ==. The first asks whether two references/binding are to the same object; the second asks only whether the two objects store the same information. See the different results produced for the example below. Also note that if x is y is True, then x == y must be True too (why?). For example, compare execution of the following scripts: the only difference is the second statement in each: y = x vs. y = list(x) x = ['a'] y = x # Critical: y and x share the same reference print('x:',x,'y:',y,'x is y:',x is y,'x == y:',x==y) x [0] = 'z' # Mutate x (could also append something to it) print('x:',x,'y:',y,'x is y:',x is y,'x == y:',x==y) x = ['a'] y = list(x) # Critical: y refers to a new list with the same contents as x print('x:',x,'y:',y,'x is y:',x is y,'x == y:',x==y) x [0] = 'z' # Mutate x (could also append something to it: x+) print('x:',x,'y:',y,'x is y:',x is y,'x == y:',x==y) Finally there is a copy module in Python that defines a copy function, so we can import it as: from copy import copy Assuming x is a list, we can replace y = list(x) by y = copy(x). How is y = list( (1,2,3) ) different from y = copy((1,2,3))? We could also just: import copy (the module) and then write y = copy.copy(x) but from copy import copy is better. ------------------------------------------------------------------------------ Hashable/Mutable and how to Change Things: Python uses the term Hashable, which has the same meaning as immutable. So hashable and mutable are OPPOSITES: You might see this message relating to errors when using sets with UNHASHABLE values or dicts with UNHASHABLE keys: since hashable means immutable, then un-hashable means un-immutable which simplifies (the two negatives cancel) to mutable. So unhashable is mutable. Here is a quick breakdown of standard Python types Hashable/immutable: strings, tuples, frozenset mutable/unhashable: list, sets, dicts The major difference between tuples and list in Python is the former is immutable and the later is not. If some other datatype (values in sets, keys in dictionaries) needs hashable/immutable parts, use a tuple. A frozenset can do everything that a set can do, but doesn't allow any mutator methods to be called. The constructor for a frozenset is frozenset(...) not {}. Note that once you've constructed a frozen set you cannot change it (because it is immutable). If you have a set s and need an equivalent frozenset, just write frozenset(s). We will study hashing a bit: it is a technique for allowing speedy access to sets and dicts. ICS-46 (Data Structures) studies it in more depth. ------------------------------------------------------------------------------ Comprehensions: list, tuple, set, dict Comprehensions are compact ways to generate complicated (but not too complicated) lists, tuples, sets, and dicts. That is, they compactly solve some problems but cannot solve all problems. The general form of a list comprehension is as follows, where f means any function (or expression) using var and p means any predicate (or bool expresssion) using var. [f(var) for var in iterable if p(var)] Meaning: collect together into a list all of f(var) values, for var taking on every value in iterable, but only collect the f(var) value if p(var) is True. For tuple or set comprehensions, we would use () and {} as the outermost grouping symbol instead of [] (we'll talk about dicts at the end, which use {} and : to get the job done and be distinguised from sets which use {}). Note that the "if p(var)" part is optional and we can also write comprehensions as follows (in which case it has the same meaning as p(var) always being True). [f(var) for var in iterable] literally [f(var) for var in iterable if True] for example x = [i**2 for i in irange(1,10) if i%2==0] # note: irange not range print(x) prints the squares of all the integers from 1 to 10, but only if the integer is even (leaves a remainder of 0 when divided by 2). Run it. Change it a bit to get is to do something else. Here is another example x = [2*c for c in 'some text' if c in 'bcdfghjklmnpqrstvwxy'] print(x) which prints a list with strings that are doubled for all the consonants (no aeiouy -or spaces for that matter): ['ss', 'mm', 'tt', 'xx', 'tt'] We can translate a list comprehension into equivalent code that uses more familiar Python features. x = [] # start with an empty list for var in iterable: # iterate through iterable if p(var): # if var is acceptable? x.append(f(var)) # add f(var) next in the list But often using a comprehension (in the right places: where you want to generate some list, tuple, set or dict) is simpler. Not all lists that we build can be written as simple comprehensions, but the ones that can are often very simple to write, read, and understand. Note that we can add-to (mutate) lists, sets, and dicts, but not tuples. For tuples we would have to write this code with x = () at the top and x = x + (var,) in the middle: which builds an entirely new tuple by catenating the old one and a one-tuple (containing only x) and then binding x to the newly constructed tuple. Here is something interesting: x = {c for c in "I've got plenty of nothing"} # note ' in str delimited by " print(sorted(x)) It prints a set of all the characters (in a list in sorted order) in the string but because it is a set, each character occurs one time. So even though a few c's have the same value, only one of each appears in the set because of the semantics/meaning of sets. Note it prints [' ', "'", 'I', 'e', 'f', 'g', 'h', 'i', 'l', 'n', 'o', 'p', 't', 'v', 'y'] If we used a list comprehension, it would be much longer because, for example, the character 't' would occur 3 times in a list (but occurs only once in a set) The form for dict comprehensions is similar, here k and v are functions (or expressions) using var. Notice the {} on the outside and the : on the inside, separating the key from the value. {k(var) : v(var) for var in iterable if p(var)} So x = {k : len(k) for k in ['one', 'two', 'three', 'four', 'five']} print(x) prints a dictionary that stores keys that are these five words whose associated values are the lengths of the words. Because dicts aren't ordered, it would print as {'four': 4, 'three': 5, 'one': 3, 'five': 4, 'two': 3} Finally, w can write a nested comprehension x = {c for word in ['i', 'love', 'new', 'york'] for c in word if c not in 'aeiou'} print(x) It says to collect c's, by looking in each word w in the list, and looking at each character c in each word: so the c collected at the beginning is the c metioned after the list. It prints a set of each different letter that is not a vowel, in each word in the list. I could write this expanding one part of the comprehension but leaving the inner one x = set() for word in ['i', 'love', 'new', 'york']: x = x.union( {c for c in word if c not in 'aeiou'} ) print(x) or write it with no comprehensions at all x = set() for word in ['i', 'love', 'new', 'york']: for c in word: if c not in 'aeiou': x.add(c) print(x) So which of these is the most comprehendable: the pure comprehension, the hybrid loop/comprehension, or the pure nested loops? What is important is that we know how all three work, can write each correctly in any of these ways, and then we can decide afterwords which way we want the code to appear. As we program more, our preferences might change. WARNING: Once students learn about comprehensions, sometimes they go a bit overboard. Here are some warning signs: When writing a comprehension, you should (1) use the result produced and (2) typically not mutate anything in the comprehension. If the purpose of your computation is to mutate something, don't use a comprehension. ------------------------------------------------------------------------------ The zip function (and enumerate too) There is a very interesting function called zip that takes an arbitrary number of iterable arguments and zips/interleaves them together (like a zipper does for the teeth on each sid). Let's start by looking at just two argument versions of zip. What zip actually produces is an iterable -the ability to get the results of zipping, not the result itself. So to "get the result itself" we should use a for loop or comprehension (as shown in most of the examples below). Execute the following code z = zip( ('a','b','c'), (1, 2, 3) ) # I used two tuples for iterators print('z:',z,'comprehension of z:',[i for i in z]) The result of the comprehension is [('a', 1), ('b', 2), ('c', 3)] which zips/interleaves the values from the first iterable and the values from the second: [(first from first,first from second),(second from first,second from second), (third from first,third from second)] What happens when the iterables are of different lengths? Try it z = zip( ['a','b','c'], (1, 2) ) # I used a list and a tuple for iterables print([i for i in z]) # prints [('a', 1), ('b', 2)] So when one iterable runs out, the process stops. Here is a more complex example with three iterable parameters of different sizes. Can you predict the results: do so, and only then run the code. z = zip( ['a','b','c','d'], (1, 2, 3), ['1st', '2nd', '3rd', '4th'] ) print([i for i in z]) Of course, this generalizes for any number of arguments, interleaving them all (from first to last) until any iterable runs out. So the number of values in the result is the minimum of the number of values of the argument iterables. Here is something to try result = [i for i in zip( whatever you want: try different ones)] print( [i for i in zip(*result)] ) What is the result? Have you ever seen the * before used as a unary prefix operator (written pre/before its single argument)? Note one way zip is very useful: suppose we want to iterate over values in two lists simultaneously, l1 and l2, operating on the first pair of values in each, the second pair of values in each, etc. We can use zip to do this by writing: for (i,j) in zip(l1,l2): process the next pair of values in each This is a convenient time to toss in another quick example: enumerate. It also also produces an iterable as a result, but has just one iterable argument, and an optional 2nd argument that is a number. It returns tuples whose first values are numbers (starting at the value of the second parameter; leave it out and unsurprisingly, they start as 0) and whose second values are the values in the iterable. So, if we write e = enumerate(['a','b','c','d'], 5) print([i for i in e]) it prints [(5, 'a'), (6, 'b'), (7, 'c'), (8, 'd')] Given l = ['a','b','c','d','e'] we could use enumerate to write the following code more simply for i in range(len(l)): print(i+1,l[i]) (which prints 1 a, 2 b, 3 c, 4 d, 5 e) by for (i,x) in enumerate(l,1) print(i,x]) You might ask now, why do these things (zip/enumerate) produce iterables and not just tuples or lists? That is an excellent question. It goes right along with the excellent question why does sorted(...) produce a list and not an iterable? ------------------------------------------------------------------------------ **kargs for dictionary of not matched named arguments in function calls Recall the use of *args in a function parameter list. We can also write the symbol **kargs (we can write ** and any word, but kargs is the accepted one). If we use it to specify a parameter, it must occur as the last parameter. kargs stands for keyword arguments. Basically, if Python has any keywords arguments that do not match keyword parameters (see the large discussion of argument/paramteter binding above, which includes *args but doesn't include **kargs) they are all put in a dictionary thati is stored in the last parameter named kargs. So, imagine we define the following function def f(a,b,**kargs): print(a,b,kargs) and call it by f(c=3,a=1,b=2,d=4) it prints: 1 2 {'c': 3, 'd': 4} Here Python found two named arguments (c=3 and d=4) whose names did not appear in the function header of f (which specifies only a and b, and of course the special **kargs), so while Python bound a to 1 and b to 2 (the parameter names specified in the function header, matched to similarly named arguments) it created a dictionary with all the other named arguments: {'c': 3, 'd': 4} and bound kargs to that dict. The same result would be printed for f(1,2,d=4,c=3) We will use **kargs to understand a special use of of dict constructors (below). We will also use **kargs (and learn something new in the process) when discussing how to call overridden methods using inheritance: a frequent use of **kargs. ------------------------------------------------------------------------------ Lists, Tuples, Sets, Dictionaries (frozenset and defaultdict) You need to have pretty good grasp of these important data types, meaning how to construct them and the methods/operations we can call on them. Really you should get familiar with reading the online documentation for all these data types (see the Python Library Reference link on the homepage for the course). 4. 6: Sequence Types includes Lists (mutable) and Tuples (immutable) 4. 9: Set Types includes set (mutable) and frozenset (immutable) 4.10: Mapping Types includes dict and defaultdict (both mutable) 4.6: Sequence Types includes Lists (mutable) and Tuples (immutable) These sequence operations (operators and functions) are defined in 4.6.1 x in s, x not in s, s + t, s * n, s[i], s[i:j], s[i:j:k], len(s), min(s), max(s), s.index(x[, i[, j]]), s.count(x) Mutable sequence allow the following operations, defined in 4.6.3 s[i] = x , s[i:j] = t, del s[i:j], s[i:j:k] = t,, del s[i:j:k], s.append(x) s.clear(), s.copy(),s.extend(t), s.insert(i, x), s.pop([i]), s.remove(x), s.reverse() It also discusses list/tuple constructors and sort for list. note that the append method is especially important for building up sequences like lists. Also x = s.pop(i) is equivalent to x = s[i] del s[i] and if i is absent, Python uses the value 0 (the first index in the sequence) ----- 4. 9: Set Types includes set (mutable) and frozenset (immutable) These set (operators and functions) are defined in 4.6.1.9 len(s), x in s, x not in s, isdisjoint(other), issubset(other), set <= other, set < other, issuperset(other), set >= other, set > other, union(other, ...), intersection(other, ...), difference(other, ...), symmetric_difference(other), copy; also the operators | (for union), & (for intersection), - (for difference), and ^ (for symmetic difference) Sets, which are mutable, allow the following operations update(other, ...), intersection_update(other, ...), difference_update(other, ...), symmetric_difference_update(other), add(elem), remove(elem), discard(elem), pop(), clear(); also the operators |= (union update), &= (intersection update), -= (difference update), ^= (symmetric difference update) ----- 4.10: Mapping Types includes dict and defaultdict (both mutable) These dict (operators and functions) are defined in 4.10 d[key] = value , del d[key], key in d, key not in d, iter(d), clear(), copy(), fromkeys(seq[, value]), get(key[, default]), items(), keys(), pop(key[, default]), popitem(), setdefault(key[, default]), update([other]), values() Notes on dicts: d[k] accesses the value of a key (throws an exception if k not in d) d.get(k,default) returns d[k] if k in d, otherwise returns default d.setdefault(k[,default]) returns d[k] if k in d, else if k not in d (a) d[k] = default (b) return d[k] equivalent (and more efficient than) if k in d: return d[k] else d[k] = default return d[k] There is a type called defaultdict (see 8.3.4) whose constructor takes the name of a class that will be used to construct a new value whenever a key is accessed for the first time (i.e., that key is not in the dictionary). Here is an example of defaultdict in action. from collections import defaultdict # in same module as namedtuple letters = ['a', 'x', 'b', 'x', 'f', 'a', 'x'] freq_map = defaultdict(int) # int not int(); int() returns 0 (when called) for l in letters: freq_map[l] += 1 # in dict, exception raised if l not in d print(freq_map) # defaultdict calls int() putting 0 there first We could use a dict, but we would have to rewrite the code as follows (which is a bit less efficient too); defaultdict does the same thing, but automatically. letters = ['a', 'x', 'b', 'x', 'f', 'a', 'x'] freq_dict = dict() # note dict only for l in letters: if l not in freq_dict: # must check l in freq_dict before freq_dict[l] freq_dict[l] = int() # int() constructor returns 0; could write 0 here freq_dict[l] += 1 # l is guaranteed in freq_dict, either because print(freq_dict) # it was there originally, or put there Another way to do the same thing (but is a bit less efficient) uses the setdefault method (listed above) letters = ['a', 'x', 'b', 'x', 'f', 'a', 'x'] freq_dict = dict() # note dict only for l in letters: freq_dict[l] = freq_dict.setdefault(l,0) + 1 print(freq_dict) Here we evaluate the right side of the = first; if l is already in the map, its associated value is returned; if not, l is put in the map with an associated value of 0, then this assocated value is returned (and incremented, and stored back into freq_dict[l] replacing the 0 just put there). Often we use defaultdicts with list instead of int: list() creates an empty list associated with a new key (and later we add things to that list); likewise for sets. Just like with comprehensions, what is important is that we know how things like defaultdicts and dicts (with the setdefault method) work, so that we can correctly write code in any of these ways. We can decide afterwords which way we want the code to appear. As we program more, our preferences might change. ------------------------------------------------------------------------------ Exceptions: example from prompt_for_int We do two things with exceptions in Python: we raise them and we handle them. Exceptions were not in early programming languages, but were introduced big time in the Ada language in the early 1980s, and have been parts of most new languages since then. A function raises an exception if it cannot do the job it is being asked to do. Rather than fail silently, possibly producing a bogus answer that gets used to compute a bogus result, it is better that the computation declares a problem and if no one can recover from it (see handling exceptions below) the compuation halts. For example, if your program has a list l and you write l[5] but l has nothing stored in this position, Python raises an exception. If you haven't planned for this possibility, and told Python to handle the exception and how to continue the calculation, the program just terminates stops and Python prints: IndexError: list index out of range Why doesn't it print the index value 5 and the lenth of list l?. That certainly seems like important information. There are lots of ways to handle exceptions, and not a ton of code that needs to (at least in the 30s series of classes). But here is a drastically simplified example from my prompt class (this isn't the real code, but a simplification for looking at a good use of exception handling). But it does a good job of introducing the try/except control form which tries to execute a block of code and handles any exceptions it throws. Exception is the generic exception; we can have many excepts, each one specifying a more specific exception to handle, and what to do when that exception is raised. In fact, int('x') throws the ValueError exception, so we could replace Exception by ValueError in the except clause below. If we raised the IndexError exception in some code, and except Exception would catch it (but not except ValueError). def prompt_for_int(prompt_text): while True: try: response = input(prompt_text+': ') # response is used in except answer = int(response) return answer except Exception: print(' You bozo, you entered "',response,'"',sep='') print(' That is not a legal int') print(prompt_for_int('Enter a positive number')) So, this is an "infinite" loop, but there is a return statement; if it gets executed that terminates the loop. So the loop body is a try/except; the body of the try/except 1) it prompts the user (this cannot fail) 2) it calls int(response) on the user's input (which can raise the ValueError exception if the user types characters that cannot be interpreted as an integer) 3) if no exception is raised, the return statement returns an int object representing the integer the user typed. But if the exception is raised, it is handled by the except clause, which prints some information. Now the try/except is finished, but it is in an infinite loop, so it goes around the loop again, reprompting the user (over and over until the user enters a legal int). Actually, the body of try could be just response = input(prompt_text+': ') # response is used in except return int(answer) If an exception is thrown while the return is evaluating the value to return, it still gets handled in except. We CANNOT write it in one line because the name response is used in the except clause (in the first print). If this wasn't the case we could write return int(input(prompt_text+': ')) # if response not used in except Finally, in Java we throw and catch exceptions (obvious opposites) so I might sometimes use the wrong term. That is becuase I think more generally about programming than "in a language", and translate what I'm thinking to the terminology of the language I am using, but sometime I get it wrong. ------------------------------------------------------------------------------ Name spaces (for classes) __dict__ Every instance of a class has a special variable named __dict__ that stores all its bindings in a dictionary. During this quarter we will systematically study class names that start and end with two underscores. Writing self.x = 1 IN a class method is similar to writing self.__dict__['x'] = 1; both associate a name with a value in a class instance. In fact, if z refers to a class instance, we can write z.x = 1 and it is similar to writing z.__dict__['x'] = 1. We will explore the uses of this kind of knowledge later in the quarter. Here is a brief illustration of the point above. Note that there is a small Pyhon script that illustrates the point. This is often the case. class C: def __init__(self): pass def make_x(self,v): self.x = v def make_y(self,v): self.__dict__['y'] = v c = C() c.x = 1 print(c.x) c.__dict__['x'] = 2 print(c.x) c.make_x(3) c.make_y(4) print(c.x,c.y) ------------------------------------------------------------------------------ Trivial Things. An empty dict is created by {} and empty set by set() (we can't use {} for an empty set because Python would think it is a dict). A one value tuple must be written like (1,) with that "funny" comma (we can't write just (1) because that is just the value 1, not a tuple storing just 1). ------------------------------------------------------------------------------ Questions: What can we say about len of set(d.keys()) compared to len of d.keys? len of set(d.values()) compared to len of d.values()? len of set(d.items()) compared to len of d.items()? I should have about 100 here but ran out of time.