Defining and Using Functions

How to make pieces of code reusable and more logical? Create functions!

There are two ways of creating functions:

  • the def statement
  • the lambda statement, useful for creating short anonymous functions.

Simple example

We are going to start with a simple function that just prints out a message.


In [24]:
# define your first ever function
def my_pet(your_favourite_animal):
    print(your_favourite_animal + " is the best!")
    print("Congratulations, you have used your first ever python function!")
    
# Hint = if you are getting this error:
# TypeError: function_name() missing 1 required positional argument: 'your_favourite_animal'
# It means that you called a function without supplying it with input

Put the name of an animal into the brackets. Hint: be sure to make it a string!


In [26]:
my_pet('Mr Toad')


Mr Toad is the best!
Congratulations, you have used your first ever python function!

Note: printing data out does not return it to you as output. To get output from a function, you have to use return statement:


In [4]:
def square_fun(x):
    return x**2

In [5]:
result = square_fun(4)

In [6]:
result


Out[6]:
16

Another example

Now for something a little more complex. Here we combine a function with a loop, and save the data.


In [27]:
### 'def' allows us to define a function and call it 'fibonacci'
## 'N' allows us to pass an argument (value/statement) into the function, in this case, 'N' will be a number of our choice. 

def fibonacci(N):
    L = []                  # 1
    a, b = 0, 1             # 2
    while len(L) < N:       # 3
        a, b = b, a + b     # 4
        L.append(a)         # 5

    return L                # 6

# 1    'L' creates an empty data set, so that we can put data into it and save. 
# 2    Here we are just defining variables 'a, b' to the starting numbers '0, 1'
# 3    Now we start a continous loop 'while' and it continues indefinitely until the length(L) is > 'N' before stopping
# 4     Do some maths and save over the values 'a, b' This allows us to save the value of 'a' into the empty dataset 'L'.
# 5    We append the data so that it does not overwrite previous values. 
# 6     Return 'L' to python

Now we have a function named fibonacci which takes a single argument N, does something with this argument, and returns a value; in this case, a list of the first N Fibonacci numbers:


In [8]:
fibonacci(10)
# Now we can call the function by typing the name, and putting a value of our choice into the function using brackets.


Out[8]:
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
  • Notice that there is no type information associated with the function inputs or outputs
  • Python functions can return any Python object, simple or compound

Default Argument Values

Often when defining a function, there are certain values that we want the function to use most of the time, but we'd also like to give the user some flexibility. In this case, we can use default values for arguments. Consider the fibonacci function from before. What if we would like the user to be able to play with the starting values? We could do that as follows:

But now we can use the function to explore new things, such as the effect of new starting values:


In [9]:
fibonacci(10, 0, 2)
# This doesn't work? 
# Why?
# Well, we have not told the function that 'a, b' should be the values '0, 2'
# Time to redesign the function a bit!


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-9-a6743b6a52a9> in <module>()
----> 1 fibonacci(10, 0, 2)
      2 # This doesn't work?
      3 # Why?
      4 # Well, we have not told the function that 'a, b' should be the values '0, 2'
      5 # Time to resign the function a bit!

TypeError: fibonacci() takes exactly 1 argument (3 given)

We have redefined the function to include 'a' and 'b' as arguments.


In [28]:
def fibonacci(N, a=0, b=1):
    """Calculate Fibonacci sequence"""
    L = []                  # 1
    while len(L) < N:       # 3
        a, b = b, a + b     # 4
        L.append(a)         # 5

    return L                # 6

The values can also be specified by name if desired, in which case the order of the named values does not matter:


In [30]:
fibonacci(b=3, a=1, N=10)


Out[30]:
[3, 4, 7, 11, 18, 29, 47, 76, 123, 199]

A more useful example

Now let's calculate the potential temperature ($\theta$) of the atmosphere. Below is a function to do that, and from its docstring you can understand what data should be put in, and what comes out. (See, how useful docstrings are? Always write them in your code!).


