7. Functions

We have been using functions all along. In fact, the Hello, world! example used the print() function to print to the terminal. Here, we take an in-depth look at how to create functions. Remember, functions are just verbs or actions, they do something. The basic syntax for defining a function is as follows.

def <function_name>(<parameters:optional>):
   # perform logic
   # optional return

The <function_name> is the name of the function, and the <parameters:optional> denote variables that are passed into the function. A function may or may not return data. In the case where something is returned back to the caller, a return command must be used, followed by what is being returned back. The print() function is an example of a function that does not return anything.

Note

Naming things is hard in computer science. Typically, variables should be nouny and functions should be verby. Meaning, variable names should look like nouns and function names should look like verbs.

Note

The values, variables, parameters or arguments that are passed into a function are called inputs while the item being passed back out through the return statement is called the output. Typically, a function can have any number of inputs, but only one output. In the case where multiple outputs are required, such outputs are stored in a single data structure such as a list, set, tuple or dictionary.

7.1. Basic function

Your most basic function could just print Hello, world!. Notice that there are no variables passed to the function (thare are no variables defined inside the parentheses). Also, nothing is returned from this function (there is no return statement).

1def say_hello():
2   print('Hello, world!')

To use this function, we simply type in the name followed by parentheses say_hello(). Functions are a way to group logic that can be reused. If we want to reuse the function, simply call it again. When we call a function, this usage is sometimes referred to as invoking the function. When we say use, call or invoke the function, we mean the same thing.

 1def say_hello():
 2   print('Hello, world!')
 3
 4# call the function above twice
 5say_hello()
 6say_hello()
 7
 8# call the function above 10 times using a loop
 9# note we use the range() function which generates a range of numbers
10# from zero up to (and excluding) the specified number
11for _ in range(10):
12   say_hello()

Now, we want to modify say_hello() to accept an argument name and invoke it 10 times.

 1def say_hello(name):
 2   print(f'Hello, {name}!')
 3
 4# invoke the function once
 5say_hello('Sarah')
 6
 7# invoke the function many times using a loop
 8names = ['John', 'Jack', 'Sam', 'Jeff']
 9
10for name in names:
11   say_hello(name)

What if we want to say hello to a person using their first and last name?

 1def say_hello(first_name, last_name):
 2   print(f'Hello, {first_name} {last_name}!')
 3
 4# invoke the function once
 5say_hello('Sarah', 'Smith')
 6
 7# invoke the function many times using a loop
 8names = [('John', 'Smith'), ('Jack', 'Johnson'), ('Sam', 'Smith'), ('Jeff', 'Johnson')]
 9
10for fname, lname in names:
11   say_hello(fname, lname)

7.1.1. Exercise

Write a function called say_bye() that prints Bye, world!. Call this function once and then call this function 20 times.

Solution.

1def say_bye():
2   print('Bye, world!')
3
4say_bye()
5
6for _ in range(20):
7   say_bye()

7.1.2. Exercise

Write a function to characterize the weather qualitatively based on the temperature (the temperature is a required parameter).

  • if the temperature is less than or equal to 60, print cold

  • else if the temperature is less than or equal to 80, print cool

  • else if the temperature is less than or equal to 90, print warm

  • else print hot

Solution.

 1def describe_weather(temperature):
 2   if temperature <= 60.0:
 3      print(f'{temperature} is cold')
 4   elif temperature <= 80.0:
 5      print(f'{temperature} is cool')
 6   elif temperature <= 90.0:
 7      print(f'{temperature} is warm')
 8   else:
 9      print(f'{temperature} is hot')
10
11# invoke the function once
12describe_weather(77.7)
13
14# invoke the function many times using a loop
15temperatures = [30.2, 77.5, 80.2, 101.1]
16
17for temperature in temperatures:
18   describe_weather(temperature)

7.2. Function with one arguments

Here are two functions add_one() and minus_one(). Each of these functions take in a parameter, argument or variable named num. While add_one() adds one to the passed in integer and returns that result, subtract_one() subtracts one from the passed in integer and returns that result.

 1def add_one(num):
 2    return num + 1
 3
 4
 5def minus_one(num):
 6    return num - 1;
 7
 8
 9x = 10
