Python functions

The function objects in Python allow you to give a name to a series of expressions that may or may not return a result. We have already seen a few functions. print() is a function, type() is a function, help() is a function. We have also used some standard mathematical functions like math.exp(). Functions are extremely important in data science. The models we build are essentially functions with parameters that are tuned to the data. So, we have to understand how functions work.

To syntax for defining a function in Python is as follows:

def function_name(some_inputs):
    some expressions
    return something (optional)

Let’s start with very simple functions.

Functions with no inputs that return nothing

Let’s start first with functions that have no inputs and return nothing.

def print_hello():
    """
    Prints hello.
    """
    print('Hello there!')

First, let’s run this:

print_hello()
Hello there!

See for yourself that the type of print_hello is a function:

type(print_hello)
function

The other thing that I want you to notice is the text in the triple quotes below the function definition. This is called the docstring of the function. You use it to document what the function does so that other people know how to use it. It is not essential to have a docstring. However, it is a very good practice to do so if you want to remember what your code does. The docstring is what the help() function sees. Check this out:

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

print_hello()
    Prints hello.

Finally, let’s see if print_hello() returns anything. Let’s try to grab whatever it returns and print it.

res = print_hello()
print('Res is: ', res)
Hello there!
Res is:  None

Alright! Now you see why None is useful. A function that returns nothing, returns None.

Let me end this section by saying a few things about how I chose the function name. I could have called the function f() or ph() or banana(). Why did I choose the name print_hello(). Well, because this is what the function does. It print hello. In general, it is a good idea to pick nice discreptive names for your functions. Since the functions typically do something, you should use a verb in its name. Also, use complete words. Do not abbriviate the words. Notice also that I am using underscore to separate the words. Do that as well. It’s a style preference and it makes your code looks better. Also, do not use any capital letters unless it is absolutely justified. For example, do not use Print_hello() or print_Hello() or Print_Hello(). You will not remember which one you picked and you would have to go back and check wasting your time. Using only lower case letters and underscores makes it easier to remember the function names.

Questions

  • Remove the docstring from the definition of print_hello, rerun the code block that defines it, and then try calling help(print_hello) again. Do you like the help you see now? That’s why you have to have a docstring.

# your code here

Functions with inputs that return nothing

Let’s make a function that takes the name of someone as input and prints a hellow statement. Here you go:

def print_hello_to(name):
    """
    Prints hello using the name provided.
    
    Arguments:
    
    name    -   The name of a person.
    
    Returns: Nothing
    """
    print('Hello there ' + name + '!')

Let’s use it:

print_hello_to('Ilias')
Hello there Ilias!
print_hello_to('Philippos')
Hello there Philippos!

Now, see the help() function applied to print_hello_to:

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

print_hello_to(name)
    Prints hello using the name provided.
    
    Arguments:
    
    name    -   The name of a person.
    
    Returns: Nothing

You can have multiple inputs to a function. Let’s write a function with two inputs:

def print_hello_to_two(name1, name2):
    """
    Prints hello using the names provided.
    
    Arguments:
    
    name1    -   The name of the first person.
    name2    -   The name of the second person.
    
    Returns: Nothing
    """
    print('Hello there ' + name1 + ' and ' + name2 + '!')
print_hello_to_two('Ilias', 'Philippos')
Hello there Ilias and Philippos!

Question

  • Create a function that says hello to three people.

# your code here

Numerical functions

When I am talking about numerical functions, I mean things like \(f(x) = x^2\) or \(g(x) = \sin(x)\) and so on. These functions typically take a single input that is a real number and they return also a single input which is a real number. Here are some examples:

def square(x):
    """
    Calculates the square of ``x``.
    
    Arguments:
    x     -   The real number you wish to square
    
    Returns: The square of ``x``.
    """
    return x ** 2
help(square)
Help on function square in module __main__:

square(x)
    Calculates the square of ``x``.
    
    Arguments:
    x     -   The real number you wish to square
    
    Returns: The square of ``x``.
square(2)
4
square(23.0)
529.0

Because real functions are used so often, there is actually a shortcut. It is called lambda functions. To define a lambda function, the syntax is:

func_name = lambda inputs: single_expression_you_want_to_return

Here is the square function in a single line:

alt_square = lambda x: x ** 2
alt_square(2)
4
alt_square(23.0)
529.0

You will see me using both.

What happens if you pass an input of the wrong type?

Python does not check what types of inputs you give two a function. For example, you can pass to square() whatever type you want. Python will try to evaluate it and it will throw an error if it cannot do. Let’s give square a string to see what happens:

square('one')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-33-1674fed473a2> in <module>
----> 1 square('one')

<ipython-input-26-412f27890e5e> in square(x)
      8     Returns: The square of ``x``.
      9     """
---> 10     return x ** 2

TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

That’s a very nice error message. It is a TypeError. You are going to feel the urge to skip over these messsages. Don’t do it! Read the message. It tells you where the problem is. The problem here is that the exponentation operator ** is not defined for strings. Let’s replicate the error directly:

'one' ** 2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-34-eafaa763ebf3> in <module>
----> 1 'one' ** 2

TypeError: unsupported operand type(s) for ** or pow(): 'str' and 'int'

So, this kind of expression is meaningless for Python. Be careful with the inputs you pass to your functions.

Default parameters

Some times you may have a function with multiple inputs and some of them have unique values. Let’s make a function that makes a plot of a numerical function on a given range.

First, load some libraries we are going to need:

import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
sns.set(rc={"figure.dpi":100, 'savefig.dpi':300})
sns.set_context('notebook')
sns.set_style("ticks")
from IPython.display import set_matplotlib_formats
set_matplotlib_formats('retina', 'svg')
import numpy as np

And here is the function definition:

def plot_func(f, left=0, right=1, color='r'):
    """
    Plot function f over the range of values [left, right] using color.
    
    Arguments:
    f     -    The function you want to plot. Yes, you can have a function
               as in input to another function!
    left  -    The left side of the interval over which you plot.
    right -    The right side of the interval over which you plot.
    color -    The color you want to use.
    """
    fig, ax = plt.subplots()
    xs = np.linspace(left, right, 100)
    ax.plot(xs, f(xs), color=color)
    ax.set_xlabel('$x$')
    ax.set_ylabel('$f(x)$')

Let’s try it out. First, notice that you can call it by just providing a function:

plot_func(square)
../_images/python-functions_46_0.svg

Let’s call it with another function as well:

cube = lambda x: x ** 3

plot_func(cube)
../_images/python-functions_48_0.svg

Notice the default plotting range was used [left, right] = [0, 1] and the color was also the default one (red). Let’s change the left side of the interval to -1 and the color to blue:

plot_func(cube, left=-1, color='blue')
../_images/python-functions_50_0.svg

Notice that the order of the so-called “keyword arguments” does not matter. The following produces the same result:

plot_func(cube, color='blue', left=-1)
../_images/python-functions_52_0.svg

But you cannot provide a keyword argument before an regular one. The following results in an error:

plot_func(color='blue', cube, left=-1)
  File "<ipython-input-43-828534a920ca>", line 1
    plot_func(color='blue', cube, left=-1)
                            ^
SyntaxError: positional argument follows keyword argument

Again, do not panic when you see an error like this! Read it. Doesn’t it make sense? So, remember that regular arguments (otherwise known as positional arguments) must be before the keyword arguments.

Questions

  • Extend plot_func so that you can also change the style with which it plots a function.

# your code here