NB: Functions Calling Functions

Purpose: * Illustrate concept of function design * Demonstrate how functions can break down a process into simple components * Demonstrate how component functions build on each other * Introduce idea of functional groups * Motivate use of classes (to be introduced later)

Basic Insight

Functions contain any code, so they can contain functions. * Functions can call other functions * Functions can define new functions

We create functions that call functions in order to break a complex process into components. * Some functions focus on simple component processes * Other functions combine these into higher order processes * Some functions may be focused on computation, while others may be focused on interacting with users or data sources * We can think of this a division of labor, or “separation of concerns,” among functions

When you create groups of functions, they often form natural groups that associated with a common process or task. * These function groups often share variables in addition to calling each ohter

Let’s look at some examples to illustrate these points.

Example 1: Converting Temperatures

Here are three functions that work together to make a temperature converter.

Notice how the last function integrates the first two.

def f_to_c(temp):  
    """
    Converts F to C and returns a rounded result.
    Expects an integer and returns an integer.
    """
    return round((temp - 32) * (5/9))
    
def c_to_f(temp):  
    """
    Converts C to F and returns a rounded result.
    Expects an integer and returns an integer.
    """
    return round(temp * (9/5) + 32)
    
def convert(temp, scale): 
    """
    Combines conversion functions into a two-way converter.
    Expects a souce temp (int) and a target scale ('f' or 'c').
    """
    if scale.lower() == "c":
        return f_to_c(temp)  # function call to f_to_c
    else:
        return c_to_f(temp)  # function call to c_to_f

Now, here is function that combines the above functions into a user-facing interface to the other functions.

##| tags: []
def convert_app():
    """
    Provides a user-interface to the the conversion functions.
    """
    
    # Get user input
    temp = int(input("Enter a temperature: "))                
    scale = input("Enter the scale to convert to: (c or f) ")[0].lower()
    
    # Infer source scale, to be used in the output message
    if scale == 'c':
        current_scale = 'f'
    else:
        current_scale = 'c'
    
    # Do the conversion
    converted = convert(temp, scale)
    
    # Print results for user
    print(f"{temp}{current_scale.upper()} is equal to {converted}{scale.upper()}.")
convert_app()
Enter a temperature:  45
Enter the scale to convert to: (c or f)  f
45C is equal to 113F.

A More Pythonic Solution

We replace if/then statements with dictionary logic.

## Put your logic in the data structure
converters = {
    'c': lambda t: (t - 32) * (5/9),
    'f': lambda t: t * (9/5) + 32
}
def convert_app2():
    
    # Input from user
    source_temp  = int(input("Enter a temperature: "))                
    target_scale = input("Enter the scale to convert to: (c or f) ")
    
    # Internal computations
    target_temp  = converters[target_scale](source_temp)
    # source_scale = list(set(converters.keys()) - set(target_scale))[0]
    source_scale = (set(converters.keys()) - set(target_scale)).pop()
    
    # Output to user
    print(source_temp, source_scale, "converted becomes:" , round(target_temp), target_scale)
convert_app2()

Example 2: Counting Vowels

## Predicate functions - often used as helper functions that return True or False

def is_vowel(l):
    if l == "a" or l == "e" or l == "i" or l == "o" or l == "u":
        return True  # if the letter is a vowel, return True
    else:
        return False # else, return False
        
def num_vowels(my_string):
    my_string = my_string.lower()
    count = 0
    for i in range(len(my_string)): # for each character
        if is_vowel(my_string[i]):  # call function above
            count += 1              # increment count if true
    return count
    
def vcounter():
    my_str = input("Enter a string: ")
    vcount = num_vowels(my_str)
    print(f"There are {vcount} vowels in the string.")
vcounter()

A More Pythonic Solution

We can use a lambda function with a comprehension to replace the fisrt two functions above.

vowel_count = lambda x: len([char for char in x.lower() if char in "aeiou"])
test_str = "Whatever it is, it is what it is."
vowel_count(test_str)

Example 3: Calculating Tax

We write two related functions: * One to compute the tax based on a gross pay and a tax rate. * One to compute the net pay using the previous function.

In addition, we want to write some functions that use these functions to interact with a user. * One to get the input value of the gross pay and print the tax. * One to print the net pay based on the previous function.

Note the division of labor, or “separation of concerns”, in these functions: * Some do calculative work * Some do interactive work

To compute tax, we have these data:

gross_pay    tax_rate
---------------------
0   - 240    0%
241 - 480    15%
481 - more   28% 
This time, we want to create a group of functions that expect some global variables to exist and use these instead of return statements.
In the code below, we globalize any variables that are assigned in our functions.
This allows them to be shared by all the other functions.
Note that this is effective when our global environment – the containing script – contains only these functions.
Later in this course, we will look at mechanisms to segment our code in this way.
def compute_tax():
    """
    Computes tax rate and applies to gross pay to get tax.
    Expects gross_apy to be defined globally.
    Adds tax_rate and tax to globals for use by other functins.
    """

    global tax_rate, tax

    # Get rate by lower bound
    if gross_pay > 480:
        tax_rate = .28
    elif gross_pay > 240:
        tax_rate = .15
    else:
        tax_rate = 0
        
    tax = gross_pay * tax_rate
            
def compute_net_pay():
    """
    Computes net pay based on globals produced by compute_tax().
    Expects gross_pay and tax to be defined globally.
    Adds net_pay to to globals.
    """
    
    global net_pay
    
    net_pay = gross_pay - tax

def get_tax():   
    """
    Computes and prints tax based on user input.
    Essentially a wrapper around compute_tax().
    Adds gross_pay to globals.
    """
    
    global gross_pay
    
    gross_pay = int(input("Enter your gross pay in dollars: "))                            
    
    compute_tax()
    
    print(f"Based on a tax rate of {round(tax_rate * 100)}%, the tax you owe on ${gross_pay} is ${round(tax)}.")
    
def get_net_pay():
    """
    Computes and prints net pay based on globals.
    """
    
    compute_net_pay()
    
    print(f"Your take home (net) pay is ${round(net_pay)}.")
    
def do_all():
    "Runs both user-facing functions."
    get_tax()
    get_net_pay()
get_tax()
get_net_pay()
do_all()

Concluding Observations

  • Notice how each example has functions that build on each other.
  • These functions share both data and a general goal.
  • The fact that data and functions go together is the motivation for creating classes.