Defining Functions in Modules In the scripts we have written, we have already used many functions imported from various modules (at least builtins, prompt, predicate, math, goody, and random: can you name a function used from each of these modules)? We have also seen functions, although not examined their details (we will do so here), in many of the lectures on control structures, using them to capture the computation of a script. In this lecture we will explore how to write functions in our own scripts, and write functions in modues that we can use as libraries. We will learn two simple statements, return and raise, and very importantly, review Python's rule for binding arguments to parameters. We start by defininig the EBNF for Python's return statement, which is the only new statement we must know to write functions (although we will also cover the raise statement later in this lecture, to show how code -typically functions- can raise exceptions. return_statement <= return expression Note that return is a Python keyword. Just as a break statement can be used only inside the body (block) that a loop repeats, a return statement can be used only inside the body (block) that defines a function. We will see return statements with simple and complicated expressions, illustrated below. return True # return a literal (it is an expression) return a # return a variable (it is an expression) return sqrt((x1-x2)**2 + (y1-y2)**2)# return a "real" expression w. operators Semantically, whenever Python executes a return statement (always in the body of a function) it first evaluates the expression (which results in some object) and the function terminates returning that object as the result of the function. Once a function executes return, it executes no more statements, no matter where it is executing (in a loop, in a try/catch, etc.) So when a return statement is executed, that is it: the function terminates (although technically if it is in a block-try in a try/catch, which has finally: clause, the block-finally is executed right before the function terminates). More advanced functions frequently bind an answer to name that might be updated many times in the function, and finally return that result; but literals and expressions using operators (and even calling other function calls) abound in return statements. Before we review the EBNF for function headers, parameters, function calls, and arguments formally, we will examine and discuss a simple concrete function example and discuss these terms more informally (and quite extensively). First, below is the EBNF for a function-defintion, which is also a Python statement (maybe we should call it a function-definition-statement, but that is mouthful). function-definition <= def function-header: block When Python executes a function definition, it binds the name of the function (in the function header) to a function object. We can call/execute the function object by referring to it using its name, followed by parentheses that contain all the arguments needed to match the parameters. We discuss this all in more detail below. The EBNF combines the keyword def, with a function-header (typically just the name of the function followed by the names of its parameters in parentheses), followed by a colon: and of course, what always follows a colon in the EBNF we have seen for control structures, is a block (which here is called the body of the function: a mini-script Python executes when we call the function), which can contain any number of return statements. As with break statements, we want to limit the number of return statements in a function body, but we will not be as restrictive with return statements: even the first example we show will have two return statements. So let's explore that that first example. It is often useful to be able to compute the minimum of two float values. We have seen how to do this using an if statement, so we will now write the min function, containing this simple control structure along with two returns: one in each block (the whole block). def min(x : float, y : float) -> float: if x <= y: return x else: return y In this example we have chosen to specify annotations in the function header; we use x : float, y: float to specify that this function has two parameters; the parameter x should refer to a float value object and the parameter y should refer to a float value object. Also, we use -> float to specify that this function will return a reference to a float value object. These annotations are NOT CHECKED by Python (much more on this topic later in ICS-31 and throughout the entire 30s sequence). Annotations are, like comments, for our benefit only. To Python the following function definition, without annotations, has exactly the same meaning def min(x,y): if x <= y: return x else: return y We will soon learn that this definition might even be better suited to this function, because min works correctly on types other than float. But for now let's assume we use this function only to compute the minimum of two float values. Please note that like most functions, this function's body does not prompt the user for information, nor does it print anything. It gets its "input" via its parameters, and does its "output" by returning a value. We might use this function definition, by calling the function to solve the following task. We buy an item at a special sale, where for the ultimate cost, we pay either 90% of its price or is its price minus $5, whichever is cheaper. Here is a script that prompts the user for the price and prints the cost we must pay, by calling the min function (which would occur earlier in the script). price = prompt.for_float('Enter price of item') cost = min(.90*price, price-5.00) print('The sale cost of this item is',cost) In the second line of code, we call the min function and bind the result it returns to the name cost. We call a function by writing its name, followed in parentheses (whenever we see parentheses, it means a function call) by its arguments (typically one for each parameter in the function's header, although we will discuss default parameter values soon). As in the header (where all the parameters are separated by commas), all the arguments are are separated by commas as well. Semantically when we call a function, Python first evaluates each argument (each can be a simple or complicated expression) and transmits or passes a reference to each resulting object to its matching parameter; this just means that Python uses the value of each argument, in the function call, to initialize its matching parameter, in the function definition. It is equivalent to writing a sequence of assignment statements: first-parameter = first-argument, then second-parameter = second-argument, etc. Once the parameters are bound to their matching argument values, Python executes statements in the body (block) of the function until it executes a return statement telling it what value the function returns. If Python executes the last statement in a function (there are no more to execute) without executing a return statement, Python automatically executes the equivalent of return None. If None starts showing up in your computations, look for a function call that returned None, because the function called is mistakenly missing a return statement in its body. Of course, some function do return None purposely, as the print function does. It is called for its effect -to display information in the console- not to compute a value. So, here is an example of the script running. In some sense we are back to writing code as in Program #1, although now we will explore how the function we wrote works. Enter price of item: 100. The sale cost of this item is 90.0 In this run, the user enters the float value 100.00. Python executes the function call by (1) ...computing the value of the first argument as 90. and the value of the second argument as 95. (2) ...binding the parameter named x to the value object 90. and the parameter named y to the value object 95. (3) ...executing the body of the function. The if's test is true: x <= y (90. <= 95.) so it executes the return statement, which terminates the function and returns the object x refers to, 90. When the function call terminates, its parameters (and any variable defined in the function's body (block) are deleted. Whenever the function is called again, it starts over with all new bindings for the parameters and any local names used in the function (we'll see a few functions with local names soon). Every function call executes similarly, using these three same steps, although step (3) can execute more complicated function bodies with more complicated statements. The idea of calling a function to get a result is analogous to calling someone for advice. We tell them the relevant information (pass them arguments) and then wait until they deliver their answer. We don't need to worry about HOW they determine the answer, it is enough to know how to call them and what information they need to determine the answer. ------------------------------ Debugger Interlude Copy/paste this function definition and three lines of code into an Eclipse scrpt and run the script to ensure it produces the correct result. Remember to include import prompt at the top. You will note that the definition of min is flagged with a warning that reads: "Assignment to reserved built-in symbol: min". This is bacause the name min is automatically imported from the builtins module (and yes, it is a function that computes the minimum of its arguments). We have rebound the name min to refer to the function object that we specified, and this is OK, although rebinding other functions (like print, which is also automatically imported from builtins) can cause Python execute our scripts in very strange ways. Now switch to the Debugger Perspective and run this script again. First set a breakpoint on the first line after the function definition (the line calling prompt.for_float), and the use the step-over button (the arrow going over an little block) to execute the three lines. After stepping over the second line, look at the variables window. After some special double-underscore names, we will see cost, min, price, and prompt: the four names defined in this script. Notice how the cost and price values display, as expected; notice how the min and prompt values display, as a special function object and a special module object respectively. Now debug the program again. But this time when the debugger indicates it is about to execute the second line, using the step-into button (the arrow going between two little blocks). Note that the focus (the highlighted line that Python indicates it will execute next) shifts to the first line of the min function: the if statement. So instead of stepping over the line of code that called the function (and executing all its code, including the entire function call) we step into the function, to watch it execute in detail. Again, look at the Variables tab/window. The debugger has shifted focus to the function and shows its parameter variables (x and y) and their bindings (the values 90.0 and 95.0 if you entered the values I did above). So, the function has been called and the arguments have been transmitted/passed to their parameters, but it has not executed the function's body yet. Now look at the Debug tab and look under "Main Thread". The first indented entry on my computer shows "min [functions.py:4]" indicating that Python is currently executing the min function on line 4 of the script (that is the line the min's if statement appears on, when I cut/pasted the code into a script). I put this script into a module named "functions", so the line beneath this one says " [functions.py:10]". If we click that entry, the debugger shifts its focus to the line in the script where this function is called (in my script on line 10): it is highlighted, and the Variable tab/window again shows the name in the module. Click "min [functions.py:4]" again and the focus shifts back to the min function: its statement and variables. So, in this way we can carefully examine the execution of a function (if we want) and flip back and forth between the body of a function and the statement in the module that called it: in the former we can see the parameters, in the later the arguments. Using the step-over button (there are no function calls in min, so technically we can use either step- button) watch how it executes the if, then executes the return statement in the if's first block. When we step-over the return statement (1) "min [functions.py:4]" disappears from under Main Thread in the Debug tab (2) Focus returns to the statement calling min, and the Variables tab is updated The function is done and we are back to the statement in our program that called the min function. So, we have now learned what we can control how we use the debugger to execute functions in our scripts: step-over executes the entire function without showing us any details; step-into treats the function like a mini-scipt that we can step through just like any script (once the parameters are bound to their matching arguments). This gives us a coarse-grained or fine-grained control of our functions when we step through them. In fact, we can step into any function that we write or import. If we are finished stepping inside a function, and we want to return to the statement that called the function, we can easily do so by using the step-return button (the arrow going out from between two little blocks). Rerun the debugger and after stepping into min, immediately step-return out of it. Note that the stepping operations are used so often, they are also bound to the keys F5, F6, F76, and F8 (stop-into, step-over, step-return, and resume). Also, we can set a [un]conditionial breakpoint on any line INSIDE a function, and the debugger will stop there (just as it would for setting breakpoints in a script). Finally, before we leave this interlude, go back to the PyDev perspective. Then replace the call to min in the script with the following (which includes just one argument). cost = min(.90*price) Rerun this script. Python can still execute the first statement, which prompts for the price. But when Python tries to execute the second statement it will detect that it cannot successfully call the min function, because min's header lists two parameters (and neither with a default argument) but the rewitten call now contains only one argument. Because it cannot successfully initialize all the parameters, Python raises the TypeError exception and prints the following. Traceback (most recent call last): File "C:\Users\Pattis\workspace\zexperiment\functions.py", line 10, in cost = min(.90*price) TypeError: min() missing 1 required positional argument: 'y' What this exception means by TypeError is that the prototype of min (which really means the header, showing two parameters) is not successfully matched by this call to min, which transmits/passes just one argument. This is not the kind of exception that we write a try/except to handle: we need to fix our script to call min with the correct number of arguments. Likewise, if we changed the same line to cost = min(.90*price, price-5., "Can't you give me a better value?") Python would detect that it cannot successfully call the min function (the call now supplies three arguments, but the function sill only defines two parameters) so Python will raise the following exception. Traceback (most recent call last): File "C:\Users\Pattis\workspace\zexperiment\functions.py", line 10, in cost = min(.90*price, price-5., "Can't you give me a better value?") TypeError: min() takes 2 positional arguments but 3 were given Of final interest, if we changed the same line to (function name is now mini) cost = mini(.90*price, price-5.) Eclipse would show this line as a syntax error because there is no mini function even defined. But Python will still execute the script, but when it reaches this line it will detect an error and raise the following exception. Traceback (most recent call last): File "C:\Users\Pattis\workspace\zexperiment\functions.py", line 10, in cost = mini(.90*price, price-5.) NameError: name 'mini' is not defined ------------------------------ Equivalent functions Here is a second way to write an equivalent min function. Ideally, a function should contain just one return statement, at its end. In fact, we can prove mathematically that there is always a way to write any function with just one return statement. But sometimes functions are easier to write (and understand) if they have multiple return statements. Thus, we will adopt a more pragmatic approach, putting simplicity as the paramount aspect of the code that we write: if multiple return statements make a function simpler to write and easier to understand, use them. But be able to argue why; don't just use them because you are sloppy. I would argue, for example, that the min function defined above, which has two return statements, is simpler than the one below, which has only one return statement. def min (x,y): if x <= y answer = x else: answer = y return answer Instead of one if statement, this function's body is a sequence of two statements: one that decides how to define answer, and one that returns that value. The original function just chooses which of its parameters to returns, and returns it immediatley, without defining any local names. As soon as Python knows whether x <= y, it knows which value to return, and returns it. Therefore, I think that the original function is simpler to write and easier to understand. ------------------------------ In fact, we can write the min function using a conditional expression, which we will now discuss. Note that Python has an conditional STATEMENT (the if statement, which we have studied), which uses the if keyword and possibly the elif/else keywords. It also has a conditional EXPRESSION, which always uses the if/else keywords. A conditional statement decides which (if any) other statements to execute; a conditional expression decides which of two other expressions to evaluate, to compute the result of the entire expression. The form of a conditional expression is as followed expresson-T if expression-B else expression-F Often I put conditional expresions inside parentheses for clarity, but it is only needed if operator precedence dicates their use. Semantically, the result of this expression is either the value of expression-T (if expression-B evaluates to True) or expression-F (if expression-B evaluates to False). Python first evaluates expression-B, and the evaluates exactly one of the two other expressions. Like other short-circuit operators in Python (and/or) it evaluates only the subexpressions it needs to evaluate to determine its result. For a first example, we can write the min function using a simpler conditional expression. def min (x,y): return (x if x <=y else y) Here we have a block that is just one return statement; the conditional expression decides what gets returned, either the value x or y refers to, based on how x compares to 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 fail, or see that the code looks too complicated. Here is another example; it always prints x's value followed by some message. if x % 2 == 0: print(x,'is even') else: print(x,'is odd') We can rewrite it as a single call to the print function, that first prints x's value and then, using a conditional expression, 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 call to the print function, but using the catenate operator, which requires explicitly converting x to a string: str(x). print(str(x) + (' is even' if x%2 == 0 else ' is odd')) As a final example, here is a function that returns the singular or plural value of a word based on a number. I have commented out the equivalent if statement and its multiple returns. def number_match(number : int, singular : str, plural : str) -> str: return (singular if number == 1 else plural) # if number == 1: # return singular # else: # return plural We can call this function as follows print(brush_count, number_match(brush_count,'brush','brushes')) It might be more useful to write the following function, which doesn't repeat the 'brush' part of 'brush' and 'brushes'. It is an art to determine the the most useful form of a function. Think and experiment. def number_match_suffix(number : int, root : str, plural_suffix : str) -> str: return root + ('' if number == 1 else plural_suffix) Which we can call as follows print(brush_count, number_match_suffix(brush_count,'brush','es')) Of course, it would be problematic to print 1 goose vs. 2 geese with number_match_suffix, but easy to do with number_match. What all these uses have in common is that we started with a very simple if/else statement whose blocks did the same things: returning or printing. If this is the case, we can often simplify the code with an if expression. Once students learn about conditional expressions, they try to remove every if statement, converting it to a conditional expression: it OK to try, but if you fail or end up with something that looks much more complex, stick with the if/else statement. ------------------------------ Polymorphic Functions Let's return to an issue dealing with annotations. All versions of the min function require of their parameters only that their argument values can be compared using the <= operator. So long as the types of the arguments are the same (e.g., both int, float, string, even bool) this function will return as a result the minimum of the two values. In fact, because of implicit conversion things like min(1,.5) and min(1.,False) return results: .5 and False respectively. A function that works correctly on many different types is called polymorphic. The term poly+morphous means many+forms, so these function work on many forms (we would say many types of data). We have used the term overloaded to refer to the same phenomenon. There are argument for this function cannot compute and answer and raises an exception: if we call min(1,'a') Python detects the problem and reports the following exception: TypeError: unorderable types: int() <= str(), which means there is no defiintion in Python of <= whose left argument is an int and whose right argument is str. So annotating min's parameters by using int, or float, or str would not tell the whole story. We can annotate these parameter with : Object, although that doesn't tell the whole story either, because the arguments matching the parameters cannot be any arbitry objects, they must be comparable. So, we will use annotations when they add useful information, but not use them when they won't ------------------------------------------------------------------------------ Functions: simple examples and use (simple parameters, no default arguments) In this section we first describe how to write a function (a beginner's lesson) and then we will show and commment on many sample functions. As described before, a function is basically a mini-script that receives its input through its parameters and produces its output by returning a result. So, to write a function a beginnner would first write a script that prompted for the input, did the computation, and printed the output. Then the computation part of the script would be put in the body of a function. Its header would include a descriptive name for the function, and all the parameters needed for the function to compute. It would include one or more return statements to produce the result. Note that every parameter should be used (refered to) somewhere inside the function, otherwise it shouldn't be a parameter. We can test such a function by moving the prompt statements remaining in the script after the function definition, and then placing a call to the function passing ther results of these prompts are arguments, and either storing the result in a name and printing it, or just printing the result returned by the function directly. See how we tested the min function above. Now, let's look at sample functions. First, here are two functions that compute whether a year is a leap year. def is_leap_year (year : int) -> bool: return (year%4 == 0 and year%100 != 0) or year%400 == 0 def is_leap_year (year : int) -> bool: if year%400 == 0: return True if year%100 != 0 return True if year%4: return False This function hides a very messy calculation inside a well-named and easy to call function. A year is a leap year if it is divisible by 4 but not 100, unless it is also divislbe by 400; this function is correct for the next few thousand years. It has just one parameter: the year on which to do the calculation, and always returns a bool (True or False) value. def distance (x1 : float, y1 : float, x2 : float, y2 : float) -> float: return math.sqrt( (x1-x2)**2 + (y1-y2)**2 ) Actually, this function works with combination of ints and floats, but it always returns a float result (because of of the square root: even math.sqrt(4) returns a result of 2.0 (a float). We could write it more succinctly as def distance (x1,y1, x2,y2) -> float: return math.sqrt( (x1-x2)**2 + (y1-y2)**2 ) This function computes the simple Euclidean distance between two points on a plane, which must be specified as four parameters: the X- and Y-coordinate of each point (although a better function would use two parameters, each an instance of a Point class, to represent these four values; we will get there soon). Some functions have quite a few parameters (see below for even more). def in_circle (point_x, point_y, center_x, center_y, radius,) -> bool: return distance(center_x,center_y,point_,point_) <= radius This function calls the previously written distance function to compute whether a point (whose coordinates are point_x and point_y) falls within a circle (whose center's coordinates are center_x and center_y, and whose radius is radius). It returns True if the distance from the center of the circle to the points does not exceed the radius of the circle. Note that four of the coordinate parameters to in_circle become arguments to the call of distance; this role switch is common in functions calling other functions. By layering functions on top of functions (with later ones calling earlier ones) each is kept small, but each new function accomplishes much more because it calls other functions and leverages by building on their power. This layer mechanism enables power programming. Here are another function that appeared first in the if lecture, when illustrating cascaded if statements def compute_grade(test_percent): if test_percent >= 90: return 'A' elif test_percent >= 80: return 'B' elif test_percent >= 70: return 'C' elif test_percent >= 60: return 'D' else: return 'F' We will soon be able to write code that can read student names and test scores from a file, and we could use this function to help us print the name and grade of every student. We have already used the factorial function that we imported from the math library. If it were not there, we could write it ourselves, as follows. def factorial(n : int) -> int: answer = 1 for i in irange(2,n): answer *= i return answer This function is interesting because it defines two local names (answer and i, along with its parameter n). We accumulate the product of all the integers 1 up to n in the local name answer, by initializing it and repeatedly multiplying it by the next integer; finally we return the value it has accumulated. When writing functions, beginners sometimes have difficulty determining when to define a parameter name and when to define a local name. Here is where thinking about prototypes helps: any information that must be communicated to the function by arguments must be stored in a parameter; local names help in the computation, but do not need to be initialized by arguments. We need n to specify the factorial that we are computing, but answer is always initialized to 1 and i comes from iterating over an irange. Functions should have the fewest number of parameters possible; if a name can be defined locally, it should be. Remember that when functions terminate, all parameter names and local names are deleted from the function's namespace. Here are two more function we defined in the for loop lecture. def vowel_count(s : str) -> int: count = 0 for c in s: if c in 'aeoiuAEIOU': count += 1 return count def is_legal(word : str, dict_file : str) -> bool: # look for a word in the dictionary file for l in open(dict_file): if word == l.rstrip(): # found it; legal return True return False # couldn't find it in the dictionary: illegal The first returns a count of the vowels in its parameter s (using local names count and c), initializing and accumulating the answer in the local name count. The second determines if its word parameter is legal, by looking for it on every line in in the file whose name is dict_file. Thre is only one local name, l, which iterates over the lines in the file, whose rstipped values are compared to word for equality. Here the return statements each use literals: either True if one of the lines is found to be the word, and False (outside the loop, executed after it terminates) if no line had the word. Note that the return True statement that appears in the for loop, when executed, terminates the function, and therefore also terminates the for loop it is in: it does not have to execute a break statement to terminate the loop: once it knows the answer, it can execute return and be done with the function. Here are two similar is_legal functions (notice how each uses else:) one of which is CORRECT and one of which is INCORRECT. Can you spot the wrong one? More importantly, can you explain why it is wrong? They are different only in the indentation of the else: return False def is_legal(word : str, dict_file : str) -> bool: for l in open(dict_file): if word == l.rstrip(): return True else: # Here is the else: note its indentation return False def is_legal(word : str, dict_file : str) -> bool: for l in open(dict_file): if word == l.rstrip(): return True else: # Here is the else: note its indentation return False Here is a function that returns how many time its test_number parameter must go through the collatz process before it becomes 1. def cycle_count(test_number : int) -> int: count = 0 while test_number != 1: count += 1 if test_number % 2 == 0: test_number = test_number//2 else: test_number = 3*test_number + 1 return count And finally, here is a functional version of prompting for an int and trapping user-entry errors. Of course, the entire purpose of this function is to do safe input, so it does prompting and printing (in the case of errors) unlike any of these other functions. If you compare this code with the original, you will see that we added a prompt string as a parameter, and we directly return the result of calling int(string_rep), which either returns the correct int value or raises an exception that is handled and therefore stays within the loop. The exception interrupts the return statment. def prompt_for_int(message : str) -> int: while True: try: string_rep = input(message) return int(string_rep) except ValueError: print('Entry error (',string_rep,') is not a legal int') An example call is x = prompt_for_int('Enter x: ') These 10 functions appear in a functionproject download that accompanies this lecture. You can create a project with this module, and write another module to import its functions and call them, or just put calls to its functions inside calls to print at the end of this module. ------------------------------------------------------------------------------ Functions in scripts; functions in modules Some functions fulfill a very specific purpose in the module in which they are written. Other functions are more general, and might be reused in more than one script. We can place these general functions in a module and place the module in the courselib folder, so we can reuse its functions. Any module in the courselib is automatically available for import into our scripts, or any other modules. I have written a variety of general-purpose modules that are already in the courselib. It makes sense to catergorize similar functions and put them in the same module: it makes it easier for programmers to remember what to import and from where. It is a bit too early in your programming career to be thinking about writing general-purpose functions: I'm more concerned that you know how to call the general purpose functions already in Python's (and the courselib) library. Also, by using genaral purpose functions, you'll get some insight into what makes them reusable, and when it comes time to write your own, you'll have acquired some programming taste. One final word of caution. If you put functions/modules into your courselib, we will not have access to this code when we grade your assignments. So if you are asked to submit a script, you must put all the functions you wrote for that assignment in the script Don't submite code that imports from any modules you yourself wrote in courselib. ------------------------------------------------------------------------------ EBNF details/review (named parameters and default arguments) In this lecture we have discussed parameter/argument binding, but only for simple functions (where the parameters are bound to their matching argument by position). We have seen other functions (notably print) where we specify some parameters by position and others by names; and if we don't specify the names of these parameters, they are initialized with default arguments. As we become more sophisticated as programmers, and write more generally useful functions, we are more likely to write functions that specify default arguments for some parameter, and call these functions using a combination of positional and named arguments. To this end we review the EBNF for defining and calling functions. These details appear in Chapter 4; see sections 4.2/4.3 for details. qualified-name <= identifier{.identifier} annotation <= type-name default-argument <= =expression parameter <= name[:annotation][default-argument] | *[name[annotation]] function-header <= qualified-name([parameter{,parameter}]) [->annotation] Paraphrased, each function name and its parameter are indentifiers, with the parameters separated by commas. Each parameter can optionally be annotated and have a default argument. expression <= qualified-name | function-call argument <= [name=]expression function-call <= expression([argument{,argument}]) Paraphrased, each function call is typically an identifier, following by parentheses, in which arguments are separated by commas. Each argument can be just an expression, or an expression prefaced by the name of a parameter. ------------------------------------------------------------------------------ Argument/Parameter Matching (leaves out **kargs, talked about later) Let's explore the argument/parameter matching rules first discussed in Chapter 4. First we classify arguments and parameters, according the options they include. We will explore these rules generally first, and then apply them carefully to many examples. 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) ------------------------------------------------------------------------------ The raise statement (and assertions that can raise exceptions) We have already discussed how to handle raise exceptions with try/except statement. Now is an appropriate time to begin discussing the other end of exception processing: how to raise them after detecting a problem. The EBNF rule for raising an exception (using the keyword raise) is similar to return. raise_statement <= raise expression Expression it typically the name of some exception (see the end of the try/except lecture for a list) followed by () in which we put a string that identifies the problem. For example, given that our factorial method only works for non-negative integers, we might modify it as follows, to detect a bad argument and raise the ValueError exception with an appropriate message (rather than return 1, which is what the code would do for a negative number. def factorial(n : int) -> int: if n < 0 raise ValueError('math.factorial: n ('+n+') must be non-negative') answer = 1 for i in irange(2,n): answer *= i return answer A simple if statement, the first in the function, determines whether or not the argument is bad, and if so raises an exception. It is common to check all the necessary preconditions on arguments at the start of a function's body, grouping such code together and separating it from the code that actually performs the function's computation (which executes only after all the preconditions on the parameters have been checked, and non raise an exception). In this example, if the argument matching parameter n is a negative value, Python and raises the ValueError exception. When a raise statement raises an exception, Python abandons the sequential execution of the block in which it appears, and goes back to the site making the call to the function to determine if the call is in a try/except statement that handles the exception. Typically an exception message includes the module name, the function name, and some error message that might list the values that were found objectionable. We can also use Python's assert statement which has the EBNF assert_statement <= assert expression-B, expression-S where expression-B evalutes to a bool and expression-S evaluates to a string. Semantically, if expression-B evaluates to True, nothing happens (and the next statement in the sequence is executed); if expression-B evaluates to False, Python executes the equivalent of raise AssertionError(expression-S). So, if we want to raise the AssertionError exception instead of ValueError, we can replace the if/raise combination with just assert n>=0, 'math.factorial: n ('+n+') must be non-negative' Notice the test is opposite, saying what must be True for the exception NOT to be raised. We will continue to discuss exceptions throughout the quarter; for now, it is enough that you know how to raise exceptions with if/raise and assert. Meanwhile, it is very easy to experiment with raise and assert in the Python interpreter. ------------------------------------------------------------------------------ Statements that don't do anything It is possible to write something like x+1 as a statement in Python. This instructs Python to compute the value x+1, but not store it anywhere (no assignment statement) or print it (no print statement). Typically a statement like this is an intent error: we meant to store or print the result. It is common for begining programmers to forget to write return for their return statements, and just write expression whose value should be returned. Recall that if the last statement in a function is not a return statement, then Python will return None for the function. For example if we wrote the INCORRECT function def f(x): x+1 Calling print(f(1)) would print None. It does not return the value of x+1, it computes that value and then does nothing with it. And since the end of the function's body has now been executed, Python returns None. Here is the CORRECT way to write this function. def f(x): return x+1 Calling print(f(1)) would print 2. ------------------------------------------------------------------------------ A bit of magic for a later lecture The predicate module defines the function length_equal. If we call this function with an int, it returns a function object: this function object takes a string as a parameter and returns a boolean, whether the strings length is equal to specified length. Thus, we can write either: f = length_equal(3) # f refers to the function object length_equal(3) returns print( f('abc') ) # Call the function object with the string 'abc' which prints True; or we can write print( length_equal(3)('abc') ) Here length_equal(3) returns a reference to a function object, by following it with ('abc') we are calling that function object with the string 'abc'. Here is how the length_equal function is defined. def length_equal(i): def len_eq(s): return len(s) == i return len_eq Notice inside length_equal, the name len_eq is bound to a function that has a str parameter and returns a bool result; then the length_equal function returns this function object. This a a very powerful feature in Python.