Table of Contents



Introduction

A function is a block of code that performs a specific task and only runs when it is called. Functions help break our program into smaller modular chunks. As our program grows larger with time, functions make it more organized and manageable. More importantly, a function is called only when there is a need and thus avoids repetition of the same chunk of code. When called, a function may take in certain data known as parameters and output certain results.

Defining Functions

The following is the syntax of a typical function.

Syntax

Defining a Function.

1def function_name(parameters):
2	statement(s)
3    return value1, value2, ...

The keyword def denotes the start of the function. Function names follow the same rules as variable names in Python. The parameters (or arguments) are optional data which we can pass to the function when calling the function. A colon (:) is used to mark the end of the function header. The statements which represent the body of the function must be indented (by a tab). An optional return statement is used to return value(s) from the function (separated by commas). If it does not exist, the function automatically returns None. Let’s take a look at a simple function.

Example

Defining and calling a function.

1def test_fct(x):
2	return x+2, x+3
3
4val = 1
5print(f'If {val} is passed, the function will return {test_fct(val)} ')
(3, 4)
If 1 is passed, the function will return (3, 4)

The above function takes in one parameter x and returns a tuple (x+2, x+3) with two results. It is simply called using the function name with an argument i.e. test_fct(val). Here, val is the argument.

We may distinguish between two types of parameters.

  • Required parameters: do not have a default value
  • Optional parameters: supplied with a default value (including None).

The following example converts speeds in either “km/h” or “mi/h” into “m/s”. The vel parameter (required) is the input speed and the source parameter (optional) is either “km/h” or “mi/h” where the former is specified as the default.

Example

Using optional parameters in a function.

 1def convert_to_SI(vel, source = 'km/h'):
 2    if source.lower() == "km/h":
 3        s1 = vel*(1000/3600)
 4        return print(f'A speed of {vel} km/h is the same as {s1:,.2f} m/s.')
 5    elif source.lower() == "mi/h":
 6        s2 = vel*(1609/3600)
 7        return print(f'A speed of {vel} mi/h is the same as {s2:,.2f} m/s.')
 8    else:
 9        return print(f"Unknown source: {source}")
10
11convert_to_SI(50, 'mi/h') # first call
12convert_to_SI(60)  # second call
13convert_to_SI(60, "kmh")  # third call
A speed of 50 mi/h is the same as 22.35 m/s.
A speed of 60 km/h is the same as 16.67 m/s.
Unknown source: kmh

In the above example, the source parameter in the function has a default value of “km/h”. This means that in calling the function, the source parameter is assumed to be “km/h” if the corresponding argument isn’t specified. For example, in the second call, we have omitted the second argument and the source parameter is assumed to be “km/h”. In the third call, we have purposely used an invalid source argument, “kmh”. The function returns an error statement accordingly.

Calling Functions

To reiterate, a parameter is the variable listed inside the parentheses in the function definition. An argument, on the other hand, is the value that is sent to the function when it is called. We have already seen how we can call simple functions. The syntax is:

Syntax

Calling a Function.

1(value1, value2, ...) = function_name(positional_arg, arg_name=value, ...)

When calling a function, we distinguish between two kinds of arguments.

  • Positional arguments are values that are matched to the parameters according to their positions in the function definition.
  • Keyword arguments are arguments specified in the form arg_name=value where arg_name matches a certain parameter name in the function. Such arguments can be positioned in any order within the parentheses as long as the arg_name is specified.

Let’s take a look at the previous “convert_to_SI” function.

In the following, both arguments specified are positional arguments since parameter names are not provided.

1convert_to_SI(50, 'mi/h')
A speed of 50 mi/h is the same as 22.35 m/s.

In the following, we input only one positional argument since the second argument corresponds to an optional parameter (defaults as ‘km/h’).

1convert_to_SI(60)
A speed of 60 km/h is the same as 16.67 m/s.

In the following, we specify two keyword arguments; order is immaterial.

1convert_to_SI(source= "km/h", vel = 60)
A speed of 60 km/h is the same as 16.67 m/s.

Example

Find and print all prime numbers within a certain range. Recall that a prime number is one which is only divisible by 1 and itself.

This problem has been solved previously when we discussed looping techniques. In this example, we are going to employ a function to achieve the same results.

1def check_prime(x):
2   for i in range(2, x):
3        if (x % i) == 0: # test if num if divisible by numbers other than itself and 1
4            break          # if so, it is not prime, break the inner loop and move on to next num in the outer loop
5   else:
6        return print(f'{x} is prime')
7
8for num in range(2,30):
9    check_prime(num)
2 is prime
3 is prime
5 is prime
7 is prime
11 is prime
13 is prime
17 is prime
19 is prime
23 is prime
29 is prime

