CMSC 14100 — Lecture 10

The return statement

Recall that a function can produce a value. This value is specified by the return statement. When the return statement is executed, the function call evaluates to the value specified by the return statement and the control flow leaves the function.

It is possible for a function to have multiple return statements in different parts of the function body. For a simple example, consider a function that produces the absolute value of a given number.


def absolute(x):
    if x < 0:
        return -x
    else:
        return x
    

Many, many years ago, it was sometimes considered a bad idea to have multiple return statements. The reasoning was that having multiple points at which your function could exit made it harder to read and reason about your program.

One possible reason for this was that the standard terminal size was 80 by 24, so you could only see 24 lines of code on your screen at a time. If you had a particularly long function, it was difficult to have a holistic view of the entire thing and would make early exits easier to miss. Nowadays, we can display much more than 24 lines, so that isn't as much of an issue.

(Notice that this is where the 80 character line limit comes from. You might ask why we still maintain that particular historical limit, even though monitors are much, much wider nowadays. There are two reasons for this. First, and most importantly, 80 characters is at just under the upper limit for readability of texts in general. Secondly, having a line limit discourages excessive indenting and thereby limits the amount of nested code—a common signal that you should consider breaking up a function is when you end up many indentation levels deep.)

We can rewrite is_prime with multiple return statements so that it reads a bit more naturally.


def is_prime(n):
    """
    Determines whether the given integer is a prime number. A prime 
    number is an integer n greater than 1 that has only 1 and n as its 
    divisors.

    Input:
        n (int): the integer to be tested

    Output: True if n is a prime number and False otherwise (bool)
    """
    if n == 1:
        return False
    else:
        for i in range(2,n):
            if n % i == 0:
                return False
        return True
    

Notice that instead of having to break out of the loop, we can simply execute a return statement, finish execution of the function, and produce the desired value.

Now, suppose we might want to return multiple values. At first, this seems like it breaks the rules about what a function is. We'll see shortly that this is not the case. But let's consider the following problem: given a list, we would like to find both the smallest and largest values in the list.


def minmax(lst):
    """
    Given a list of numbers, produces the smallest and largest elements 
    in the list.

    Input:
        lst (list[float]): A list of (real) numbers

    Output: the minimum and maximum numbers in lst (tuple[float,float])
    """
    return min(lst), max(lst)
    

The actual return statement appears to be producing two values. In reality, Python will just do tuple packing on it and the function will produce one value: a tuple. And because our function gives us a tuple, we can then use tuple unpacking on the result.


    min_val, max_val = minmax([2,4,3,4,3,1,5])
    

Parameters

Parameters are the formal name for the variables that are assigned to things that get passed to the function when the function is call. The specific values that parameters get bound to are called arguments.

Recall that when a function call occurs, the expressions that the function is called with are evaluated into values. These values get assigned to the parameters for use in the body of the function. In effect, the values get copied when they are passed to a function. (Recall however, that the specifics of "values" means we're really dealing with references)

Let's consider the following problem: we would like to generate $n$ random integers. If we try to come up with examples, we run into some questions: which integers are we considering? Is there a range? If so, what should our inputs to the function be?

If we decide that we should specify a range, then we can describe our function more carefully.


def gen_randint(n, low, high):
    """
    Given integers n, low, and high, produces a list of n random 
    integers between low and high.

    Input:
        n (int): number of integers to generate, should be positive
        low (int): lower bound
        high (int): upper bound, should be greater than low

    Output: list of n integers between low and high (list[int])
    """
    lst = []
    for i in range(n):
        lst.append(random.randint(low, high))
    return lst
    

However, suppose we realize that most of the times we call this function, we really want to generate numbers between 0 and 100. One neat feature of Python is the ability to set default values for parameters.


def gen_randint(n, low=0, high=100):
    """
    Given integers n, low, and high, produces a list of n random 
    integers between low and high.

    Input:
        n (int): number of integers to generate, should be positive
        low (int): lower bound (default: 0)
        high (int): upper bound, greater than low (default: 100)

    Output: list of n integers between low and high (list[int])
    """
    lst = []
    for i in range(n):
        lst.append(random.randint(low, high))
    return lst
    

Parameters with default values become optional. This adds a complication. Ordinarily, arguments are provided to a function call in order. But suppose we want to use a different upper bound. Does this mean we are also forced to use a lower bound?

The way to get around this is to include the name of the optional parameter.


    gen_randint(20, high=2000)
    

We formally split up arguments into two types:

Because positional arguments are determined based on their position, optional parameters must be placed after those parameters that are not optional.