# | include: false
import numpy as np
import pandas as pd
256852) np.random.seed(
Functions (Nice)
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
-
def multiply(x, y): return x * y = [1, 2] numbers *numbers) multiply(
We can use unpacking to make passing arguments to functions a breeze!
The
*
is essential! Without this, Python will assignx=numbers
and complain thaty
is missing.2
-
def multiply(*args): = 1 result for number in args: *= number result return result = [1, 2, 3] numbers *numbers) multiply(1, 2, 3, 4, 5) multiply(
What if we want our function to multiply more than two numbers?
6
This will work, too!
120
**kwargs
-
def multiply(x, y, z): return x * y * z # Let's use the function = {'x': 1, 'y': 2, 'z': 3} numbers **numbers) multiply(
We can also pass keyword arguments using a dictionary:
The
**
is essential!6
-
def multiply(x, y, z): return x * y * z # Let's use the function = {'y': 2, 'z': 3} numbers 1, **numbers) multiply(
We can mix positional arguments and a dictionary!
6
-
def add_powers(numbers, power): = 0 result for number in numbers: += number**power result return result # Let's use the function = {'numbers': [1, 2, 3], 'power': 2} kwargs **kwargs) add_powers(
14
-
def add_powers(**kwargs): = kwargs['numbers'] numbers = kwargs['power'] power = 0 result for number in numbers: += number**power result return result # Let's use the function =[1, 2, 3], power=2) add_powers(numbers= {'numbers': [1, 2, 3], 'power': 2} kwargs **kwargs) add_powers(
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!')
*= 2
inside_number *= 2
inside_array *= 2
inside_list
print(f"INSIDE|\tNumber: {inside_number}(id: {id(inside_number)}), Array: {inside_array}(id: {id(inside_array)}), List: {inside_list}(id: {id(inside_list)})")
= 10
outside_number = np.array([10])
outside_array = [10]
outside_list
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:
=input("Give me a number and I will calculate its square.")
number=int(number)**2
squareprint(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:
=input("Give me a number and I will calculate its square.")
number=int(number)**2
squareprint(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!')