10
11y = add_one(x)
12z = minus_one(x)

7.2.1. Exercise

Write two functions.

  • times_two(num) should multiply the passed in number by 2 and return the result

  • divide_by_two(num) should divide the passed in number by 2 and return the result

Invoke these functions for a variety of numbers.

Solution.

 1def times_two(num):
 2   return num * 2
 3
 4def divide_by_two(num):
 5   return num / 2
 6
 7numbers = [32, 17, 8, 5, 18]
 8
 9for number in numbers:
10   result_1 = times_two(number)
11   result_2 = divide_by_two(number)
12
13   print(f'results of passing in {number} to functions are {result_1} and {result_2}')

7.2.2. Exercise

Write a function to characterize the weather qualitatively based on the temperature (the temperature is a required parameter).

  • if the temperature is less than or equal to 60, return cold

  • else if the temperature is less than or equal to 80, return cool

  • else if the temperature is less than or equal to 90, return warm

  • else return hot

Convert the list of quantitative temperatures below to one of qualitative values using the new function defined.

1temperatures = [30.2, 77.5, 80.2, 101.1]

Solution.

 1def describe_weather(temperature):
 2   if temperature <= 60.0:
 3      return 'cold'
 4   elif temperature <= 80.0:
 5      return 'cool'
 6   elif temperature <= 90.0:
 7      return 'warm'
 8   else:
 9      return 'hot'
10
11# use a list comprehension
12temperatures = [30.2, 77.5, 80.2, 101.1]
13conditions = [describe_weather(t) for t in temperatures]
14print(conditions)

7.2.3. Exercise

Write a functions to compute compute the area and perimeter of a square e.g. get_area(side) and get_perimeter(side).

  • area = side x side

  • perimeter = side x 4

Compute the areas and perimeters for the following squares (represented by integers) in the list.

1squares = [10, 5, 88, 3, 4, 3]

Solution.

 1def get_area(side):
 2   return side * side
 3
 4def get_perimeter(side):
 5   return side * 4
 6
 7squares = [10, 5, 88, 3, 4, 3]
 8
 9for side in squares:
10   area = get_area(side)
11   perimeter = get_perimeter(side)
12
13   s = f'side={side} | area = {area} | perimeter = {perimeter}'
14   print(s)

7.2.4. Exercise

Write a functions to compute compute the area and circumference of a circle e.g. get_area(radius) and get_circumference(radius).

  • area = 3.14 x radius x radius

  • circumference = 2 x 3.14 x radius

Compute the areas and perimeters for the following circles (represented by integers) in the list.

1circles = [10, 5, 88, 3, 4, 3]

Solution.

 1def get_area(radius):
 2   return 3.14 * radius ** 2
 3
 4def get_circumference(radius):
 5   return 2 * 3.14 * radius
 6
 7circles = [10, 5, 88, 3, 4, 3]
 8
 9for radius in circles:
10   area = get_area(radius)
11   circumference = get_circumference(radius)
12
13   s = f'radius={radius} | area = {area} | circumference = {circumference}'
14   print(s)

7.3. Function with two arguments

Here is a demonstration of a function with two arguments and a return value.

1# function with 2 arguments
2def multiply(a, b):
3    return a * b
4
5
6x = 10
7y = 8
8
9z = multiply(x, y)

7.3.1. Exercise

Create the following functions.

  • add(a, b) should add a and b and return the result

  • minus(a, b) should subtract a from b and return the result

  • times(a, b) should multiply a and b and return the result

  • divide(a, b) should divide b by a and return the result

Invoke these functions for a variety of pairs of numbers and print the results.

1pairs = [(10, 2), (13, 4), (16, 5), (88, 7)]

Solution.

 1def add(a, b):
 2   return a + b
 3
 4def minus(a, b):
 5   return b - a
 6
 7def times(a, b):
 8   return a * b
 9
