NB: Introduction to Functions

Objectives - Explain the benefits of functions - Illustrate how to use built-in functions - Illustrate how to create and use your own (user-defined) functions - Demonstrate the scope and lifetime of a variable - Illustrate global and local nature of variables through functions - Demonstrate function parameter use - Provide recommendations on how to create and document functions - Show how to print and write docstrings

Concepts - functions - built-in functions - user-defined functions - variable scope - global versus local variables - default arguments - *args - function call - docstring

Introduction

A function is piece of source code, separate fom the larger program, that performs a specific task.

This section of code is given a name and can be called from the main program. It is called by using its given name.

Functions are the verbs of a programming language. They signify action, and take subjects and objects (as it were).

Functions take input and produce output.

  • Function inputs are called both parameters and arguments.
  • Outputs are called return values

Functions are always written with parentheses at the end of their names, e.g.

len(some_list)

Internally, they contain a block of code to do their work.

Often the producte a transformation … from simple to complex.

When you use a function, we say you call a function. Programmers speak of “function calls” and “callbacks”.

Benefits

Reduce complex tasks into simpler tasks.

Eliminate duplicate code – no need to re-write, reuse function as needed.

Code reuse. Once function is written, you can reuse it in any other program.

Distribute tasks to multiple programmers. For example, each function can be written by someone.

Hide implementation details, i.e. abstraction.

Increase code readability.

Improve debugging by improving traceability. Things are easier to follow; you can jump from function to function.

Built-in Functions

Python provides many built-in functions. See Python built-in functions.

We’ve looked at many of these already.

These are functions that are available to use any time your are running Python.

To take one simple example, this is a built-in function: bool().

Takes an argument \(x\) and returns a boolean value, i.e. True or False.

bool(0), bool(500)

Imported Functions

Python is meant to be a highly modular language.
It is not designed to have a lot of special purpose functions built into it.
These keeps it light and highly customizable.

Many functions (and other stuff) can be imported into a program to add to the functions that you can call in a script.

There are also many packages to bring in additional functions.

Packages and Libraries

User-Defined Functions

Python makes it easy for you to write your own functions. These are called user-defined functions.

Let’s write a function to compare the list against a threshold.

def vals_greater_than_or_equal_to_threshold(vals, thresh):
    '''
    This is the "docstring" of a function. It is optional but expected. It describes it's 
    purpose and the nature of the input and return values, as well as a sense of what it does.
    More elaborate information should appear in external documentation packages with the function.
    
    PURPOSE: Given a list of values, compare each value against a threshold
    
    INPUTS
    vals    list of ints or floats
    thresh  int or float
    
    OUTPUT
    bools  list of booleans
    '''
    
    bools = [val >= thresh for val in vals]
    
    return bools

Let’s break down the components

The function definition starts with def, followed by name, one or more arguments in parenthesis, and then a colon.

Next comes a docstring to provide information to users about how and why to use the function.

The function body follows.

:astly is a return statement

The function call allows for the function to be used.
It consists of function name and required arguments:

vals_greater_than_or_equal_to_threshold(arg1, arg2) where arg1, arg2 are arbitrary names.

About the docstring

A docstring m occurs as first statement in module, function, class, or method definition

Internally, it is saved in __doc__ attribute of the function object.

It needs to be indented.

It can be a single line or a multi-line string.

Let’s test our function

The function body used a list comprehension for the compare:

[val >= thresh for val in vals]

## validate that it works for ints

x = [3, 4]
thr = 4

vals_greater_than_or_equal_to_threshold(x, thr)
[False, True]
## validate that it works for floats

x = [3.0, 4.2]
thr = 4.2

vals_greater_than_or_equal_to_threshold(x, thr)
## vals_greater_than_or_equal_to_threshold("foo", "bar")

This gives correct results and does exactly what we want.

Users can print the docstring

print(vals_greater_than_or_equal_to_threshold.__doc__)

print the help

help(vals_greater_than_or_equal_to_threshold)
?vals_greater_than_or_equal_to_threshold

Let’s test our function

The function body used a list comprehension for the comparison:

[val >= thresh for val in vals]

## validate that it works for ints

x = [3, 4]
thr = 4

vals_greater_than_or_equal_to_threshold(x, thr)
## validate that it works for floats

x = [3.0, 4.2]
thr = 4.2

vals_greater_than_or_equal_to_threshold(x, thr)

This gives correct results and does exactly what we want.

Print the docstring

print(vals_greater_than_or_equal_to_threshold.__doc__)

Print the help

help(vals_greater_than_or_equal_to_threshold)

Use the ? prefix …

?vals_greater_than_or_equal_to_threshold

Passing Parameters

Functions need to be called with correct number of parameters.

This function requires two params, but the function call includes only one param.

def fcn_bad_args(x, y):
    return x + y
