Function Execution in Python

When a function is called, the first statement of that function starts to ex­ecute, and it continues, statement by statement, through the code until the last statement of that function or until it returns prematurely. When that last state­ment executes, then the execution continues from the place where it was called. As a function can be called from many places, Python has to remember where the function was called so that it can return. Parameters can be expressions or variables, and normally differ each time the function is called. Functions can also access variables defined elsewhere.

Most importantly, functions return values.

1. Returning a Value

All functions return a value, and as such can be treated within expressions as if they were variables having that value. Assuming the existence of a cosine function, it could be used in an expression in the usual ways. For example,

x = cosine(x)*r

if cosine(x) < 0.5:

print (cosine(x)*cosine(x))

In these cases, the value returned by the function is used by the code to cal­culate a further value or to create output. The expression “cosine(x)” resolves to a value of some Python type. The most common purpose of a function is to cal­culate a value, which is then returned to the calling part of the program and can possibly be used in a further calculation. But how does a function get its value?

In a return statement.

The return statement assigns a value and a type to the object returned by the function. It also stops executing the function and resumes execution at the loca­tion where the function was called. A simple example would be to return a single value, such as an integer or floating-point number:

return 0

returns the value 0 from a function. The return value could be an expression:

return x*x + y*y

A function has only one return value, but it can be of any type, so could be a list or tuple that contains multiple components:

return (2,3,5,7,11)

return [“fluorine”,”chlorine”,”bromine”,”iodine”,“astatine”]

Expressions can include function calls, so a return value can be defined in this way as well; for example

return cosine(x)

One of the simplest functions that can be used as an example is one that cal­culates the square of its parameter.

def square (x):

return x*x

The print statement

print (square(12))

prints

144

Interestingly, the statement

print(square(12.0))

prints

144.0

The same function returns an integer in one case and a float in the other. Why? Because the function returns the result of an expression involving its pa­rameter, which in one case was integer and in the other was real. This implies that a function has no fixed type and can return any type at all. Indeed, the same function can have return statements that return an integer, a float, a string, and a list independent of type of the parameter passed:

def test (x):   # Return one of four types depending on x

if x<1:

return 1

if x<2:

return 2.0

if x<3:

return “3”

return [1,2,3,4]

print (test(0))

print (test(1))

print (test(2))

print (test(3))

The output is as follows:

1

2.0

3

[1, 2, 3, 4]

Problem: Write a function to calculate the square root of its parameter.

Two thousand years ago, the Babylonians had a way to calculate the square root of a number. They understood the definition of a square root: that if y*y = x, then y is the square root of x. They figured out that if y was an over-estimate to the true value of the square root of x, then x/y would be an underestimate. In that case, a better guess would be to average those two values: the next guess would be y1 = (y + x/y)/2. The guess after that would be y2 = (y1+x/y1)/2, and so on. At any point in the calculation, the error (difference between the correct answer and the estimate) can be found by squaring the guess yi and subtracting x from it, knowing that yi*yi is supposed to equal x.

The function therefore starts by guessing what the square root might be. It cannot be 0, because then x/y would be undefined. x is a good guess. Then, we construct a loop based on the expression y2 = (y1+x/y1)/2, or more generally, yi+1 = (yi+x/yi)/2 for iteration i. At first, run this loop a fixed number of times (here, we use 20 times).

This correctly computes the square root of 2 to 15 decimal places. This is probably more than is necessary, meaning that the loop is executing more times than it needs to. In fact, changing the 20 iterations to only 6 still gives 15 correct places. This is exceptional accuracy: if the distance between the Earth and the sun were known this accurately, it would be within 0.006 inches of the correct value. The Babylonians were very clever.

What’s the square root of 10000? If the number of iterations is kept at 6, then the answer is a very poor one indeed: 323.1. Why? Some numbers (large ones) need more iterations than others. To guarantee that a good estimate of the square root is returned, an estimate of the error should be used. When the error is small enough, then the value is good enough. The error is x-yi*yi. The function should not loop a fixed number of times, but instead should repeat until the error is less than, say, 0.0000001. This function is named roote, where the “e” is for “error.”

