Functions

Functions

Source: this section is heavily based on Chapter 4 of [ThinkCS].

Motivation

In an earlier chapter, we saw the following code:

n = int(input("Give a number: "))
count = 0
while n > 0:
    count = count + 1
    n = n // 10
print(count)

This code allowed the user to type in a number, performed a calculation on this number, and then printed the outcome of this calculation.

In this specific case, we were counting the number of digits in the decimal representation of an integer.

While this code is readable, and has desired functionality, most programmers do not consider this good code.

As a programmer you will often have to write programs that provide additional and more complex functionality than this. For instance, suppose we are implementing a calculator, and this calculator should implement additional functionality such as addition, subtraction, multiplication, division, sinus, cosinus...; such a program would become very long very quickly. We need some approach to structure code if many different functionalities have to be implemented.

The core building block in Python for organising the functionality of your code, is to divide your code into functions. In Python, a function is a named sequence of statements that belong together. Their primary purpose is to help us organize programs into chunks that match how we think about the problem.

For our earlier example, this is one way to write this code in an equivalent manner using a function:

def ndigits ( n ):
    count = 0
    while n > 0:
        count = count + 1
        n = n // 10
    return count

number = int(input("Give a number: "))
print(ndigits(number))

What we have done here, is that we have given a name to our calculation, ndigits. We have specified that the code of this calculation operates on a variable n. At the end of the calculation, we indicate that the outcome of the calculation is what is stored in the variable n.

Subsequently, in the unnamed part of our code we ask the user of our program to type in a number; for this number, we execute the ndigits function, and we print the outcome of the calculation. Note that when we execute this program, it will start by asking the user to give a number; the code within our function ndigits is not (yet) executed: this will only happen after the user has typed in a number, in the last line of the program.

The structure of this code is better. We have separated the user interaction from the calculation; it is now clear which chunk of the code calculates the number of digits.

Now it is easier to extend the program, for instance, consider this program:

def ndigits ( n ):
    count = 0
    while n > 0:
        count = count + 1
        n = n // 10
    return count

def sumdigits ( n ):
    digitsum = 0
    while n > 0:
        digitsum = digitsum + n % 10
        n = n // 10
    return digitsum

number = int(input("Give a number: "))
choice = int(input("Do you want to calculate the number of digits (1) or to sum the digits (2): "))
if choice == 1:
    print(ndigits(number))
if choice == 2:
    print(sumdigits(number))

Here we have added second functionality to our program. Study for yourself what this functionality does!

Moreover, it is easier to reuse code now. For instance, consider the following code:

def ndigits ( n ):
    count = 0
    while n > 0:
        count = count + 1
        n = n // 10
    return count

for i in [344,23,3493]:
    print(ndigits(i))

Our function ndigits has not changed; we only changed how it is used. In this particular case, we print the number of digits for 3 integers.

It is sometimes said that programmers are lazy; this is true. Most programmers want to write the same code only once. Functions allow you to do so and be lazy.

The core insight we had here is that the calculation done in ndigits could be used in a number of different ways; it is useful to put this code together and give it a name, such that we can reuse it.

Let's now consider what a function in general looks like.

Syntax

The general syntax for a function definition is:

def NAME( PARAMETERS ):
    STATEMENTS

We can make up any names we want for the functions we create, except that we can't use a name that is a Python keyword, and the names must follow the rules for legal identifiers.

There can be any number of statements inside the function, but they have to be indented from the def. In the examples in this book, we will use the standard indentation of four spaces. Function definitions are the second of several compound statements we will see, all of which have the same pattern:

  1. A header line which begins with a keyword and ends with a colon.
  2. A body consisting of one or more Python statements, each indented the same amount --- the Python style guide recommends 4 spaces --- from the header line.

We've already seen the for loop which follows this pattern.

So looking again at the function definition, the keyword in the header is def, which is followed by the name of the function and some parameters enclosed in parentheses. The parameter list may be empty, or it may contain any number of parameters separated from one another by commas. In either case, the parentheses are required. The parameters specifies what information, if any, we have to provide in order to use the new function.

A Turtle Example

Suppose we're working with turtles, and a common operation we need is to draw squares. "Draw a square" is an abstraction, or a mental chunk, of a number of smaller steps. So let's write a function to capture the pattern of this "building block":