10def divide(a, b):
11   return b / a
12
13pairs = [(10, 2), (13, 4), (16, 5), (88, 7)]
14
15for a, b in pairs:
16   s = """
17   {a} + {b} = {add(a, b)}
18   {a} - {b} = {minus(a, b)}
19   {a} * {b} = {times(a, b)}
20   {a} / {b} = {divide(a, b)}
21   """.strip()
22
23   print(s)
24   print('')

7.3.2. Exercise

Write a functions to compute compute the area and perimeter of a rectangle e.g. get_area(width, length) and get_perimeter(width, length).

  • area = width x length

  • perimeter = 2 x width + 2 x length

Compute the areas and perimeters for the following rectangles (represented by tuples) in the list.

1rectangles = [(10, 5), (88, 3), (4, 3)]

Solution.

 1def get_area(width, length):
 2   return width * length
 3
 4def get_perimeter(width, length):
 5   return 2 * width + 2 * length
 6
 7rectangles = [(10, 5), (88, 3), (4, 3)]
 8
 9for width, length in rectangles:
10   area = get_area(width, length)
11   perimeter = get_perimeter(width, length)
12
13   s = f'width={width}, length={length} | area = {area} | perimeter = {perimeter}'
14   print(s)

7.3.3. Exercise

Write a functions to compute compute the area of a triangle e.g. get_area(base, height).

  • area = 0.5 x base x height

Compute the areas for the following triangles (represented by tuples) in the list.

1triangles = [(10, 5), (88, 3), (4, 3)]

Solution.

 1def get_area(base, height):
 2   return 0.5 * base * height
 3
 4triangles = [(10, 5), (88, 3), (4, 3)]
 5
 6for base, height in triangles:
 7   area = get_area(base, height)
 8
 9   s = f'base={base}, height={height} | area = {area}'
10   print(s)

7.4. Function with three arguments

Here is an example of a function with 3 arguments as inputs, and the output is the product of the inputs.

 1# function with 3 arguments
 2def multiply(a, b, c):
 3    return a * b * c
 4
 5
 6w = 10
 7x = 8
 8y = 4
 9
10z = multiply(w, x, y)

7.4.1. Exercise

Write a function that takes in 3 integer arguments compute(x, y, z). The function return the result of x + y * z.

Solution.

1def compute(x, y, z):
2   return x + y * z
3
4print(compute(10, 5, 8))

7.5. Function with a list argument

A function can accept anything as input, including other other functions! Here, concat() accepts a list of strings as input and returns the concatenation of the strings in the list using the join() function.

1# function with list argument
2def concat(names):
3    return ','.join(names)
4
5
6first_names = ['John', 'Jack', 'Joe']
7s = concat(first_names)

7.5.1. Exercise

Python has many built-in functions for math operations. The sum() function takes in a collection of numbers and returns the sum across all the values. The len() function takes in a collection of numbers and returns the number of elements in that collection. Write a function get_average(numbers) that returns the expected value of a list of numbers. Remember, the expected value is the sum of the numbers divided by the total number of elements.

1numbers = [5, 18, 29, 787, 2, 3, 88]

Solution.

1def get_average(numbers):
2   total = sum(numbers)
3   n = len(numbers)
4   average = total / n
5   return average
6
7numbers = [5, 18, 29, 787, 2, 3, 88]
8average = get_average(numbers)
9print(f'The average is {average:.5f}')

7.5.2. Exercise

Write a function to compute the sample variance of a list of numbers.

  • the variance \(\sigma^2\) is defined as \(\sigma^2 = \frac{\sum (x_i - \bar x)^2}{n - 1}\)

1numbers = [5, 18, 29, 787, 2, 3, 88]

Solution.

 1def get_average(numbers):
 2   total = sum(numbers)
 3   n = len(numbers)
 4   average = total / n
 5   return average
 6
 7def get_variance(numbers):
 8   avg = get_average(numbers)
 9   n = len(numbers)
10   variance = sum([(n - avg)**2 for n in numbers]) / (n - 1)
11   return variance
12
13numbers = [5, 18, 29, 787, 2, 3, 88]
14variance = get_variance(numbers)
15print(f'The variance is {variance:.5f}')

7.6. Function with default value argument

