- Using and defining functions
- Implementing mathematical functions
- Using functions to organize code
- Passing arguments and returning values
- Example: superposition of sound waves
- Q&A
- Exercises
- Creative Exercises
Passing arguments and returning values
Next, we examine the specifics of Python’s mechanisms for passing arguments to and returning values from functions. These mechanisms are conceptually very simple, but it is worthwhile to take the time to understand them fully, as the effects are actually profound. Understanding argument-passing and return-value mechanisms is key to learning any new programming language. In the case of Python, the concepts of immutability and aliasing play a central role.
Call by object reference
You can use parameter variables anywhere in the body of the function in the same way as you use local variables. The only difference between a parameter variable and a local variable is that Python initializes the parameter variable with the corresponding argument provided by the calling code. We refer to this approach as call by object reference. (It is more commonly known as call by value, where the value is always an object reference—not the object’s value.) One consequence of this approach is that if a parameter variable refers to a mutable object and you change that object’s value within a function, then this also changes the object’s value in the calling code (because it is the same object). Next, we explore the ramifications of this approach.
Immutability and aliasing
As discussed in SECTION 1.4, arrays are mutable data types, because we can change array elements. By contrast, a data type is immutable if it is not possible to change the value of an object of that type. The other data types that we have been using (int, float, str, and bool) are all immutable. In an immutable data type, operations that might seem to change a value actually result in the creation of a new object, as illustrated in the simple example at right. First, the statement i = 99 creates an integer 99, and assigns to i a reference to that integer. Then j = i assigns i (an object reference) to j, so both i and j reference the same object—the integer 99. Two variables that reference the same objects are said to be aliases. Next, j += 1 results in j referencing an object with value 100, but it does not do so by changing the value of the existing integer from 99 to 100! Indeed, since int objects are immutable, no statement can change the value of that existing integer. Instead, that statement creates a new integer 1, adds it to the integer 99 to create another new integer 100, and assigns to j a reference to that integer. But i still references the original 99. Note that the new integer 1 has no reference to it in the end—that is the system’s concern, not ours. The immutability of integers, floats, strings, and booleans is a fundamental aspect of Python. We will consider the advantages and disadvantages of this approach in more detail in SECTION 3.3.
Integers, floats, booleans, and strings as arguments
The key point to remember about passing arguments to functions in Python is that whenever you pass arguments to a function, the arguments and the function’s parameter variables become aliases. In practice, this is the predominant use of aliasing in Python, and it is important to understand its effects. For purposes of illustration, suppose that we need a function that increments an integer (our discussion applies to any more complicated function as well). A programmer new to Python might try this definition:
def inc(j): j += 1
and then expect to increment an integer i with the call inc(i). Code like this would work in some programming languages, but it has no effect in Python, as shown in the figure at right. First, the statement i = 99 assigns to global variable i a reference to the integer 99. Then, the statement inc(i) passes i, an object reference, to the inc() function. That object reference is assigned to the parameter variable j. At this point i and j are aliases. As before, the inc() function’s j += 1 statement does not change the integer 99, but rather creates a new integer 100 and assigns a reference to that integer to j. But when the inc() function returns to its caller, its parameter variable j goes out of scope, and the variable i still references the integer 99.
This example illustrates that, in Python, a function cannot produce the side effect of changing the value of an integer object (nothing can do so). To increment variable i, we could use the definition
def inc(j): j += 1 return j
and call the function with the assignment statement i = inc(i).
The same holds true for any immutable type. A function cannot change the value of an integer, a float, a boolean, or a string.
Arrays as arguments
When a function takes an array as an argument, it implements a function that operates on an arbitrary number of objects. For example, the following function computes the mean (average) of an array of floats or integers:
def mean(a): total = 0.0 for v in a: total += v return total / len(a)
We have been using arrays as arguments from the beginning of the book. For example, by convention, Python collects the strings that you type after the program name in the python command into an array sys.argv[] and implicitly calls your global code with that array of strings as the argument.
Side effects with arrays
Since arrays are mutable, it is often the case that the purpose of a function that takes an array as argument is to produce a side effect (such as changing the order of array elements). A prototypical example of such a function is one that exchanges the elements at two given indices in a given array. We can adapt the code that we examined at the beginning of SECTION 1.4:
def exchange(a, i, j): temp = a[i] a[i] = a[j] a[j] = temp
This implementation stems naturally from the Python array representation. The first parameter variable in exchange() is a reference to the array, not to all of the array’s elements: when you pass an array as an argument to a function, you are giving it the opportunity to operate on that array (not a copy of it). A formal trace of a call on this function is shown on the facing page. This diagram is worthy of careful study to check your understanding of Python’s function-call mechanism.
A second prototypical example of a function that takes an array argument and produces side effects is one that randomly shuffles the elements in the array, using this version of the algorithm that we examined in SECTION 1.4 (and the exchange() function just defined):
def shuffle(a): n = len(a) for i in range(n): r = random.randrange(i, n) exchange(a, i, r)
Incidentally, Python’s standard function random.shuffle() does the same task. As another example, we will consider in SECTION 4.2 functions that sort an array (rearrange its elements so that they are in order).
Arrays as return values
A function that sorts, shuffles, or otherwise modifies an array taken as argument does not have to return a reference to that array, because it is changing the contents of a client array, not a copy. But there are many situations where it is useful for a function to provide an array as a return value. Chief among these are functions that create arrays for the purpose of returning multiple objects of the same type to a client.
As an example, consider the following function, which returns an array of random floats:
def randomarray(n): a = stdarray.create1D(n) for i in range(n): a[i] = random.random() return a
Later in this chapter, we will be developing numerous functions that return huge amounts of data in this way.
THE TABLE BELOW CONCLUDES OUR DISCUSSION of arrays as function arguments by highlighting some typical array-procession functions.
mean of an array |
def mean(a): total = 0.0 for v in a: total += v return total / len(a) |
dot product of two vectors of the same length |
def dot(a, b): total = 0 for i in range(len(a)): total += a[i] * b[i] return total |
exchange two elements in an array |
def exchange(a, i, j): temp = a[i] a[i] = a[j] a[j] = temp |
write a one-dimensional array (and its length) |
def write1D(a): stdio.writeln(len(a)) for v in a: stdio.writeln(v) |
read a two-dimensional array of floats (with dimensions) |
def readFloat2D(): m = stdio.readInt() n = stdio.readInt() a = stdarray.create2D(m, n, 0.0) for i in range(m): for j in range(n): a[i][j] = stdio.readFloat() return a |
Typical code for implementing functions with arrays |