import turtle

def draw_square(t, sz):
    """Make turtle t draw a square of sz."""
    for i in range(4):
        t.forward(sz)
        t.left(90)


wn = turtle.Screen()        # Set up the window and its attributes
wn.bgcolor("lightgreen")
wn.title("Alex meets a function")

alex = turtle.Turtle()      # Create alex
draw_square(alex, 50)       # Call the function to draw the square
wn.mainloop()
/syllabus/info1-theory/assets/alex04.png

This function is named draw_square. It has two parameters: one to tell the function which turtle to move around, and the other to tell it the size of the square we want drawn. Make sure you know where the body of the function ends --- it depends on the indentation, and the blank lines don't count for this purpose!

Docstrings for documentation

If the first thing after the function header is a string, it is treated as a docstring and gets special treatment in Python and in some programming tools. .. For example, when we type a built-in function name with an unclosed parenthesis in Tonny (Not), a tooltip pops up, telling us what arguments the function takes, and it shows us any other text contained in the docstring.

Docstrings are the key way to document our functions in Python and the documentation part is important. Because whoever calls our function shouldn't have to need to know what is going on in the function or how it works; they just need to know what arguments our function takes, what it does, and what the expected result is. Enough to be able to use the function without having to look underneath. This goes back to the concept of abstraction of which we'll talk more about.

Docstrings are usually formed using triple-quoted strings as they allow us to easily expand the docstring later on should we want to write more than a one-liner.

Just to differentiate from comments, a string at the start of a function (a docstring) is retrievable by Python tools at runtime. By contrast, comments are completely eliminated when the program is parsed.

Defining a new function does not make the function run. To do that we need a function call. We've already seen how to call some built-in functions like print, range and int. Function calls contain the name of the function being executed followed by a list of values, called arguments, which are assigned to the parameters in the function definition. So in the second last line of the program, we call the function, and pass alex as the turtle to be manipulated, and 50 as the size of the square we want. While the function is executing, then, the variable sz refers to the value 50, and the variable t refers to the same turtle instance that the variable alex refers to.

Once we've defined a function, we can call it as often as we like, and its statements will be executed each time we call it. And we could use it to get any of our turtles to draw a square. In the next example, we've changed the draw_square function a little, and we get tess to draw 15 squares, with some variations.

import turtle

def draw_multicolor_square(t, sz):
    """Make turtle t draw a multi-color square of sz."""
    for i in ["red", "purple", "hotpink", "blue"]:
        t.color(i)
        t.forward(sz)
        t.left(90)

wn = turtle.Screen()        # Set up the window and its attributes
wn.bgcolor("lightgreen")

tess = turtle.Turtle()      # Create tess and set some attributes
tess.pensize(3)

size = 20                   # Size of the smallest square
for i in range(15):
    draw_multicolor_square(tess, size)
    size = size + 10        # Increase the size for next time
    tess.forward(10)        # Move tess along a little
    tess.right(18)          #    and give her some turn

wn.mainloop()
/syllabus/info1-theory/assets/tess05.png

Functions can call other functions

Let's assume now we want a function to draw a rectangle. We need to be able to call the function with different arguments for width and height. And, unlike the case of the square, we cannot repeat the same thing 4 times, because the four sides are not equal.

So we eventually come up with this rather nice code that can draw a rectangle.

def draw_rectangle(t, w, h):
    """Get turtle t to draw a rectangle of width w and height h."""
    for i in range(2):
        t.forward(w)
        t.left(90)
        t.forward(h)
        t.left(90)

The parameter names are deliberately chosen as single letters to ensure they're not misunderstood. In real programs, once we've had more experience, we will insist on better variable names than this. But the point is that the program doesn't "understand" that we're drawing a rectangle, or that the parameters represent the width and the height. Concepts like rectangle, width, and height are the meaning we humans have, not concepts that the program or the computer understands.

Thinking like a scientist involves looking for patterns and relationships. In the code above, we've done that to some extent. We did not just draw four sides. Instead, we spotted that we could draw the rectangle as two halves, and used a loop to repeat that pattern twice.

But now we might spot that a square is a special kind of rectangle. We already have a function that draws a rectangle, so we can use that to draw our square.

def draw_square(tx, sz):        # A new version of draw_square
    draw_rectangle(tx, sz, sz)