In [31]:
def calc_theta(t, p, p0=1e5, r_d=287.04, c_p=1004.5):
    """
    Calculate air potential temperature

    Parameters
    ==========
    t : air temperature (K)
    p : air pressure (Pa)
    
    Optional inputs
    ---------------
    p0 : reference pressure (Pa), optional. Default is 1e5.
    r_d : gas constant of air (J kg^-1 K^-1), optional. Default is 287.04.
    c_p : specific heat capacity at a constant pressure (J kg^-1 K^-1), optional. Default is 1004.5.
    
    Returns
    =======
    theta: air potential temperature (K)
    """
    theta = t * (p0 / p) **(r_d / c_p)
    return theta

Exercise

Can you calculate $\theta$ for a "real" temperature of 14$^\circ$C and a pressure of 1003 hPa? Be careful with units!


In [34]:
# Your code here
# help(calc_theta)

*args and **kwargs: Flexible Arguments

How about a a function which you don't initially know how many arguments there are? We can use the special form *args (arguments) and **kwargs (keyword arguments)to catch all the arguments that are passed.

Here is an example:


In [14]:
def catch_all(*args, **kwargs):
    print("args =", args)
    print("kwargs = ", kwargs)

In [15]:
catch_all(1, 2, 3, python=4, conda=5)


('args =', (1, 2, 3))
('kwargs = ', {'python': 4, 'conda': 5})

In [16]:
catch_all('a', keyword=2)


('args =', ('a',))
('kwargs = ', {'keyword': 2})

Here it is not the names args and kwargs that are important, but the * characters preceding them. args and kwargs are just the variable names often used by convention, short for "arguments" and "keyword arguments". The operative difference is the asterisk characters: a single * before a variable means "expand this as a sequence", while a double ** before a variable means "expand this as a dictionary".


In [17]:
inputs = (1, 2, 3)
keywords = {'pi': 3.14}

catch_all(*inputs, **keywords)

print()
print('compare to this:')
print()

catch_all(inputs, keywords)


('args =', (1, 2, 3))
('kwargs = ', {'pi': 3.14})
()
compare to this:
()
('args =', ((1, 2, 3), {'pi': 3.14}))
('kwargs = ', {})

Anonymous (lambda) Functions

"lambda" [parameter_list]: expression

Earlier we quickly covered the most common way of defining functions, the def statement. You'll likely come across another way of defining short, one-off functions with the lambda statement. It looks something like this:


In [18]:
add = lambda x, y: x + y
add(1, 2)


Out[18]:
3

This lambda function is roughly equivalent to


In [19]:
def add(x, y):
    return x + y

So why would you ever want to use such a thing? Primarily, it comes down to the fact that everything is an object in Python, even functions themselves! That means that functions can be passed as arguments to functions.

As an example of this, suppose we have some data stored in a list of dictionaries:


In [20]:
data = [{'first':'Guido', 'last':'Van Rossum', 'YOB':1956},
        {'first':'Grace', 'last':'Hopper',     'YOB':1906},
        {'first':'Alan',  'last':'Turing',     'YOB':1912}]

Now suppose we want to sort this data. Python has a sorted function that does this:


In [21]:
sorted([2,4,3,5,1,6])


Out[21]:
[1, 2, 3, 4, 5, 6]

But dictionaries are not orderable: we need a way to tell the function how to sort our data. We can do this by specifying the key function, a function which given an item returns the sorting key for that item:


In [22]:
# sort alphabetically by first name
sorted(data, key=lambda item: item['first'])


Out[22]:
[{'YOB': 1912, 'first': 'Alan', 'last': 'Turing'},
 {'YOB': 1906, 'first': 'Grace', 'last': 'Hopper'},
 {'YOB': 1956, 'first': 'Guido', 'last': 'Van Rossum'}]

In [23]:
# sort by year of birth
sorted(data, key=lambda item: item['YOB'])


Out[23]:
[{'YOB': 1906, 'first': 'Grace', 'last': 'Hopper'},
 {'YOB': 1912, 'first': 'Alan', 'last': 'Turing'},
 {'YOB': 1956, 'first': 'Guido', 'last': 'Van Rossum'}]

While these key functions could certainly be created by the normal, def syntax, the lambda syntax is convenient for such short one-off functions like these.

References

A Whirlwind Tour of Python by Jake VanderPlas (O’Reilly). Copyright 2016 O’Reilly Media, Inc., 978-1-491-96465-1