Return values

  • Some of the functions we have used produce, or return, a value, but
  • Most of the functions we have written are _void__, meaning they don’t return any value
  • We are going to now focus on writing functions that return something
  • The book refers to these as fruitful functions
  • A return statement ends the function immediately and gives the value of the expression following it to the caller
  • The expression can be complicated, or a simple variable holding a value
  • A variable is preferred as it simplifies debugging
  • Just because a function returns a value doesn't mean that the caller has to use or store it- Also, you should only have one return statement in a function (despite what the book says)
  • Multiple return statements can complicate debugging code
  • Consider the code below

In [1]:
def do_something_multiple( x ):
    if( x < 0 ):
        # Do something complicated
        return -1
    elif( x > 10 ):
        # Do something complicated
        return 1
    else:
        return 0

def do_something_single( x ):
    # Create a value to hold the result
    result = 0
    
    if( x < 0 ):
        # Do something complicated
        result = -1
    elif( x > 10 ):
        # Do something complicated
        result = 1
    else:
        result = 0

    # Return the result
    return result


  File "<ipython-input-1-ca8f0e497ebe>", line 5
    elsif( x > 10 ):
                    ^
SyntaxError: invalid syntax
  • While both functions do the same thing, the second is easier to debug
  • The first function has three (3) potential paths to the function, meaning there are three paths that need to be checked in case of an error
  • The second function has only one way in and out of the function, meaning it is easier to track down an error

Incremental development

  • As you write more and longer functions, you will find yourself debugging more often
  • As the complexity increases, so does the probability of errors and the need for debugging
  • Incremental development is a way of dealing with this complexity and reducing the chance of errrors
  • The goal is to only add small portions of code at a time and test them before moving on
  • The key aspects of the process are:
    1. Start with a working program and make small, incremental changes. At any point, if there is an error, you should have a good idea as to where it is.
    2. Use temporary variables to hold intermediate values so you can display and check them.
    3. Once the program is working, remove some of the scaffolding or consolidate multiple statements into compound expressions, but only if it doesn't make the program hard to read.
  • Displaying the algorithm's current progress is a simple form of logging
  • Adding print statements for crucial values as you write code is a form of scaffolding
  • This type of code helps you write the software, but isn't useful in the final version
  • TODO Work hypotenuse example from the textbook (Ex. 6.2)

Composition

  • Building larger components from smaller components is called composition
  • Here, we see it as calling functions from within another function
  • We did this with the area of a donut function discussed in a previous chapter
  • Although the book says that we can remove the temporary variable radius and result in its circle_area function once we are done with development, I disagree
  • It makes the code much more readable and easier to debug/refactor in the future
  • The idea of composition is fundamental in programming and we will use it consistently

Boolean functions

  • Functions can also return boolean values
  • Again, don't follow the boks example by having more than one return statement in a function
  • Boolean return values are common for functions that ask True/False or yes/no questions
  • Try to think of some questions of this type you may want to ask
  • These types of functions are often used in conditional statemetns
  • When you do this, don't test the result against True or False
  • It is unnecessary and an opportunity to insert a logic error

In [2]:
def is_between( x, y, z ):
    return (x <= y) and (y <= z)

print( is_between( 1, 2, 3) )
print( is_between( 1, 0, 3) )

# Do this
if( is_between( 1, 2, 3 ) ):
    print( 'It is between the values' )

# Don't do this
if( is_between( 1, 2, 3 ) == True ):
    print( 'It is between the values' )


True
False
It is between the values
It is between the values

More recursion

  • We have only covered a small portion of Python, but we have covered enough to implement any general algorithm
  • We are still missing things for input and output, but that’s it
  • To illustrate this point, the book develops a recursive function to compute factorials
  • Since we have covered additional recursive functions beyond countdown, we can move on

Leap of faith

  • Following the flow of execution is one way to read a program
  • However, that can quickly become tiresome
  • Another way is to skip over functions and assume they worked correctly
  • The author refers to this as a leap of faith
  • It could also be referred to as the happy path
  • You do this instinctively when you read code with a print or input functions
  • This can be especially helpful when you are writing a recursive function

Checking types

  • When we encountered the problem of floats and negative values as arguments to the countdown and fibonacci functions, we had two choices
    1. Generalize the function to work with the alternative data
    2. Check the parameters to ensure they are correct
  • Sometimes the first option will work, but
  • Most often the second is better
  • This is especially true for functions that have specific requirements regarding the validity of the arguments
  • Code that handles the check is referred to as the guardian
  • It protects the rest of the code from values that might cause an error
  • What are some real-world examples?

Debugging

  • Divide and conquer gives us many opportunities for debugging
  • We have seen this as we build smaller, helper functions and test them before building larger functions
  • If a function isn’t working, there are three possibilities to consider: – There is something wrong with the arguments passed (a precondition is violated) – There is something wrong with the function itself (a postcondition is violated) – There is something wrong with the return value or the way it is used in client code
  • Using careful detective work, you can determine where the error lies
  • The first possibility can be tested by logging the function’s parameter values
  • The second possibility can be tested by logging out the state of the system as the function does its work
  • Also, call the function with arguments for which the correct output is known and testable
  • The third possibility can be tested by looking at how the function is being used and called

Exercises

  • Write a function that computes the sum of the numbers from 1 to n using both an iterative and a recursive approach. Which was easier? Why?

In [3]:
def sum_numbers_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def sum_numbers_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0
  • Write a function that implements multiplication using only addition using both an iterative and a recursive approach. Which was easier? Why?

In [4]:
def multiply_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def multiply_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0
  • Write a function that checks to see if a number n is a prime number using both an iterative and a recursive approach. Which was easier? Why?

In [5]:
def is_prime_iterative( n ):
    # INSERT YOUR CODE HERE
    return 0

def is_prime_recursive( n ):
    # INSERT YOUR CODE HERE
    return 0