Functions (Good)

What to Expect in this Chapter

In this chapter, I will tie up some loose ends about functions like types of arguments and docstrings. I will also discuss exception handling so that you can better understand how to deal with errors. By the end of this chapter, you will know the difference between positional, keyword and default arguments of functions. You will also be able to write code that checks and handle potential problems.

Check, balances and contingencies

Now is a good time to discuss pre-empting problems even though this topic applies to more than just functions. For example, I asked you to check for zero division in one of the previous exercises related to the divided() function. This is because users and programmers are not infallible, and you cannot think of everything that can go wrong; it is good to have checks, balances and contingencies. There are two standard ways Python allows us to do this.

assert

Python has a command called assert that can check a condition and halt the execution if necessary. It also gives the option of printing a message.

The basic syntax is as follows:

assert condition-to-check, message

assert stops the flow if the condition fails. Here is an example.

assert x >= 0, "x is becoming negative!"

The programme will run for as long as the condition is True. If it fails, then an AssertationError is raised, and the programme stops running!

So, the following will run without a problem

x = 10
assert x >= 0, "x is becoming negative!"

but not the following.

x = -1
assert x >= 0, "x is becoming negative!"

By the way, I encourage you to include ‘print()’ statements here and there to let the outside world know what is happening in the innards of your programme. Otherwise, you will be staring at a blank cell, wondering what is happening.

try-except

A technical name for things going wrong is exceptions. For example, division by zero will raise a ZeroDivisionError. An exception left unhandled will halt the flow of the programme. However, if you are a control freak, Python offers an (absurdly) simple ‘try-except’ structure to catch and handle these exceptions yourself.

The try-except syntax can also be used to ensure that your programme is able to handle some situations that are beyond your control. For example, when I use Python to speak to the LumiNUS server I use try-except to handle situations when the server does not respond.

Let me show you how to use the try-except flow control statement.

We can solicit a user response using the input() function. Let’s say we do this and ask for a number, as shown in the snippet below.

number=input("Give me a number and I will calculate its square.")
square=int(number)**2              # Convert English to number
print(f'The square of {number} is {square}!')

This will work fine if the typecasting int(number) makes sense. What if the input is not a number but something else like ‘hahaha’?

Let’s use the try-except to get around this problem.

try:
    number=input("Give me a number and I will calculate its square.")
    square=int(number)**2
    print(f'The square of {number} is {square}!')
except:
    print(f"Oh oh! I cannot square {number}!")

Notice how I have enclosed (and protected) that part of the code that we think can potentially get into trouble in the try block. If something (anything) goes wrong, Python will ignore the error and run the code in the except block.

Some loose ends

Positional, keyword and default arguments

In the past chapter, some of you may have noticed that I was (carelessly) switching between passing two styles of passing arguments to the function greeting(). I wrote greeting('Super Man') or greeting(name='Super Man'). We need to talk a bit more about this so that you are not bewildered when you see other people’s code.

There are three ‘ways’ to pass a value to an argument. I will call them positional, keyword or default. To make this clearer consider the following function.

def funny_add(a, b, c=1):
    return a + 10*b + 100*c

If I call the function using funny_add(1, 2, 3), I am telling Python to assign 1, 2, 3 to a, b, c using the positional order of the arguments.

Another way is to explicitly specify the keyword to assign the values by calling the function as funny_add(c=3, b=1, a=2) (No, the order does not matter)

Further, since c is optional, I can choose not to specify it (of course, provided I want c to be 1).

Below are some examples of how you can combine these three styles. However, one style (keyword followed by positional) confuses Python and won’t work.

funny_add(1, 2)           # Two positional, 1 default
## 121
funny_add(1, 2, 3)        # Three positional
## 321
funny_add(a=1, b=2)       # Two keyword, 1 default
## 121
funny_add(c=3, b=1, a=2)  # Three keyword
## 312
funny_add(1, c=3, b=2)    # One positional, 2 keyword
## 321
funny_add(1, b=2)         # One positional, 1 keyword, 1 default
## 121

However, the following will not work because Python cannot unambiguously figure out what the position of 1 is?

funny_add(a=2, 1)         # Won't work.
                          # Keywords cannot be followed by 
                          # positional

Docstrings

Python has a docstring feature that allows us to document what a function does inside the function. This documentation (i.e. the docstring) is displayed when we ask Python to show us the help info using help().

Here is a simple example.

def funny_add(a, b, c=1):
    '''
    A test function to demonstrate how 
    positional, keyword and default arguments 
    work.
    '''
    return a + 10*b + 100*c

A docstring needs to be sandwiched between a pair of ''' (or """) and can span multiple lines.

Let’s see if it works by asking for help.

help(funny_add)
Help on function funny_add in module __main__:

funny_add(a, b, c=1)
    A test function to demonstrate how 
    positional, keyword and default arguments 
    work.

Docstrings can be used for writing multiline comments, but the practice is frowned upon by Puritans; tread at your own risk.

Function are first-class citizens

Python functions are called first-class citizens because they have the same privileges as variables. This opens up very useful possibilities for scientific programming because we can pass a function as an argument to another function!

Consider this:

def my_function(angle, trig_function):
        return trig_function(angle)

# Let's use the function
my_function(np.pi/2, np.sin)        
## 1.0
my_function(np.pi/2, np.cos)        
## 6.123233995736766e-17
my_function(np.pi/2, lambda x: np.cos(2*x))  
## -1.0

Note: When we pass a function as an argument, we do not include the parenthesis ().

More unpacking

There is more to unpacking. For example, unpacking can make extracting information from lists and arrays a breeze. Here are some examples.

  1. x, y, z = [1, 2, 3]
    print(x, y, z)
    1 2 3
  2. x, y, z = np.array([1, 2, 3])
    print(x, y, z)
    1 2 3
  3. x, *y, z = np.array([1, 2, 3, 4, 5])
    print(x, y, z)
    1 [2, 3, 4] 5
  4. x, *_, y = [1, 2, 3, 4, 5]
    print(x, y)
    1 5