fcn_bad_args(10)
TypeError: fcn_bad_args() missing 1 required positional argument: 'y'

Parameter Order

When calling a function, parameter order matters.

def fcn_swapped_args(x, y):
    out = 5 * x + y
    return out
x = 1
y = 2
fcn_swapped_args(x, y)
7
fcn_swapped_args(y, x)
11

Generally it’s best to keep parameters in order.

You can swap the order by putting the parameter names in the function call.

fcn_swapped_args(y=y, x=x)

Weirdness Alert

Note that the same name can be used for the parameter names and the variables passed to them.

The names themselves have nothng to do with each other!

In other words, just because a function names an argument foo,
the variables passed to it don’t have to name foo or anything like it.
They can even be named the same thing – it does not matter.

Unpacking List-likes with *args

The * prefix operator can be passed to avoid specifying the arguments individually.

def show_arg_expansion(*models):
    
    print("models          :", models)
    print("input arg type  :",  type(models))
    print("input arg length:", len(models))
    print("-----------------------------")
    
    for mod in models:
        print(mod)    

We can pass a tuple of values to the function …

show_arg_expansion("logreg", "naive_bayes", "gbm")
models          : ('logreg', 'naive_bayes', 'gbm')
input arg type  : <class 'tuple'>
input arg length: 3
-----------------------------
logreg
naive_bayes
gbm

You can also pass a list to the function.

If you want the elements unpacked, put * before the list.

models = ["logreg", "naive_bayes", "gbm"]
show_arg_expansion(*models)
models          : ('logreg', 'naive_bayes', 'gbm')
input arg type  : <class 'tuple'>
input arg length: 3
-----------------------------
logreg
naive_bayes
gbm

This approach allows your function to accept an arbitrary number of arguments.

show_arg_expansion('a b c d e f g'.split())

The reverse is true, too.

You can use the * prefix to pass list-like objects to a function that specifies its arguments.

def arg_expansion_example(x, y):
    return x**y
my_args = [2, 8]
arg_expansion_example(*my_args)

But, the passed object must be the right length.

my_args2 = [2, 8, 5]
arg_expansion_example(*my_args2)
## **my_dict

Default Arguments

Use default arguments to set the value of arguments when left unspecified.

def show_results(precision, printing=True):
    precision = round(precision, 2)
    if printing:
      print('precision =', precision)
    return precision
pr = 0.912
res = show_results(pr)
precision = 0.91

The function call didn’t specify printing, so it defaulted to True.

NOTE: Default arguments must follow non-default arguments. This causes trouble:

def show_results(precision, printing=True, uhoh):
    precision = round(precision, 2)
    if printing:
      print('precision =', precision)
    return precision
SyntaxError: non-default argument follows default argument (<ipython-input-19-29f5905a75a5>, line 1)

Returning Values

Functions are not required to have return statement.

If there is no return statement, a function returns None.

Functions can return no value (None), one value, or many.

Many values are returned as a tuple.

Any Python object can be returned.

## returns None, and prints.

def fcn_nothing_to_return(x, y):
    out = 'nothing to see here!'
    print(out)
fcn_nothing_to_return(x, y)
nothing to see here!
r = fcn_nothing_to_return(1, 1)
print(r)
nothing to see here!
None
## returns three values

def negate_coords(x, y, z):
    return -x, -y, -z 
a, b, c = negate_coords(10, 20, 30)
print('a =', a)
print('b =', b)
print('c =', c)
a = -10
b = -20
c = -30
foo = negate_coords(10, 20, 30)
foo, len(foo)
((-10, -20, -30), 3)

If you don’t need an output, use the dummy variable _

d, e, _ = negate_coords(10,20,30)
print('d =', d)
print('e =', e)

Note: It’s generally a good idea to include return statements, even if not returning a value.

This shows that you did not forget to consider the return value.

You can use return or return None.

Functions can contain multiple return statements.

These may be used under different logical conditions.

def absolute_value(num):
    if num >= 0:
        return num
    return -num
absolute_value(-4)
absolute_value(4)

For non-negative values, the first return is reached.
For negative values, the second return is reached.

Function Design

A function is not just a bag of code!

Some good practices for creating and using functions:

  • design a function to do one thing

Make them as simple as possible, which makes them:

  • more comprehensible
  • easier to maintain
  • reusable

This helps avoid situations where a team has 20 variations of similar functions.

Give your function a good name.

  • It should reflect the action it performs.
  • Be consistent in your naming conventions.
  • A name like compute_variances_sort_save_print suggests the function is overworked!

If the function compute_variances also produces plots and updates variables, it will cause confusion.

Always give your function a docstring - Particularly important since indicating data types is not required.
- As a side note, you can include this information by using type annotation.

Finally, at some point you may be interested to learn some of the formatting languages that have been developed to write docstrings. See Lutz 2019 and this web page about Documenting Python Code for more info.