There are some points worth noting here:

  • Functions can call other functions.
  • Rewriting draw_square like this captures the relationship that we've spotted between squares and rectangles.
  • A caller of this function might say draw_square(tess, 50). The parameters of this function, tx and sz, are assigned the values of the tess object, and the int 50 respectively.
  • In the body of the function they are just like any other variable.
  • When the call is made to draw_rectangle, the values in variables tx and sz are fetched first, then the call happens. So as we enter the top of function draw_rectangle, its variable t is assigned the tess object, and w and h in that function are both given the value 50.

So far, it may not be clear why it is worth the trouble to create all of these new functions. Actually, there are a lot of reasons, but this example demonstrates two:

  1. Creating a new function gives us an opportunity to name a group of statements. Functions can simplify a program by hiding a complex computation behind a single command. The function (including its name) can capture our mental chunking, or abstraction, of the problem.
  2. Creating a new function can make a program smaller by eliminating repetitive code.

As we might expect, we have to create a function before we can execute it. In other words, the function definition has to be executed before the function is called.

Flow of execution

In order to ensure that a function is defined before its first use, we have to know the order in which statements are executed, which is called the flow of execution. We've already talked about this a little in an earlier chapter.

Execution always begins at the first statement of the program. Statements are executed one at a time, in order from top to bottom.

Function definitions do not alter the flow of execution of the program, but remember that statements inside the function are not executed until the function is called. Although it is not common, we can define one function inside another. In this case, the inner definition isn't executed until the outer function is called.

Function calls are like a detour in the flow of execution. Instead of going to the next statement, the flow jumps to the first line of the called function, executes all the statements there, and then comes back to pick up where it left off.

That sounds simple enough, until we remember that one function can call another. While in the middle of one function, the program might have to execute the statements in another function. But while executing that new function, the program might have to execute yet another function!

Fortunately, Python is adept at keeping track of where it is, so each time a function completes, the program picks up where it left off in the function that called it. When it gets to the end of the program, it terminates.

What's the moral of this sordid tale? When we read a program, don't read from top to bottom. Instead, follow the flow of execution.

Watch the flow of execution in action

In Thonny, we can watch the flow of execution by "stepping" through any program. Thonny will highlight the code that is being executed and will show the values of the variables. One can also enable a view of all the variables in a side pane of the tool.

This is a powerful mechanism for building a deep and thorough understanding of what is happening at each step of the way. Learn to use stepping feature well, and be mentally proactive: as you work through the code, challenge yourself before each step: "What changes will this line make to any variables in the program?" and "Where will flow of execution go next?"

Let us go back and see how this works with the program above that draws 15 multicolor squares. First, we're going to add one line of magic below the import statement --- not strictly necessary, but it will make our lives much simpler, because it prevents stepping into the module containing the turtle code.

import turtle
__import__("turtle").__traceable__ = False

Now we're ready to begin. Put the mouse cursor on the line number of the line where we create the turtle screen, and double click. A red circle will appear; this indicates we wish to stop the execution of the progam at this line. Subsequently, start debugging the program by pressing the "debug" button. This will run the Python program up to, but not including, the line where we have put the red circle. Our program will "break" now, and provide a highlight on the next line to be executed, something like this:

/syllabus/info1-theory/assets/breakpoint_new.png

At this point we can press the F7 key (step into) repeatedly to single step through the code. Observe as we execute lines 10, 11, 12, ... how the turtle window gets created, how its canvas color is changed, how the title gets changed, how the turtle is created on the canvas, and then how the flow of execution gets into the loop, and from there into the function, and into the function's loop, and then repeatedly through the body of that loop.

While we do this, we also see the values of the variables, and can confirm that their values match our conceptual model of what is happening.

After a few loops, when we're about to execute line 20 and we're starting to get bored, we can use the key F6 to "step over" the function we are calling. This executes all the statements in the function, but without having to step through each one. We always have the choice to either "go for the detail", or to "take the high-level view" and execute the function as a single chunk.

There are some other options, including one that allow us to resume execution without further stepping.

Functions that require arguments

Most functions require arguments: the arguments provide for generalization. For example, if we want to find the absolute value of a number, we have to indicate what the number is. Python has a built-in function for computing the absolute value:

>>> abs(5)
5
>>> abs(-5)
5

In this example, the arguments to the abs function are 5 and -5.

Some functions take more than one argument. For example the built-in function pow takes two arguments, the base and the exponent. Inside the function, the values that are passed get assigned to variables called parameters.

>>> pow(2, 3)
8
>>> pow(7, 4)
2401

Another built-in function that takes more than one argument is max.

>>> max(7, 11)
11
>>> max(4, 1, 17, 2, 12)
17
>>> max(3 * 11, 5**3, 512 - 9, 1024**0)
503

max can be passed any number of arguments, separated by commas, and will return the largest value passed. The arguments can be either simple values or expressions. In the last example, 503 is returned, since it is larger than 33, 125, and 1.

Functions that return values

All the functions in the previous section return values. Furthermore, functions like range, int, abs all return values that can be used to build more complex expressions.

So an important difference between these functions and one like draw_square is that draw_square was not executed because we wanted it to compute a value --- on the contrary, we wrote draw_square because we wanted it to execute a sequence of steps that caused the turtle to draw.

A function that returns a value is called a fruitful function in this book. The opposite of a fruitful function is void function --- one that is not executed for its resulting value, but is executed because it does something useful. (Languages like Java, C#, C and C++ use the term "void function", other languages like Pascal call it a procedure.) Even though void functions are not executed for their resulting value, Python always wants to return something. So if the programmer doesn't arrange to return a value, Python will automatically return the value None.

How do we write our own fruitful function? In an earlier chapter we saw the standard formula for compound interest, which we'll now write as a fruitful function:

/syllabus/info1-theory/assets/compoundInterest.png
def final_amt(p, r, n, t):
    """
      Apply the compound interest formula to p
       to produce the final amount.
    """

    a = p * (1 + r/n) ** (n*t)
    return a         # This is new, and makes the function fruitful.

# now that we have the function above, let us call it.
toInvest = float(input("How much do you want to invest?"))
fnl = final_amt(toInvest, 0.08, 12, 5)
print("At the end of the period you'll have", fnl)
  • The return statement is followed by an expression (a in this case). This expression will be evaluated and returned to the caller as the "fruit" of calling this function.

  • We prompted the user for the principal amount. The type of toInvest is a string, but we need a number before we can work with it. Because it is money, and could have decimal places, we've used the float type converter function to parse the string and return a float.

  • Notice how we entered the arguments for 8% interest, compounded 12 times per year, for 5 years.

  • When we run this, we get the output

    At the end of the period you'll have 14898.457083

    This is a bit messy with all these decimal places, but remember that Python doesn't understand that we're working with money: it just does the calculation to the best of its ability, without rounding. Later we'll see how to format the string that is printed in such a way that it does get nicely rounded to two decimal places before printing.

  • The line toInvest = float(input("How much do you want to invest?")) also shows yet another example of composition --- we can call a function like float, and its arguments can be the results of other function calls (like input) that we've called along the way.

  • Note how also in this example we have separated the user interaction from the calculation, which is done in the function, as in the example at the beginning of this chapter.

Notice something else very important here. The name of the variable we pass as an argument --- toInvest --- has nothing to do with the name of the parameter --- p. It is as if p = toInvest is executed when final_amt is called. It doesn't matter what the value was named in the caller, in final_amt its name is p.

These short variable names are getting quite tricky, so perhaps we'd prefer one of these versions instead:

def final_amt_v2(principalAmount, nominalPercentageRate,
                                    numTimesPerYear, years):
    a = principalAmount * (1 + nominalPercentageRate /
                         numTimesPerYear) ** (numTimesPerYear*years)
    return a

def final_amt_v3(amt, rate, compounded, years):
    a = amt * (1 + rate/compounded) ** (componded*years)
    return a

They all do the same thing. Use your judgement to write code that can be best understood by other humans! Short variable names are more economical and sometimes make code easier to read: E = mc2 would not be nearly so memorable if Einstein had used longer variable names! If you do prefer short names, make sure you also have some comments to enlighten the reader about what the variables are used for.

Variables and parameters are local

When we create a local variable inside a function, it only exists inside the function, and we cannot use it outside. For example, consider again this function:

def final_amt(p, r, n, t):
    a = p * (1 + r/n) ** (n*t)
    return a

If we try to use a, outside the function, we'll get an error:

>>> a
NameError: name 'a' is not defined

The variable a is local to final_amt, and is not visible outside the function.

Additionally, a only exists while the function is being executed --- we call this its lifetime. When the execution of the function terminates, the local variables are destroyed.

Parameters are also local, and act like local variables. For example, the lifetimes of p, r, n, t begin when final_amt is called, and the lifetime ends when the function completes its execution.

So it is not possible for a function to set some local variable to a value, complete its execution, and then when it is called again next time, recover the local variable. Each call of the function creates new local variables, and their lifetimes expire when the function returns to the caller.

Turtles Revisited

Now that we have fruitful functions, we can focus our attention on reorganizing our code so that it fits more nicely into our mental chunks. This process of rearrangement is called refactoring the code.

Two things we're always going to want to do when working with turtles is to create the window for the turtle, and to create one or more turtles. We could write some functions to make these tasks easier in future:

def make_window(colr, ttle):
    """
      Set up the window with the given background color and title.
      Returns the new window.
    """
    w = turtle.Screen()
    w.bgcolor(colr)
    w.title(ttle)
    return w


def make_turtle(colr, sz):
    """
      Set up a turtle with the given color and pensize.
      Returns the new turtle.
    """
    t = turtle.Turtle()
    t.color(colr)
    t.pensize(sz)
    return t


wn = make_window("lightgreen", "Tess and Alex dancing")
tess = make_turtle("hotpink", 5)
alex = make_turtle("black", 1)
dave = make_turtle("yellow", 2)

The trick about refactoring code is to anticipate which things we are likely to want to change each time we call the function: these should become the parameters, or changeable parts, of the functions we write.

A Turtle Bar Chart

The turtle has a lot more power than we've seen so far. The full documentation can be found at https://docs.python.org/3/library/turtle.html.

Here are a couple of new tricks for our turtles:

  • We can get a turtle to display text on the canvas at the turtle's current position. The method to do that is alex.write("Hello").
  • We can fill a shape (circle, semicircle, triangle, etc.) with a color. It is a two-step process. First we call the method alex.begin_fill(), then we draw the shape, then we call alex.end_fill().
  • We've previously set the color of our turtle --- we can now also set its fill color, which need not be the same as the turtle and the pen color. We use alex.color("blue","red") to set the turtle to draw in blue, and fill in red.

Ok, so can we get tess to draw a bar chart? Let us start with some data to be charted,

xs = [48, 117, 200, 240, 160, 260, 220]

Corresponding to each data measurement, we'll draw a simple rectangle of that height, with a fixed width.

def draw_bar(t, height):
    """ Get turtle t to draw one bar, of height. """
    t.left(90)
    t.forward(height)     # Draw up the left side
    t.right(90)
    t.forward(40)         # Width of bar, along the top
    t.right(90)
    t.forward(height)     # And down again!
    t.left(90)            # Put the turtle facing the way we found it.
    t.forward(10)         # Leave small gap after each bar

...
for v in xs:              # Assume xs and tess are ready
    draw_bar(tess, v)
/syllabus/info1-theory/assets/tess_bar_1.png

Ok, not fantasically impressive, but it is a nice start! The important thing here was the mental chunking, or how we broke the problem into smaller pieces. Our chunk is to draw one bar, and we wrote a function to do that. Then, for the whole chart, we repeatedly called our function.

Next, at the top of each bar, we'll print the value of the data. We'll do this by adding a function. In the body of draw_bar, by adding t.write('  ' + str(height)) as the new third line of the body. We've put a little space in front of the number, and turned the number into a string. Without this extra space we tend to cramp our text awkwardly against the bar to the left. The result looks a lot better now:

/syllabus/info1-theory/assets/tess_bar_2.png

And now we'll add two lines to fill each bar. Our final program now looks like this:

def draw_bar(t, height):
    """ Get turtle t to draw one bar, of height. """
    t.begin_fill()           # Added this line
    t.left(90)
    t.forward(height)
    t.write("  "+ str(height))
    t.right(90)
    t.forward(40)
    t.right(90)
    t.forward(height)
    t.left(90)
    t.end_fill()             # Added this line
    t.forward(10)

wn = turtle.Screen()         # Set up the window and its attributes
wn.bgcolor("lightgreen")

tess = turtle.Turtle()       # Create tess and set some attributes
tess.color("blue", "red")
tess.pensize(3)

xs = [48,117,200,240,160,260,220]

for a in xs:
    draw_bar(tess, a)

wn.mainloop()

It produces the following, which is more satisfying:

/syllabus/info1-theory/assets/tess_bar_3.png

Mmm. Perhaps the bars should not be joined to each other at the bottom. We'll need to pick up the pen while making the gap between the bars. We'll leave that as an exercise for you!

Help and meta-notation

Python comes with extensive documentation for all its built-in functions, and its libraries.

The first source information is the website of Python itself. For the version of Python we are using, this documentation can be found on https://docs.python.org/3/. For instance, the documentation of the range function can be found here: https://docs.python.org/3/library/stdtypes.html#typesseq-range.

Notice the square brackets in the description of the arguments. These are examples of meta-notation --- notation that describes Python syntax, but is not part of it. The square brackets in this documentation mean that the argument is optional --- the programmer can omit it. So what this first line of help tells us is that range must always have a stop argument, but it may have an optional start argument (which must be followed by a comma if it is present), and it can also have an optional step argument, preceded by a comma if it is present.

The examples show that range can have either 1, 2 or 3 arguments. The list can start at any starting value, and go up or down in increments other than 1. The documentation here also says that the arguments must be integers.

Other meta-notation you'll frequently encounter is the use of bold and italics. The bold means that these are tokens --- keywords or symbols --- typed into your Python code exactly as they are, whereas the italic terms stand for "something of this type". So the syntax description

for variable in list :

means you can substitute any legal variable and any legal list when you write your Python code.

Towards encapsulation and generalization

In the end, most of the programs we write consist of functions. However, decomposing a program into functions is not an easy task. In the following sections we give another elaborate example to illustrate how to write a program that consists of functions. To this aim, we will start to write a program without functions, which we will subsequently improve afterwards.

In this example, we are interested in creating tables. Before computers were readily available, people had to calculate logarithms, sines and cosines, and other mathematical functions by hand. To make that easier, mathematics books contained long tables listing the values of these functions. Creating the tables was slow and boring, and they tended to be full of errors.

When computers appeared on the scene, one of the initial reactions was, "This is great! We can use the computers to generate the tables, so there will be no errors." That turned out to be true (mostly) but shortsighted. Soon thereafter, computers and calculators were so pervasive that the tables became obsolete.

Well, almost. For some operations, computers use tables of values to get an approximate answer and then perform computations to improve the approximation. In some cases, there have been errors in the underlying tables, most famously in the table the Intel Pentium processor chip used to perform floating-point division.

Although a log table is not as useful as it once was, it still makes a good example. The following program outputs a sequence of values in the left column and 2 raised to the power of that value in the right column:

for x in range(13):   # Generate numbers 0 to 12
    print(x, "\t", 2**x)

The string "\t" represents a tab character. The backslash character in "\t" indicates the beginning of an escape sequence. Escape sequences are used to represent invisible characters like tabs and newlines. The sequence \n represents a newline.

An escape sequence can appear anywhere in a string; in this example, the tab escape sequence is the only thing in the string.

As characters and strings are displayed on the screen, an invisible marker called the cursor keeps track of where the next character will go. After a print function, the cursor normally goes to the beginning of the next line.

The tab character shifts the cursor to the right until it reaches one of the tab stops. Tabs are useful for making columns of text line up, as in the output of the previous program:

0       1
1       2
2       4
3       8
4       16
5       32
6       64
7       128
8       256
9       512
10      1024
11      2048
12      4096

Because of the tab characters between the columns, the position of the second column does not depend on the number of digits in the first column.

Two-dimensional tables

Let's now make our example a little more complex.

A two-dimensional table is a table where you read the value at the intersection of a row and a column. A multiplication table is a good example. Let's say you want to print a multiplication table for the values from 1 to 6.

A good way to start is to write a loop that prints the multiples of 2, all on one line:

for i in range(6):
    print(2 * (i+1), end="   ")
print()

Here we've used the range function. As the loop executes, the value of i changes from 0 to 5. When all the elements of the range have been assigned to i, the loop terminates. Each time through the loop, it displays the value of 2 * i, followed by three spaces.

Again, the extra end="   " argument in the print function suppresses the newline, and uses three spaces instead. After the loop completes, the call to print at line 3 finishes the current line, and starts a new line.

The output of the program is:

2      4      6      8      10     12

So far, so good. We now have some pieces of code that we wish to put in functions. The next step is to encapsulate and generalize.

Encapsulation and generalization

Encapsulation is the process of wrapping a piece of code in a function, allowing you to take advantage of all the things functions are good for.

Generalization means taking something specific, such as printing the multiples of 2, and making it more general, such as printing the multiples of any integer.

This function encapsulates the previous loop and generalizes it to print multiples of n:

def print_multiples(n):
    for i in range(6):
        print(n * (i+1), end="   ")
    print()

To encapsulate, all we had to do was add the first line, which declares the name of the function and the parameter list. To generalize, all we had to do was replace the value 2 with the parameter n.

If we call this function with the argument 2, we get the same output as before. With the argument 3, the output is:

3      6      9      12     15     18

With the argument 4, the output is:

4      8      12     16     20     24

By now you can probably guess how to print a multiplication table --- by calling print_multiples repeatedly with different arguments. In fact, we can use another loop:

for i in range(6):
    print_multiples(i+1)

Notice how similar this loop is to the one inside print_multiples. All we did was replace the print function with a function call.

The output of this program is a multiplication table:

1      2      3      4      5      6
2      4      6      8      10     12
3      6      9      12     15     18
4      8      12     16     20     24
5      10     15     20     25     30
6      12     18     24     30     36

More encapsulation

To demonstrate encapsulation again, let's take the code from the last section and wrap it up in a function:

def print_mult_table():
    for i in range(6):
        print_multiples(i+1)

This process is a common development plan. We develop code by writing lines of code outside any function, or typing them in to the interpreter. When we get the code working, we extract it and wrap it up in a function.

This development plan is particularly useful if you don't know how to divide the program into functions when you start writing. This approach lets you design as you go along.

Local variables

You might be wondering how we can use the same variable, i, in both print_multiples and print_mult_table. Doesn't it cause problems when one of the functions changes the value of the variable?

The answer is no, because the i in print_multiples and the i in print_mult_table are not the same variable.

Variables created inside a function definition are local; you can't access a local variable from outside its home function. That means you are free to have multiple variables with the same name as long as they are not in the same function.

Python examines all the statements in a function --- if any of them assign a value to a variable, that is the clue that Python uses to make the variable a local variable.

The stack diagram for this program shows that the two variables named i are not the same variable. They can refer to different values, and changing one does not affect the other.

Stack 2 diagram

The value of i in print_mult_table goes from 1 to 6. In the diagram it happens to be 3. The next time through the loop it will be 4. Each time through the loop, print_mult_table calls print_multiples with the current value of i as an argument. That value gets assigned to the parameter n.

Inside print_multiples, the value of i goes from 1 to 6. In the diagram, it happens to be 2. Changing this variable has no effect on the value of i in print_mult_table.

It is common and perfectly legal to have different local variables with the same name. In particular, names like i and j are used frequently as loop variables. If you avoid using them in one function just because you used them somewhere else, you will probably make the program harder to read.

The visualizer at http://www.pythontutor.com/visualize.html shows very clearly how the two variables i are distinct variables, and how they have independent values.

More generalization

As another example of generalization, imagine you wanted a program that would print a multiplication table of any size, not just the six-by-six table. You could add a parameter to print_mult_table:

def print_mult_table(high):
    for i in range(high):
        print_multiples(i+1)

We replaced the value 6 with the expression high. If we call print_mult_table with the argument 7, it displays:

1      2      3      4      5      6
2      4      6      8      10     12
3      6      9      12     15     18
4      8      12     16     20     24
5      10     15     20     25     30
6      12     18     24     30     36
7      14     21     28     35     42

This is fine, except that we probably want the table to be square --- with the same number of rows and columns. To do that, we add another parameter to print_multiples to specify how many columns the table should have.

Just to be annoying, we call this parameter high, demonstrating that different functions can have parameters with the same name (just like local variables). Here's the whole program:

def print_multiples(n, high):
    for i in range(high):
        print(n * (i+1), end="   ")
    print()

def print_mult_table(high):
    for i in range(high):
        print_multiples(i+1, high)

Notice that when we added a new parameter, we had to change the first line of the function (the function heading), and we also had to change the place where the function is called in print_mult_table.

Now, when we call print_mult_table(7):

1      2      3      4      5      6      7
2      4      6      8      10     12     14
3      6      9      12     15     18     21
4      8      12     16     20     24     28
5      10     15     20     25     30     35
6      12     18     24     30     36     42
7      14     21     28     35     42     49

When you generalize a function appropriately, you often get a program with capabilities you didn't plan. For example, you might notice that, because ab = ba, all the entries in the table appear twice. You could save ink by printing only half the table. To do that, you only have to change one line of print_mult_table. Change

print_multiples(i, high)

to

print_multiples(i, i)

and you get:

1
2      4
3      6      9
4      8      12     16
5      10     15     20     25
6      12     18     24     30     36
7      14     21     28     35     42     49

Functions

A few times now, we have mentioned all the things functions are good for. Let's summarize this:

  1. Capturing your mental chunking. Breaking your complex tasks into sub-tasks, and giving the sub-tasks a meaningful name is a powerful mental technique. Look back at the example that illustrated the post-test loop: we assumed that we had a function called play_the_game_once. This chunking allowed us to put aside the details of the particular game --- is it a card game, or noughts and crosses, or a role playing game --- and simply focus on one isolated part of our program logic --- letting the player choose whether they want to play again.
  2. Dividing a long program into functions allows you to separate parts of the program, debug them in isolation, and then compose them into a whole.
  3. Functions facilitate the use of iteration.
  4. Well-designed functions are often useful for many programs. Once you write and debug one, you can reuse it.

Glossary

argument
A value provided to a function when the function is called. This value is assigned to the corresponding parameter in the function. The argument can be the result of an expression which may involve operators, operands and calls to other fruitful functions.
body
The second part of a compound statement. The body consists of a sequence of statements all indented the same amount from the beginning of the header. The standard amount of indentation used within the Python community is 4 spaces.
compound statement

A statement that consists of two parts:

  1. header - which begins with a keyword determining the statement type, and ends with a colon.
  2. body - containing one or more statements indented the same amount from the header.

The syntax of a compound statement looks like this:

keyword ... :
    statement
    statement ...
docstring
A special string that is attached to a function as its __doc__ attribute. Tools can use docstrings to provide documentation or hints for the programmer. When we get to modules, classes, and methods, we'll see that docstrings can also be used there.
flow of execution
The order in which statements are executed during a program run.
frame
A box in a stack diagram that represents a function call. It contains the local variables and parameters of the function.
function
A named sequence of statements that performs some useful operation. Functions may or may not take parameters and may or may not produce a result.
function call
A statement that executes a function. It consists of the name of the function followed by a list of arguments enclosed in parentheses.
function composition
Using the output from one function call as the input to another.
function definition
A statement that creates a new function, specifying its name, parameters, and the statements it executes.
fruitful function
A function that returns a value when it is called.
header line
The first part of a compound statement. A header line begins with a keyword and ends with a colon (:)
import statement
A statement which permits functions and variables defined in another Python module to be brought into the environment of another script. To use the features of the turtle, we need to first import the turtle module.
lifetime
Variables and objects have lifetimes --- they are created at some point during program execution, and will be destroyed at some time.
local variable
A variable defined inside a function. A local variable can only be used inside its function. Parameters of a function are also a special kind of local variable.
parameter
A name used inside a function to refer to the value which was passed to it as an argument.
refactor
A fancy word to describe reorganizing our program code, usually to make it more understandable. Typically, we have a program that is already working, then we go back to "tidy it up". It often involves choosing better variable names, or spotting repeated patterns and moving that code into a function.
stack diagram
A graphical representation of a stack of functions, their variables, and the values to which they refer.
traceback
A list of the functions that are executing, printed when a runtime error occurs. A traceback is also commonly refered to as a stack trace, since it lists the functions in the order in which they are stored in the runtime stack.
void function
The opposite of a fruitful function: one that does not return a value. It is executed for the work it does, rather than for the value it returns.

References

[ThinkCS]How To Think Like a Computer Scientist --- Learning with Python 3

Page précédente Page suivante