The inputs required by a function can be set to default values. Note that if an input has a default value, it must always come last! Below, we pass in a list of strings and want to join them. The default delimiter is a comma, but the user can choose to override that delimiter when they invoke the function.

 1# function with list argument
 2def concat(names, separator=','):
 3    return separator.join(names)
 4
 5
 6first_names = ['John', 'Jack', 'Joe']
 7
 8s = concat(first_names)
 9print(s)
10
11s = concat(first_names, separator='|')
12print(s)

7.6.1. Exercise

Write a function that takes in two integer a and b as well as a math operation (+, -, *, /). The math operation should be set to + by default. Depending on the math operation, apply such operation to a and b and return the result.

1a = 14
2b = 8

Solution.

 1def do_operation(a, b, op='+'):
 2   if '+' == op:
 3      return a + b
 4   elif '-' == op:
 5      return a - b
 6   elif '*' == op:
 7      return a * b
 8   else:
 9      return a / b
10
11a = 14
12b = 8
13
14ops = ['+', '-', '*', '/']
15
16for op in ops:
17   result = do_operation(a, b)
18   s = f'{a} {op} {b} = {result}'
19   print(s)

7.7. Non-keyworded, variable-length argument

If you require any number of inputs to a function, but do not know what these inputs will look like or how many of them there will be, you can define the inputs as a non-keyworded, variable-length argument. Such argument is conventionally defined as *args and must always come after all keyworded arguments are defined.

 1# non-keyworded, variable-length argument
 2def do_print(*args):
 3    for arg in args:
 4        print(arg)
 5
 6
 7def do_special_print(symbol, *args):
 8    for arg in args:
 9        print(symbol, arg)
10
11
12names = ['John', 'Jack', 'Joe']
13
14# use *, the unpacking operator
15# * returns an iterable that is a tuple
16do_print(*names)
17
18do_special_print('*', *names)

Note

You can pretty much achieve the same effect of a non-keyworded, variable-length argument with an argument that is a list. What do you think is the difference between a function defined as do_it(*args) vs do_it(args=[])?

7.7.1. Exercise

Write a function to calculate the price of a pizza based on the toppings added to the pizza. The pizza itself cost $10.50. The topping costs are as follows.

  • mushroom: $3.50

  • suasage: $4.40

  • olives: $1.25

  • pepperoni: $2.00

  • cheese: $1.25

  • anchovy: $3.25

The function should specify a non-keyworded, variable-length argument to represent the toppings.

Solution.

 1def compute_cost(*toppings):
 2   prices = {
 3      'mushroom': 3.50,
 4      'suasage': 4.40,
 5      'olives': 1.25,
 6      'pepperoni': 2.00,
 7      'cheese': 1.25,
 8      'anchovy': 3.25
 9   }
10   cost = sum([prices[topping] for topping in toppings if topping in prices])
11   cost += 10.50
12   return cost
13
14print(compute_cost(*['mushroom', 'suasage']))
15print(compute_cost(*['olives', 'pepperoni']))
16print(compute_cost(*['cheese', 'pepperoni']))
17print(compute_cost(*['mushroom', 'suasage', 'olives', 'pepperoni', 'cheese', 'anchovy']))

7.8. Keyworded, variable-length argument

In the case when you require any number of inputs to a function, but want the caller of the function to supply keywords associated with each of the inputs, you can define the inputs as a keyworded, variable-length argument. Such argument is conventionally defined as **kwargs and must come as the very last argument when defining a function.

 1# keyworded, variable-length argument
 2def do_print(**kwargs):
 3    for key, val in kwargs.items():
 4        print(key, val)
 5
 6
 7def do_special_print(symbol, **kwargs):
 8    for key, val in kwargs.items():
 9        print(symbol, key, val)
10
11
12# use a dictionary
13# use ** unpacking operating
14data = {
15    'name': 'John Doe',
16    'age': 18
17}
18
19do_print(**data)
20
21# inline keyworded argument
22do_print(name='John Doe', age=18)
23
24do_special_print('*', **data)
25
26do_special_print('*', name='John Doe', age='18')

Note