ADVERTISEMENT



The sorted() Function

Let’s see how user-defined functions can be combined with built-in functions like the sorted() function to perform specific tasks. The sorted() function sorts the elements of a given iterable in a specific order and returns it as a list.

Syntax

The sorted() function.

1sorted(iterable, key=None, reverse=False)
Parameter Required? Default Value Description
iterable ✔️ Yes NA An ordered sequence (string, tuple, list) or an unordered collection (set, dictionary) or any other iterator.
key ❌ No None A function that serves as a key for the sort comparison.
reverse ❌ No False If True, the sorted list is reversed (sorted in descending order).

Example

Arrange a list of random integers in ascending and descending orders.

1import random as rd
2list1 = rd.sample(range(1,21),20)  # 20 random integers between 1 and 20.
3print(f'Original random list: {list1}.')
4print(f'Sorted in ascending order: {sorted(list1)}.')
5print(f'Sorted in descending order: {sorted(list1,reverse=True)}.')
Original random list: [14, 6, 16, 20, 12, 18, 8, 17, 7, 15, 19, 5, 3, 13, 4, 10, 2, 11, 1, 9].
Sorted in ascending order: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20].
Sorted in descending order: [20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1].

Recall that the sorted() function also accepts a key function as an optional parameter. We can also sort the given iterable based on the returned value of the key function. The following example sorts the above list of random integers according to the sum of the digits.

Example

Using the sorted() function with a key.

1def sum_of_digits(x):
2    return sum([int(a) for a in str(x)])
3
4print(f'Sorted according to sum of digits : {sorted(list1, key = sum_of_digits)}.')
Sorted according to sum of digits : [10, 1, 20, 2, 11, 12, 3, 13, 4, 14, 5, 6, 15, 16, 7, 8, 17, 18, 9, 19].

As another example, given a dictionary of scores with names of students as keys, we would like to sort the dictionary items according to the scores.

Example

Sorting a dictionary using the sorted() function with a key.

1scores = {"Carol":68, "Sammy":85, "Derrick":62, "Juan":49, "Wendy":79, "Tom":86}
2
3def sort_dict(item):
4    return item[1] # each item is a 2-tuple: 1st element is key, 2nd element is value
5
6{k: v for (k, v) in sorted(scores.items(), key=sort_dict, reverse=True )}
{'Tom': 86, 'Sammy': 85, 'Wendy': 79, 'Carol': 68, 'Derrick': 62, 'Juan': 49}

Lambda Functions

Python allows you to define a function in a single line via lambda expressions. Lambda expressions are anonymous functions, which are functions without a name. The standard function:

1def function_name(arg1, arg2, ...):
2    return return_value

can be rewritten as a lambda function like this:

Syntax

Lambda function.

1lambda arg1, arg2, ...: return_value

As in a previous example, we would like to sort a list of random integers according to the sum of the digits - this time using a lambda function.

1print(list1) # random integers (generated above).
2sorted(list1, key = lambda x: sum([int(a) for a in str(x)]))
[14, 6, 16, 20, 12, 18, 8, 17, 7, 15, 19, 5, 3, 13, 4, 10, 2, 11, 1, 9]

[10, 1, 20, 2, 11, 12, 3, 13, 4, 14, 5, 6, 15, 16, 7, 8, 17, 18, 9, 19]

The following example was also considered previously. Given a dictionary of scores with names as keys, we would like to sort the dictionary items according to the scores.

1print(scores)
2{k: v for k, v in sorted(scores.items(), key=lambda item: item[1], reverse=True )}
{'Carol': 68, 'Sammy': 85, 'Derrick': 62, 'Juan': 49, 'Wendy': 79, 'Tom': 86}

{'Tom': 86, 'Sammy': 85, 'Wendy': 79, 'Carol': 68, 'Derrick': 62, 'Juan': 49}

ADVERTISEMENT



Namespace and Scope (optional)

A namespace is a collection of currently defined variable names along with information about the object that each name references. A namespace is a mapping from names to objects. We can think of a namespace as a dictionary in which the keys are the object names and the values are the objects themselves.

Functions can access variables in two different scopes: global and local. A local scope is the code block or body of any Python function. Variables defined within a function are called local variables (local to the function). This local scope contains the local namespace which is a list of variable names that are defined within the function. The visibility and accessibility of the variables in the local namespace is only allowed within that function. The local namespace is created when the function is called and is destroyed when the function ends. On the other hand, global variables are defined outside of any function. They can be accessed from anywhere in the Python script.

Let’s consider a few examples to illustrate the concepts of global and local variables.

1def func(x): # parameter x is a local variable
2    a = b + x  # local variable a defined, global variable b can be accessed
3    return a
4
5b = 2
6print(func(3))
7print(x)
8print(a)
---------------------------------------------------------------------------

