Functions

What is a Function?

Function is a set of Python codes that performs a task or returns a result.

So far, you have learnt how to use some of the builtin function in Python, such as print(), len(), range(), min(), max() etc.

You can write lines of codes to perform a task or return results. But the lines of code gets larger from few lines to 100s or 1000s of lines of code. It is recommended to be break down the code to smaller chunks. This is helpful to make your code more maintainable and readable as well as avoid repetition by reusing chunks of codes. These reusable chunks are called Functions.

What are the Types of Functions?

There are generally two types of functions:

  1. Perform a task

  2. Return a value(s) or an expression(s)

What is a Function Syntax?

In Python, the building block of a function is as follow:

  1. Function starts with def keyword

  2. A uniquely identified name for the function

  3. Parameters to pass the value(s) of the function into the function. This is optional.

  4. A way to define where the function header finishes: colon :

  5. Multi-line string to document what the function input and output is or performs. This is also optional but it is recommended.

  6. Python statement(s) that performs a task

  7. If you want the function to return an output, you can use return statement. It is also optional.

def function_name(inputs):
    """
    docstrings   
    """
    # Python statements
    
    return outputs

What is return Statement?

The return statement is used to exit a function and return an output. The output can be assigned to a variable with assignment operator. The return statement is:

return output(s)

The return statement evaluates the function and returns the output. It will return a None object where there is either no expression to return or no return statement is present.

An Example of a Function

An example of a function that takes two integers and return the sum of the values. The function is created below:

def take_sum(value_1_int, value_2_int):
    """
    inputs:
    
    output:
    """
    sum_of_values = value_1_int + value_2_int
    
    return sum_of_values

Semantics: Arguments vs Parameters

In the context of take_sum() function, value_1_int and value_2_int are called parameters of take_sum() function. value_1_int and value_2_int are variables locally connected to the take_sum() function.

You can call the function name followed by a parentheses () with the arguments in between.

value_1_int = 2
value_2_int = 10

take_sum(value_1_int, value_2_int)

#output
12

Now, value_1_int and value_2_int are called the arguments of take_sum() and you also passed value_1_int and value_2_int by reference. i.e. memory addresses of the value_1_int and value_2_int are passed to the function.

A function can be created without parameters.

def greeting():
    print('Good day')
    print('Stay safe and healthy')

What is Positional Arguments?

Positional Arguments is the order in which parameters are passed into a function. It is the most common way of assigning arguments to parameters.

take_sum(2, 10) #--> value_1_int = 2, value_2_int = 10

take_sum(10, 2) #--> value_1_int = 10, value_2_int = 2

take_sum(value_1_int = 10, value_2_int = 2)

What is a Default Values in a Function?

A positional arguments can be made optional by specifying a default value for the corresponding parameter. Let's take the take_sum() function as an example.

def take_sum(value_1_int, value_2_int=10):
    sum_of_values = value_1_int + value_2_int
    
    return sum_of_values
    
    
take_sum(2)

# output: 12

The default value always comes as a last parameter in the function or any parameter that comes after it has to be assigned with default value. Let's modify take_sum() function to have three parameters.

def take_sum(value_1_int, value_2_int=10, value_3_int):
    sum_of_values = value_1_int + value_2_int + value_3_int
    
    return sum_of_values
    
# output
#  File "<ipython-input-3-4a4bb3b6c326>", line 1
#    def take_sum(value_1_int, value_2_int=10, value_3_int):
#                ^
# SyntaxError: non-default argument follows default argument

But what if you want to specify the 1st and 3rd arguments, but omit the 2nd argument? i.e. you want to specify values for value_1_int and value_3_int, but let value_2_int take on its default value.

take_sum(value_1_int=2, value_3_int=30)
take_sum(2, 30)
# or

take_sum(2, value_3_int=30)

Keyword arguments

Optionally, positional arguments can be specified by using the parameter name regardless of the parameters with or without default values.

take_sum(2, 10, 20)
# or
take_sum(2, 10, value_3_int=20)
# or
take_sum(value_1_int=2, value_2_int=10, value_3_int=20)
# or
take_sum(value_3_int=20, value_1_int=2, value_2_int=10)

When you use a named argument, it is required to use named arguments for all arguments after it.

take_sum(value_3_int=20, 10, 2)
# or
take_sum(2, value_3_int=20, 10)


# output
#  File "<ipython-input-8-86d09888c0c5>", line 6
#    take_sum(value_3_int=20, 10, 2)
#                            ^
#SyntaxError: positional argument follows keyword argument

You can also not call the named arguments when you are calling your function

take_sum(2) # value_1_int=2, value_2_int=10, value_3_int=20
take_sum(2, 100) # value_1_int=2, value_2_int=100, value_3_int=20
take_sum(2, 100, 200) # value_1_int=2, value_2_int=100, value_3_int=200

What is *args?

Remember the iterable unpacking

x, y, z = 1, 2, 3

Similar things happens when positional arugments are passed to a function

take_sum(value_1_int, value_2_int, value_3_int)

take_sum(2, 4, 6)

There is a handy trick that can be used during tuple unpacking when you have imbalance on right and left of the assignment operator. You can use a * operator to create a tuple/list from several values.

x, y, z = 1, 2, 3, 4

The above code gives an error. But we can overcome that with * operator. The first and second will be assigned to x and y respectively. The third and fourth will be a list and assigned to z