You can also pretty much achieve the same effect of a keyworded, variable-length argument with an argument that is a dictionary. What do you think is the difference between a function defined as do_it(**kwargs) vs do_it(kwargs=dict())?

7.8.1. Exercise

We have students who are left- or right-handed as shown below. Write a function to get the average age of the left- and right-handed individuals. Return the results. Make sure you use a keyworded, variable-length argument function.

 1students = {
 2   'john': {'hand': 'left', 'age': 17},
 3   'jack': {'hand': 'right', 'age': 18},
 4   'jane': {'hand': 'left', 'age': 14},
 5   'mary': {'hand': 'right', 'age': 13},
 6   'joe': {'hand': 'left', 'age': 17},
 7   'james': {'hand': 'right', 'age': 14},
 8   'nancy': {'hand': 'left', 'age': 12},
 9   'norah': {'hand': 'right', 'age': 19},
10}

Solution.

 1def get_average_ages(**persons):
 2   left_hand_ages = [data['age'] for _, data in persons.items() if data['hand'] == 'left']
 3   right_hand_ages = [data['age'] for _, data in persons.items() if data['hand'] == 'right']
 4
 5   left_avg_age = sum(left_hand_ages) / len(left_hand_ages)
 6   right_avg_age = sum(right_hand_ages) / len(right_hand_ages)
 7
 8   return {
 9      'left': left_avg_age,
10      'right': right_avg_age
11   }
12
13students = {
14   'john': {'hand': 'left', 'age': 17},
15   'jack': {'hand': 'right', 'age': 18},
16   'jane': {'hand': 'left', 'age': 14},
17   'mary': {'hand': 'right', 'age': 13},
18   'joe': {'hand': 'left', 'age': 17},
19   'james': {'hand': 'right', 'age': 14},
20   'nancy': {'hand': 'left', 'age': 12},
21   'norah': {'hand': 'right', 'age': 19},
22}
23
24results = get_average_ages(**students)
25print(results)

7.9. Mixed arguments

Here’s an example of a function with mixed argument/input types.

 1# mixed arguments: standard + *args + **kwargs
 2def do_print(a, b, *args, **kwargs):
 3    print(f'standard args: {a}, {b}')
 4
 5    for arg in args:
 6        print(f'*args: {arg}')
 7
 8    for key, val in kwargs.items():
 9        print(f'**kwargs: {key} => {val}')
10
11
12do_print(1, 2, *[3, 4, 5], **{'name': 'John Doe', 'age': 18})

7.10. Required keyword arguments

You can enforce keyword arguments by using an asterisk before the arguments you want to force as keyword arguments.

1def print_name(*, first_name, last_name):
2   print(f'{first_name} {last_name}')
3
4# print_name('john', 'doe') will not work
5print_name(first_name='john', last_name='doe')

7.11. Unpacking tuple return type

We have already seen how to unpack tuples, here, we show how we can unpack a tuple returned from a function.

 1def get_names():
 2    return 'John', 'Joe', 'Jack'
 3
 4
 5# bad way of acquiring tuple elements
 6t = get_names()
 7name1 = t[0]
 8name2 = t[1]
 9name3 = t[2]
10
11# better way of acquiring tuple elements
12name1, name2, name3 = get_names()

7.12. Lambda

A lambda is a function, however, lambda functions are strictly defined as one line logic or one-liners. Take the say_hello() function below. This function can have multiple lines of statement, although, in this case, it only has one statement which is to print hello.

1def say_hello():
2   print('hello')
3
4# call the function
5say_hello()

Now, look at this function re-defined as a lambda.

1say_hello = lambda: print('hello')
2
3# call the lambda
4say_hello()

It seems lambdas are suited for very simple logic (one-liners), and if you have complicated logic, you are better off using a function. Lambdas can also take in parameters/arguments.

 1# lambda with 1 argument
 2say_hello = lambda name: print(f'hello, {name}')
 3
 4# call lambda
 5say_hello('John')
 6say_hello('Mary')
 7
 8# lambda with 2 arguments
 9say_bye = lambda fname, lname: print(f'bye, {fname} {lname}')