This function will return the square root of any positive value of x to within 7 decimal places. It should check for negative values, though.

2. Parameters

A parameter can be either a name, meaning that it is a Python variable (ob­ject) of some kind, or an expression, meaning it has a value but no permanence in that it can’t be accessed later on – it has no name. Both are passed to a function as an object reference. The expression is evaluated before being given to the func­tion and its type does not matter in so far as Python will always know what it is; its value is assigned a name when it is passed. Consider, for example, the function square in the following context:

pi = 3.14159 r = 2.54

c = square (2*pi*r)

print (“Circumference is “, c)

The assignments to pi and r are performed, and when the call to square oc­curs, the expression 2*pi*r is evaluated first. Its value is assigned to a temporary variable, which is passed as the parameter to square. Inside the function, this parameter is named x, and the function calculates x squared and returns it as a value. It is as if the following code executes:

This is not how a function is implemented, but shows how the parameter is effectively passed; a copy is made of the parameters and those are passed. If the expression 2*pi*r was changed to a simple variable, then the internal location of that variable would be passed.

Passing more structured objects works the same way, but they can behave differently. If a list is passed to a function, then the list itself cannot be modified, but the contents of the list can be. The list is assigned another name, but it is the same list. To be clear, consider a simple function that edits a list by adding a new element to the end:

def addend (arg):

arg.append(“End”)

z = [“Start”, “Add”, “Multiply”]

print (1, z)

addend(z)

print (1, z)

The list associated with the variable z is changed by this function call. It now ends with the string “End.” The output from this is

1 [ꞌStartꞌ, ꞌAddꞌ, ꞌMultiplyꞌ] 2 [ꞌStartꞌ, ꞌAddꞌ, ꞌMultiplyꞌ, ꞌEndꞌ]

This is the resulting output because the name z refers to a thing that consists of many other parts. The name z is used to access them, and the function cannot modify the value of z itself. It can modify what z indicates; that is, the compo­nents. Think of it, if it makes it simpler, as a level of indirection. A book can be exchanged between two people. The receiver writes a not in it and gives it back. It’s the same book, but the contents are now different.

A small modification to addend() illustrates some confusing behavior. In­stead of using append to add “End” to the list, use the concatenation operator, +:

def addend (arg):

arg = arg + [“End”]

z = [“Start”, “Add”, “Multiply”]

print (1, z)

addend(z)

print (2, z)

The output is as follows:

1 [ꞌStartꞌ, ꞌAddꞌ, ꞌMultiplyꞌ] 2 [ꞌStartꞌ, ꞌAddꞌ, ꞌMultiplyꞌ]

The component “End” is not a part of the list z anymore. It was made a com­ponent inside of the function, but it’s not present after the function returns. This is because the statement

arg = arg + [“End”]

creates a new list with “End” as the final component, and then assigns that new list as a value to arg. This represents an attempt to change the value that was passed, which cannot happen: changing the value of arg will not change the value of the passed variable z. Within the function arg, there is a new list with “End” as the final component. Outside, the list z has not changed.

The way that Python passes parameters is the subject of a lot of discussion on Internet blogs and lists. There are many names given for the method used, and while the technique is understood, it does differ from the way parameters are passed in other languages and is confusing to people who learned another language like Java or C before Python. It is important to remember that the actual value of an object reference being passed cannot be assigned a new value inside the function, but the things that it references or points to can be modified.

Multiple parameters are passed by position; the first parameter passed is given to the first one listed in the function declaration, the second one passed to given to the second one listed in the declaration, and so on. They are all passed in the same manner: as object references.

3. Default Parameters

It is possible to specify a value for a parameter in the instance that it is not given one by the caller. That may not seem to make sense, but the implication is that it will sometimes be passed explicitly and sometimes not. When debugging code it is common to embed print statements in specific places to show that the program has reached that point. Sometimes it is important to print out a variable or value there, other times, it is just to show that the program got to that statement safely. Consider a function named gothere:

def gothere (count, value):

print (“Got Here:  “,count, ” value is “, value)

then throughout the program, calls to gothere would be sprinkled with a different value for count every time; the value of count indicates the statement that has been reached. This is a way of instrumenting the program, and can be very useful for finding errors. The code being debugged may look like the following:

year = 2015          # The code below is not especially

                     # meaningful

a = year % 19        # and is an example only.

gothere(1, 0)

b = year // 100

c = year % 100

gothere (2,  0)

d = (19 * a + b – b // 4 – ((b – (b + 8) // 25 + 1) // 3) + 15) % 30

e = (32 + 2 * (b % 4) + 2 * (c // 4) – d – (c % 4)) % 7

f = d + e – 7 * ((a + 11 * d + 22 * e) // 451) + 114

gothere (3, f)

month = f // 31

day = f % 31 + 1

gothere(4, day)

return date(year, month, day)

The output is as follows:

Got Here: 1 value is 0

Got Here: 2 value is 0

Got Here: 3 value is 128

Got Here: 4 value is 5

2015 4 5

The program reaches each of the four checkpoints and prints a proper mes­sage. The first two calls to gothere did not need to print a value, only the count number. The second parameter could be given a default value, perhaps None, and then it would not have to be passed. The definition of the function would now be as follows:

def gothere (count, value=None):

if value:

print (“Got Here:  “,count, ” value is “, value)

else:

print (Got Here:  “, count)

The output this time is

Got Here: 1

Got Here: 2

Got Here: 3 value is 128

Got Here: 4 value is 5

2015 4 5

The assignment within the parameter list gives the name value a special property. It has a default value. If the parameter is not passed, then it takes that value; otherwise it behaves normally. This also means that gothere can be called with one or two parameters, which can be very handy. It is important to note that the parameters that are given a default value must be defined after the ones that are not. That’s because otherwise it would not be clear what was being passed. Consider the (illegal) definition:

def wrong (a=1, b, c=12):

Now call wrong with two parameters:

wrong (2,5)

What parameters are being passed? Are they a and b? Are they a and c? It is impossible to tell. A legal definition would be

def right (b, a=1, c=12)

This function can be called as

right (19)

in which case b=19, a=1, and c=12. It can be called as

right (19, 20)

in which case b=19, a=19, and c=12. It can be called as

right (19, 19, 19)

in which case b=19, a=19, and c=19. But how can it be called passing b and c but not a?

right (19, c=19)

In this case, a has been allowed to default. The only way to pass c without also passing a is to give its name explicitly so that the call is not ambiguous.

4. None

Mistakes happen when writing code. They are unavoidable, and much time is spent getting rid of them. One common kind of mistake is to forget to assign a return value when one is needed. This is especially likely when there are multiple points in the function where a return can occur. In many programming languag­es, this will be caught as an error, but in Python it is not. Instead, a function that is not explicitly assigned a return value will return a special value called None.

None has its own type (NoneType), and is used to indicate something that has no defined value or the absence of a value. It can be explicitly assigned to vari­ables, printed, returned from a function, and tested. Testing for this value can be done using the following:

if x == None:

or with

if x is None:

5. Example: The Game of Sticks

This is a relatively simple combinatorial game that involves removing sticks or chips from a pile. There are two players, and the game begins with a pile of 21 sticks. The first player begins by removing 1, 2, or 3 sticks from the pile. Then the next player removes some sticks, again 1, 2, or 3 of them. Players alternate in this way. The player who removes the last stick wins the game; in other words, if you can’t move, you lose.

Functions are useful in the implementation of this game because both play­ers do similar things. The action connected with making a move, displaying the current position, and so on are the same for the human player and the computer opponent. The current status or state of the game is simply a number, the number of sticks remaining in the pile. When that number is zero, then the game is over, and the loser is whichever player is supposed to move next. The code for a pair of moves, one from the human and one from the computer, might be coded in Python as follows:

displayState(val)                # Show the game board

userMove = getMove()             # Ask user for their move

val = val – userMove             # Make the move

print (“You took “, userMove, ” sticks leaving “, val)

if gameOver(val):

print(“You win!”)

else:

move = makeComputerMove (val) # Calculate the

# computer’s move

print (“Computer took “, move, ” sticks leaving “, val) if gameOver(val):

print(“Computer wins!”)

The current state of the game is displayed first, and then the human player is asked for their move. The move is simply the number of sticks to remove. When the move has been made, if there are no sticks left, then the human wins. Other­wise, the computer calculates and makes a move; again, if no sticks remain then the game is over, in this case the computer being the winner. This entire section of code needs to be repeated until the game is over, of course.

There are four functions that must be written for this version: displayState(), getMove(), gameOver(), and makeComputerMove().

The function displayState() prints the current situation in the game. Specifi­cally, it prints one “O” character for each stick still in the pile, and does so in rows of 6. At the beginning of the game, this function would print the following:

O O O O O O

O O O O O O

O O O O O O

O O O

which is 21 sticks. The code is as follows:

def displayState(val):

k = val       # K represents the number of

              # sticks not printed

while k > 0: # So long as some are not printed …

if k >=6: # If there is a whole row, print it.

print (“O O O O O O ”, end=””) k = k – 6  

# Six fewer sticks are unprinted

else:

for j in range(0,k):   # Print the remainder

print (“O “, end=””) k = 0 # None remain

print (“”)

Note that the function is named for what it does. It does only one thing, it modifies no values outside of the function, and it serves a purpose that is needed multiple times. These are all good properties of a function.

The function getMove() prints a prompt to the user/player asking for the number of sticks they wish to remove and reads that value from the keyboard, returning it as the function value. Again, this function is named for what it does and performs a single, simple task. One possibility for the code is as follows:

def getMove ():

n = int(input (“Your move: Take away how many? “))

while n<=0 or n>3:

print (“Sorry, you must take 1, 2, or 3 sticks.”)

n = int(input (“Your move: Take away how many? “))

return n

The function gameOver() is trivial, but lends structure to the program. All it does is test whether the value of val, the game state variable, is zero. There may be other end-of-game indicators that could be tested here.

def gameOver (state):

if state == 0:

return True

return False

Finally, the most complicated function, getComputerMove(), can be at­tempted. Naturally, a good game presents a challenge to the player, and so the computer should win the game it if can. It should not play randomly if that is possible. In the case of this particular game, the winning strategy is easy to code. The player to make the final move wins, so if there are 1, 2, or 3 sticks left at the end, the computer would take them all and win. Forcing the human player to have 4 sticks makes this happen. The same is true if the computer can give the human player (i.e., leave the game in the state of having 8, 12, or 16 sticks). If the human moves first (as it does in this implementation), the computer tries to leave the game in a state where there are 16, 12, 8, or 4 sticks left after its move. The code could be written as follows:

def getComputerMove (val):

n = val % 4

if n<=0:

return 1

else:

return n

There some of the details needed to finish this game properly are left as an exercise.

6. Scope

A variable that is defined (first used) in the main program is called a global variable and can be accessed by all functions if they ask for it. A variable that is used in a function can be accessed by that function and is not available in the main program. It’s called a local variable. This scheme is called scoping: the locations in a program where a variable can be accessed is called its scope. It’s is easy to understand unless a global variable has the same name as a local one, in which case the question is: “what value is represented by this name?” If a vari­able named “x” is global and a function also declares a variable having the same name, this is called aliasing, and it can be a problem.

In Python, a variable is assumed to be local unless the programmer specifi­cally says it is global. This is done in a statement. For example,

global a, b, c

tells Python that the variables named a, b, and c are global variables, and are defined outside of the function. This means that after the function has completed execution, those variables can still be accessed by the main program and by any other functions that declare them to be global.

Global variables are thought by some programmers to be a bad thing, but in fact they can be quite useful and can assist in the generality of the functions that are a part of the program. A global variable should represent something that is known to the whole program. For instance, if the program is one that plays check­ers or chess, then the board can be global. There is only one board, and it is es­sential to the whole program. The same applies to any program that has a central set of data that many of the functions need to modify.

An example of central data is the game state in a video game. In the Sticks game program, the function getComputerMove() takes a parameter – the game state. There is only one game state, and although for some games it can involve many values, in this case, there is only one value: the number of sticks remaining. The function can be re-written to use the game state variable val as a global in the following way:

def getComputerMove ():

global val

n = val % 4

if n<=0:

return 1

else:

return n

Similarly, the function that determines whether the game is over could use val as a global variable. It would be poor stylistic form to have getMove() use a global variable for the user’s move. The name does imply that the function will get a move, and so that value should be returned as an explicit function return value.

If a variable is named as global, then that name cannot be used in the func­tion as a local variable, as well. It would be impossible to access it, and it would be confusing. It is a common programming error to forget to declare a variable as global. When this happens, the variable is a new one local to the function, and starts out with a value of 0. Thus, no syntax error is detected, but the calculation will almost certainly be incorrect. It is a good idea to identify global variables in their names. For example, place the string “_g” at the end of the names of all global variables. The game state above would be named val_g, for example. This would be a reminder to declare them properly within functions.

Other kinds of data that could be kept globally would include lists of names, environment or configuration variables, complex data structures that represent a single underlying process, and other programming objects that are referred to as singletons in software engineering. In Python, because they have to be explicitly named in a declaration there is a constant reminder of the variable’s scope.

7. Variable Parameter Lists

The print() function is interesting because it seems to be able to accept any number of parameters and deal with them. The statement

print(i)

prints the value of the variable i, and

print (i,j,k)

prints the value of all three variables i, j, and k. Is this some sort of special thing reserved for print() because Python knows about it? No. Any function can do this. Consider a function,

fprint ( “format string”, variable list)

where the format string can contain the characters “f” or “i” in any combination. Each instance of a letter should correspond to a variable passed to the function in the variable list, and it will be printed as a floating point if the corresponding character in the format string is “f” and as an integer if it is “i.” The call

fprint(“fi”, 12, 13)

prints the values 12 and 13 as a float and an integer, respectively. How can this be written as a Python function?

The function starts with the following definition:

def fprint (fstring, *vlist)

The expression *vlist represents a set of positional parameters, any number of them. This is preceded by a specific parameter fstring, which is the format string. A simple test of this would be to just print the variables in the list to see if it works:

def fprint (fstring, *vlist)

for v in vlist:

print v

When called as fprint(“”, 12, 13, 14,15), this prints

12

13

14

15

The list of variables after the * character is turned into a tuple, which is passed as the parameter, so the *vlist counts as a single parameter with many components.

To finish the original function, we have to remove characters from the front of the format string, match them against a variable, and print the result as the format character dictates. We need the same loop as above, but we also need an index for the format string that increases each time through and is used to indi­cate the format. It is also important that the number of format items equals the number of variables:

All of the known positional parameters must come before the variable list; otherwise the end of the variable list cannot be determined. There is a second complication, that being the existence of named parameters. Those are indicated by a parameter such as **nlist. The two * characters indicate a list of named variables.

8. Variables as Functions

Because Python is effectively untyped and variables can represent any kind of thing at all, a variable can be made to refer to a function; not the function name itself, which always refers to a specific function, but a variable that can be made to refer to any function. Consider the following functions, each of which does one trivial thing:

def print0():

print (“Zero”)

def print1():

print (“One”)

def print2():

print (“Two”)

def print3():

print(“Three”)

Now make a variable reference one of these functions by means of an assign­ment statement:

printNum = print1 # Note that there is no parameter

                  # list given

The variable printNum now represents a function, and when invoked, the function it represents will be invoked. So

printNum()

will result in the output

One

Why did the statement printNum = printl not result in the function print1 being called? Because the parameter list was absent. The statement

printNum = print1()

results in a call to print1 at that moment, and the value of the variable printNum is the return value of the function. This is the essential syntactic difference: print1 is a function value, and print1() is a call to the function. To emphasize this point, here is some code that allows the English name of a number between 1 and 3 to be printed:

There are more subtle uses in this case. Consider this use of a list

a = 1

printList = [print0, print1, print2, print3]

printNum = printList[a]

printNum()

that results in the output

One

The final iteration of this is call the function directly from the list:

printList[1]()

This works because printList[1] is a function, and a function call is a function followed by (). This is overly complicated, and so it is rarely used.

For those with an interest or need for mathematics, consider a function that computes the derivative or integral of another function. Passing the function to be differentiated or integrated as a parameter may be the best way to proceed in these cases.

Example: Find the maximum value of a function

Maximizing a function can have important consequences in real life. The function may represent how much money will be made by manufacturing vari­ous objects, how many patients can get through an emergency ward in an hour, or how much food will be grown with particular crops. If the function is easy to use, then there are many mathematically sound ways to find a maximum or minimum value, but if a function is hard to work with, then less analytical methods may have to be used. This problem proposes a search for the best pair of parameters to a problem that could be solved using a method called linear programming.

The problem goes like this:

A calculator company produces a scientific calculator and a graphing calculator. Long-term projections indicate an expected demand of at least 100 scientific and 80 graphing calculators each day. Because of the limitations on the production capacity, no more than 200 scientific and 170 graphing calculators can be made daily. To satisfy a shipping contract, a total of at least 200 calculators much be shipped each day.

If each scientific calculator sold results in a $2 loss, but each graphing calculator produces a $5 profit, how many of each type should be made daily to maximize net profits?

Let s be the number of scientific calculators manufactured and g be the num­ber of graphing calculators. From the problem statement,

100 <= s <= 200

80 <= g <= 170

Also,

s + g > 200, or g > 200 – s

Finally, the profit, which is to be maximized, is as follows:

P = -2s + 5g

First, code the profit as a function:

def profit (s, g):

return -2*s + 5*g

A search through the range of possibilities will run through all possible val­ues of s and all possible values of g; that is, s from 100 to 200 and g from 80 to 170. The function is evaluated at each point and the maximum is remembered:

Finally, the call that does the optimization calls the search function, passing the profit function as a parameter:

c = searchmax (profit, 100, 80, 200, 170, 200)

print (c)

The answer found is the tuple (100, 170), or s=100 and g = 170, which agrees with the correct answer as found by other methods. This is only one example of the value of being able to pass functions as parameters. Most of the code that does this is mathematical, but may accomplish practical tasks like optimizing perfor­mance, drawing graphs and charts, and simulating real world events.

9. Functions as Return Values

Just as any value, including a function, can be stored in a variable, any value, including a function, can be returned by a function. If a function that prints the English name of a number is desired, it could be returned by a function:

def print0():

print (“Zero”)

def print1():

print (“One”)

def print2():

print (“Two”)

def print3():

print(“Three”)

Calling this function and assigning it to a variable means returning a func­tion that can print a numerical value:

printNum = getPrintFun(2) # Assign a function to printNum

and then

printNum() # Call the function represented by printNum

results in the output

Two

The function printFun returns, as a value, the function to be called to print that particular number. Returning the name of the function returns something that can be called.

Why would any of these seeming odd aspects of Python be useful? Allowing a general case, permitting the most liberal interpretation of the language, would permit unanticipated applications, of course. The ability to use a function as a variable value and a return result are a natural consequence of Python having no specific type connected with a variable at compilation time. There are many spe­cific reasons to use functions in this way. Imagine a function that plots a graph.

Being able to pass this function another function to be plotted is surely the most general way to accomplish its task.

 

Source: Parker James R. (2021), Python: An Introduction to Programming, Mercury Learning and Information; Second edition.

Leave a Reply

Your email address will not be published. Required fields are marked *