Functions (Nice)

Warning

The material in this ‘Nice’ chapter is optional. The discussion typically deals with content beyond what a novice should know. So, please finish all the ‘Need’ and ‘Good’ portions before you go through this chapter.

1 Modularise and reuse

It is more efficient to reuse your code (dah!). Using functions1 makes this very easy. However, you can go a step further and reuse functions between projects by placing them in a separate file and importing them like in standard packages and modules like NumPy. I.e., whenever you find yourself using a set of functions often, you should consider saving them as a module that you can import.

The exercise at the end of this chapter will give you some practice.

2 The many ways to pass arguments

2.1 *args & **kwarg

You will often see the syntax *args and **kwarg. These stand for arguments and keyword arguments, respectively. They allow us flexible ways of using unpacking and dictionaries to pass information to functions. Let’s see how to do this.

*args

  1. def multiply(x, y):
        return x * y
    
    numbers = [1, 2]
    multiply(*numbers)

    We can use unpacking to make passing arguments to functions a breeze!

    The * is essential! Without this, Python will assign x=numbers and complain that y is missing.

    2
  2. def multiply(*args):
        result = 1
        for number in args:
            result *= number
    
        return result
    
    numbers = [1, 2, 3]
    multiply(*numbers)
    multiply(1, 2, 3, 4, 5)

    What if we want our function to multiply more than two numbers?

    6

    This will work, too!

    120

**kwargs

  1. def multiply(x, y, z):
        return x * y * z
    
    # Let's use the function
    numbers = {'x': 1, 'y': 2, 'z': 3}
    multiply(**numbers)

    We can also pass keyword arguments using a dictionary:

    The ** is essential!

    6
  2. def multiply(x, y, z):
        return x * y * z
    
    # Let's use the function
    numbers = {'y': 2, 'z': 3}
    multiply(1, **numbers)

    We can mix positional arguments and a dictionary!

    6
  3. def add_powers(numbers, power):
        result = 0
        for number in numbers:
            result += number**power
    
        return result
    
    # Let's use the function
    kwargs = {'numbers': [1, 2, 3], 'power': 2}
    add_powers(**kwargs)
    14
  4. def add_powers(**kwargs):
        numbers = kwargs['numbers']
        power = kwargs['power']
    
        result = 0
        for number in numbers:
            result += number**power
    
        return result
    
    
    # Let's use the function
    add_powers(numbers=[1, 2, 3], power=2)
    kwargs = {'numbers': [1, 2, 3], 'power': 2}
    add_powers(**kwargs)

    We can also set up our function to accept any keyword arguments!

    14

    This works too!

    14

3 Gotchas with passing variables to functions

3.1 The Problem

Using functions to modularise your code (and your thinking) is good. However, you need to be careful with the type of variables that you pass as arguments. To see what I am talking about, try running the following code. Can you see what is happening?

def do_something(inside_number, inside_array, inside_list):
    print('Doing something!')
    inside_number *= 2
    inside_array *= 2
    inside_list *= 2

    print(f"INSIDE|\tNumber: {inside_number}(id: {id(inside_number)}), Array: {inside_array}(id: {id(inside_array)}), List: {inside_list}(id: {id(inside_list)})")

outside_number = 10
outside_array = np.array([10])
outside_list = [10]

print(f"BEFORE|\tNumber: {outside_number}(id: {id(outside_number)}), Array: {outside_array}(id: {id(outside_array)}), List: {outside_list}(id: {id(outside_list)})")
do_something(outside_number, outside_array, outside_list)
print(f"AFTER|\tNumber: {outside_number}(id: {id(outside_number)}), Array: {outside_array}(id: {id(outside_array)}), List: {outside_list}(id: {id(outside_list)})")
BEFORE| Number: 10(id: 139949048705000), Array: [10](id: 139947793568688), List: [10](id: 139947793716800)
Doing something!
INSIDE| Number: 20(id: 139949048705320), Array: [20](id: 139947793568688), List: [10, 10](id: 139947793716800)
AFTER|  Number: 10(id: 139949048705000), Array: [20](id: 139947793568688), List: [10, 10](id: 139947793716800)

So, the function has changed the values of some variable outside the function! But, not all variables are affected.

3.2 An Explanation

For ‘immutable’ variables, what happens inside the function does not change the variable outside. In other languages, this behaviour is called passing by value.

For ‘mutable’ variables, what happens inside the function does change the variable outside. In other languages, this behaviour is called passing by reference.

So, in Python, you must be very careful about the mutability of the variable you are passing. Otherwise, you will spend a long time trying to understand why your code is acting weird.

4 There is more to exceptions

4.1 A list of exceptions

Here is an incomplete list of exceptions2. You can find more details at the Python documentation pages.

Exception Description
AssertionError Raised when the assert statement fails.
AttributeError Raised on the attribute assignment or reference fails.
EOFError Raised when the input() function hits the end-of-file condition.
FloatingPointError Raised when a floating point operation fails.
ImportError Raised when the imported module is not found.
IndexError Raised when the index of a sequence is out of range.
KeyError Raised when a key is not found in a dictionary.
NameError Raised when a variable is not found in the local or global scope.
OSError Raised when a system operation causes a system-related error.
OverflowError Raised when the result of an arithmetic operation is too large to be represented.
RuntimeError Raised when an error does not fall under any other category.
SyntaxError Raised by the parser when a syntax error is encountered.
IndentationError Raised when there is an incorrect indentation.
SystemError Raised when the interpreter detects internal error.
SystemExit Raised by the sys.exit() function.
TypeError Raised when a function or operation is applied to an object of an incorrect type.
UnboundLocalError Raised when a reference is made to a local variable in a function or method, but no value has been bound to that variable.
ValueError Raised when a function gets an argument of correct type but improper value.
ZeroDivisionError Raised when the second operand of a division or module operation is zero.

4.2 Handling specific exceptions

I was sloppy in my try-exept example in the last chapter. I could have been more specific about the type of exception. A better version of the code is:

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 ValueError:
    print(f"Oh oh! I cannot square {number}!")

4.3 try also has an else

The try-except statement also has an optional else block that is run only if everything works smoothly. Here is an example:

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 ValueError:
    print(f"Oh oh! I cannot square {number}!")
else:
    print('Yeah! Things ran without a problem!')

Exercises

Back to top

Footnotes

  1. and classes↩︎

  2. borrowed from here↩︎