10
11# call lambda
12say_bye('John', 'Doe')
13say_bye('Mary', 'Smith')

Lambdas can return values.

 1# define lambdas
 2add_one = lambda x: x + 1
 3minus_one = lambda x: x - 1
 4times_two = lambda x: x * 2
 5divide_half = lambda x: x / 2
 6
 7# call lambdas one at a time
 8print(add_one(1)) # should print 2
 9print(minus_one(1)) # should print 0
10print(times_two(1)) # should print 2
11print(divide_half(1)) # should print 0.5
12
13# compose operations using lambdas
14# should print 50
15# 50 / 2 = 25
16# 25 * 2 = 50
17# 50 - 1 = 49
18# 49 + 1 = 50
19print(add_one(minus_one(times_two(divide_half(50)))))

Functions and lambdas can be passed as arguments themselves. Below, we have a generic math lambda called do_math() which takes in two numbers a and b as well as a function/lambda as op.

 1# define lambdas
 2add = lambda a, b: a + b
 3subtract = lambda a, b: a - b
 4multiply = lambda a, b: a * b
 5divide = lambda a, b: a / b
 6
 7# this lambda takes in 3 arguments
 8# a and b are integers
 9# op is a function or lambda and will be applied to a and b
10do_math = lambda a, b, op: op(a, b)
11
12# call do_math and change the op
13a, b = 12, 7
14print(do_math(a, b, add))
15print(do_math(a, b, subtract))
16print(do_math(a, b, multiply))
17print(do_math(a, b, divide))
18
19# note here, we inline a lambda
20print(do_math(a, b, lambda a, b: a ** b))

7.12.1. Exercise

If we have the following list of first and last names of people, transform that list to a new one where each element in the new list is the initials of the corresponding person. Try using lambdas to solve this problem.

1names = [('John', 'Doe'), ('Jane', 'Smith')]

Solution.

1get_initial = lambda name: f'{name.capitalize()[0]}'
2get_initials = lambda fname, lname: f'{get_initial(fname)}.{get_initial(lname)}.'
3
4names = [('John', 'Doe'), ('Jane', 'Smith')]
5initials = [get_initials(fname, lname) for fname, lname in names]
6
7print(initials)

7.12.2. Exercise

If you have the list of numbers below, create a new list from this one by including only

  • those numbers that start with a 1 and

  • have at least 3 digits

and transform those numbers by dividing by 2 and then raising the result to the power of 2.

1numbers = [101, 100, 1003, 10002, 202, 13, 1234]

Solution.

1transform = lambda x: (x / 2.0) ** 2.0
2has_three_or_more_digits = lambda x: True if len(str(x)) >= 3 else False
3starts_with_one = lambda x: str(x).startswith('1')
4is_valid = lambda x: has_three_or_more_digits(x) and starts_with_one(x)
5
6numbers = [101, 100, 1003, 10002, 202, 13, 1234]
7nums = [transform(n) for n in numbers if is_valid(n)]
8
9print(nums)

7.13. Generators

Generator functions are those that returns a stream of data back to the caller. The caller is able to treat the output of a generator function like an iterable collection. Here’s a simple example of a generator function that generates random colors in the set of red, green and blue. Note that we have to use yield to return the color.

 1from random import choice
 2
 3def get_colors():
 4   n = 0
 5   while n < 100:
 6      yield choice(['red', 'green', 'blue'])
 7      n += 1
 8
 9# get_colors() returns something
10colors = get_colors()
11
12# what do we get when we print colors?
13print(colors)
14
15# what is the type of colors?
16print(type(colors))
17
18# we can iterate through colors
19for c in colors:
20   print(c)
21
22# we can convert the outputs of a generator to a list
23colors = list(get_colors())
24print(colors)

7.13.1. Exercise

Write a generator function that produces 50 random numbers in the range [0, 10]. Try using this generator function.

Solution.

 1from random import randint
 2
 3def get_numbers():
 4   n = 0
 5   while n < 50:
 6      n += 1
 7      yield randint(0, 10)
 8
 9
10numbers = list(get_numbers())
11print(numbers)