x, y, *z = 1, 2, 3, 4

print(x)
print(y)
print(z)
print(type(z))

Let's define the function again, notice the start at the start of third value.

def take_sum(value_1_int, value_2_int, *value_3_int):
    print(value_1_int)
    print(value_2_int)
    print(value_3_int)
    
    
take_sum(1, 2, 'string1', 'string2')
        # value_1_int = 1
        # value_2_int = 2
        # value_3_int = ('string1', 'string2') # a tuple (a minor difference)

The parameter name value_3_int is arbitrary. You can choose any name you would like, but it is customary to name it *args. So your function can be updated as below.

def take_sum(value_1_int, value_2_int, *args):
    print(value_1_int)
    print(value_2_int)
    print(args)

*args will not allow anymore positional arguments. But there is a way around it.

def take_sum(value_1_int, value_2_int, *args, value_3_int):
    print(value_1_int)
    print(value_2_int)
    print(args)
    print(value_3_int)

This will not work

take_sum(2, 10, 30, 50, 20)

This will work

take_sum(2, 10, 30, 50, value_3_int = 20)

Unpacking arguments

You can pass an iterable argument into a function with the * operator.

def take_sum(value_1_int, value_2_int, value_3_int):
    sum_of_values = value_1_int + value_2_int + value_3_int
    
    return sum_of_values
 

values = [2, 10, 20] 
take_sum(*values)
# or
take_sum(2, 10, 20)
# output
#32

What is Keyword Arguments?

The positional parameters can, optionally, be passed as named (keyword) arguments. For example, if we define a function with three positional arguments:

def get_sum(int_1, int_2, int_3):
    total = int_1 + int_2 + int_3
    return total

We can call the function get_sum() and pass the arguments. The named arguments is optional and it is up to you.

# you can call the function
get_sum(2, 3, 4)
#or
get_sum(int_1=2, int_3=4, int_2=3)

You can also rewrite the function above to make more intuitive and practical. Using *args, you can pass many values at the same time.

def get_sum(*args):
    total=0
    for value in args:
        total += value
    return total    

Mandatory Keyword Arguments

Sometimes, you need to make the keyword arguments mandatory and force the user to call the named arguments. You can do so by exhausting all the positional arguments, and specify another additional parameter or parameters in the function definition.

def get_sum(int_1, int_2, *args, int_last):
    total = int_1 + int_2 + int_last
    return total, args

The presence of *args exhausts all positional arguments and int_3 MUST be passed as a named argument.

In the function get_sum(), you will need to pass two positional arguments, one or more additional arguments (optional), and a mandatory keyword argument which goes into int_3. The int_3 argument can only be passed to the function using a named (keyword) argument.

This will not work

get_sum(2, 3, 22, 33, 44, 4)

But using named argument for int_3, it works.

get_sum(2, 3, 22, 33, 44, int_3=4)

#or

get_sum(1, 2,int_3=5)

You can even define a function that has only optional positional arguments and mandatory keyword arguments, i.e. you can force no positional arguments at all.

def get_sum(*args, max):
    total=0
    for value in args:
        total += value
    total += max
    return total

The function get_sum() can be rewritten to provide no positional arguments at all.

def get_sum(*, max):
    total=0
    for value in args:
        total += value
    total += max
    return total

Now, you have only * without specifying the name (usually args). The * indicates the end of positional arguments. You tell your function that you are not looking for positional arguments, effectively, there is no positional arguments.

if you call the function and provide positional arguments, Python raises an Exception error.

get_sum(2, 3, 22, 33, 44, max=4)

Note: The aims is to showcase no positional arguments and the get_sum() function has to be fixed.

In nutshell: *args vs *

# Function 1
def func(x, y=1, *args, z, factor=True):
    # statements


# Function 2
def func(x, y=1, *, z, factor=True):
    # statements
  1. x: mandatory positional argument (may be specified using a named argument)

  2. y: optional positional argument (may be specified positionally, as a named argument, or not at all), defaults to 1

  3. args vs *

    1. args: catch-all for any (optional) additional positional arguments

    2. *: no additional positional arguments allowed

  4. z: mandatory keyword argument

  5. factor: optional keyword argument, defaults to True

What is **kwargs?

You have a clear understanding what*args is by now. **kwargs works very similar *args, but instead of accepting positional arguments (*args returns a tuple) and accepts named arguments.

**kwargs is used to accept a variable amount of remaining keyword arguments. It returns a dictionary.

**kwargs can be specified even if the positional arguments have NOT been exhausted. This is opposite to *args.

Let's define a function:

def new_func(*, int_1, **kwargs):
  print(int_1)
  print(kwargs)
  
  new_func(int_1=1, x=2, y=4)

You can also use it by passing only the named argument int_1

new_func(int_1=1)

This will print only the mandatory positional argument and an empty dictionary.

# Output
1
{}

You can also create a function by just passing a **kwargs. For example:

def new_func(**kwargs):
  print(kwargs)
  
new_func(x=2, y=4, z=6)

You can even create a function with both *args and **kwargs.

def new_func(*args, **kwargs):
  print(args)
  print(kwargs)
  
new_func(11, 22, 33, x=2, y=4, z=6)

Can we mix args and kwargs as input to a function?

You can even call the function without providing any arguments. This result in returning an empty tuple () and an empty dictionary {}.

new_func()

# Output
()
{}

You can check the Jupyter Notebook in Colab

Last updated