NameError                                 Traceback (most recent call last)

~\AppData\Local\Temp/ipykernel_11192/1659717775.py in <module>
      4     #return a
      5
----> 6 print(func(3))
      7 b = 2
      8 #print(x)


~\AppData\Local\Temp/ipykernel_11192/1659717775.py in func(x)
      1 def func(x): # parameter x is a local variable
      2     #a = b + x  # local variable a defined, global variable b can be accessed
----> 3     print(b)
      4     #return a
      5


NameError: name 'b' is not defined

In the above, we have one global variable b since it is defined outside the function. Also, we have two local variables: a and x where a is defined within the function while x is a parameter of the function. When this function is called with the global variable b as argument, the local variable x is created and assigned the value that is currently stored in global variable b (b=2). Then the body of the function is executed and an assignment is made to local variable a which is set to be the sum of the parameter x and the global variable b.

After executing the return statement, both a and x will be destroyed. Hence, while the first print statement returns 3, the next two print statements lead to errors because they try to access variables that do not exist anymore.

Let’s take a look at the following where a local variable has the same name as a global variable.

1def func(x): # parameter x is a local variable
2    b = 10
3    a = b + x  #  b refers to the local b, not the global b
4    return a
5
6b = 2
7print(func(b))
8print(b)
12
2

In the above code, we have a global variable b=2 which is passed as an argument to the function. The function parameter x is then assigned the same value as b. A local variable b is then defined to be 10. This begs the question if the global b will also be modified to take the value of 10. The answer is NO, the global variable b is not changed by any assignment within the function. This is because the rule is that if a name is encountered on the left side of an assignment in a function, it will be considered a new local variable which is created at that point that will overwrite the global variable with the same name until the end of the function is reached.

However, if you want to deal with only one global variable b whether inside or outside the function, you have to explicitly tell Python that b should be interpreted as a global variable using the keyword global:

1def func(x): # parameter x is a local variable
2    global b # this b is a global variable
3    b = 10   # global b is now set to 10
4    a = b + x
5    return a
6
7b = 2
8print(func(b))
9print(b)
12
10
Caution

Accessing global variables from within functions should be avoided as much as possible. Passing values via parameters and returning values is usually preferable because it keeps different parts of the code as independent of each other as possible.

There is one last important point. Note that there is a subtle difference in passing values to a function depending on whether the value is from a mutable or immutable data type. All values of primitive data types and structures like numbers, booleans, strings and tuples in Python are immutable, meaning we cannot modify them without creating new objects. On the other hand, mutable data structures like lists and dictionaries can be modified without creating a completely new object.

Mutable and immutable data types are treated differently when provided as a parameter to functions as shown in the following examples. First, let’s look at the case of immutable data types.

1def func(a):
2    a = 20   # this does not change the value assigned to y
3
4b = 10
5func(b)
6
7print(b)     # will print out 3
10

First, note that the parameter a is treated as a local variable in the function. The function is then called with global variable b as argument. The variable x is now being assigned a copy of the value that variable b contains when the function is called. As a result, the value of the global variable b doesn’t change and the output produced by the last line is 3. But it only works this way for immutable objects.

What about mutable data types being passed as parameters to a function? Recall that lists are mutable. If we define a list object and assign it to two variables using the statement ‘b=a’, This means that whatever modifications made to a will happen to b as well (and vice versa) since they refer to the same list object. Let’s see what happens what we pass a list to a function.

1def func(x):
2    x.extend([4,5,6])
3
4b = [1,2,3]
5func(b)
6print(b)
[1, 2, 3, 4, 5, 6]

We realize that in contrast to passing immutable data types to a function, for values of mutable data types (like the list b above), assigning the value to function parameter x cannot be conceived as merely creating a copy of that value. Instead, x now refers to the same list object in memory as b. Therefore, any changes made to either variable x or b will change the same list object in memory. When variable x is destroyed after function execution ends, variable b will still refer to that modified list object. For those who are familiar with other languages like C, passing immutable data types to functions works like “call-by-value,” while passing mutable data types works like “call-by-reference.” Let’s consider one last example:

1def func():
2    list1.extend([4,5,6])
3
4list1 = []
5func()
6print(list1)
[4, 5, 6]

In the above, list1 is created as an empty list object. The function is then called where the global list1 object is modified in-place (due to mutability of the list object). Therefore, it is not surprising the last print statement returns a modified list. This is in contrast to the following where a new local list variable (with the same name as the global list object) is created within the function. In other words, the local variable list1 does not refer to the empty global list object.

1def func():
2    list1 = [4,5,6]
3
4list1 = []
5func()
6print(list1)
[]