Functions

Indices and tables

Defining Functions

You can group code for reuse by creating functions.

Use def to create new functions. You need to provide the name of the function and a (optional) parenthesized list of formal parameters that the function will take in.

The code for the function (the suite) is subsequently indented to indicate it belongs to that function.

fndef ::=  “def” funcname ([parameter_list]) “:”
               suite

Tip

When stubbing out your code, you can use pass as the suite to defer writing the code until later.

Try it!

Enter the following in python/ipython/jupyter-notebook to define two separate functions:

>>> def say_hello():
...     pass
>>>
>>> def my_fn(a, b):
...     pass
>>>
>>> dir()

Calling Functions

Functions are called by placing () after their name.

Arguments to the function can be given in two ways:

  • By position, where the values are given in the same order as in the parameter list. In this case, the order of the arguments provided is important. For example:

    >>> def my_fn(a, b):
     ...     print("a={} : b={}".format(a, b)
    >>> my_fn(4, 5)
    a=4 : b=5
    
  • As keywords using the notation, parameter_name = value. In this case, the order of the arguments provided is not important. For example:

    >>> def my_fn(a, b):
     ...     print("a={} : b={}".format(a, b)
    >>> my_fn(b=5, a=4)
    a=4 : b=5
    

All positional arguments must come before any keywords arguments. Consequently, you cannot intermingle positional arguments with keywords arguments.

You can force the caller to use keyword arguments by insering a * in the parameter list. Any parameter after the * must be populated using keyword arguments. For example:

>>> def my_fn(a, *, b):
 ...     print("a={} : b={}".format(a, b)
>>> my_fn(4, 5)
TypeError: my_fn() takes 1 positional argument but 2 were given
>>> my_fn(4, b=5)
a=4 : b=5

Try it!

Enter the following in python/ipython/jupyter-notebook to define a function and call it:

>>> def my_fn(a, *, b):
...     print("a={} : b={}".format(a, b)
>>> my_fn(4, b=5)
a=4 : b=5

Default Parameter Values

A formal parameter can be made optional by giving it a default value in the parameter list. A default value is provided by using the pattern:

parameter = val

For example:

>>> def my_fn(a, b=23):
...     print("a={} : b={}".format(a, b))
>>> my_fun(54)
a=54 : b=23

In this example, a is required, but b is optional.

Note

When constructing the function’s parameter list, parameters without default values must come before parameters with default values.

Default values are evaluated once and that is done when the function is defined using the scope surrounding the function being defined (i.e. not every time the function is called).

Warning

A common mistake is to assign a mutable object as a default value to a parameter. Since the function definition is only evaluated once, this means, the same default value (i.e. object) is assigned to all invocations of the function. For mutable classes, this leads to a shared default value, which is likely undesireable. For example:

>>> def my_fn(a = []):
...     a.append('*')
...     print(a)
>>> my_fn()
['*']
>>> my_fn()
['*', '*']
>>> my_fn()
['*', '*', '*']

In these situations, assign None as the default value and use that to create the default mutable object within the function. For example:

>>> def my_fn(a = None):
...     if a is None:
...         a = []
...     a.append('*')
...     print(a)
>>> my_fn()
['*']
>>> my_fn()
['*']
>>> my_fn()
['*']

Try it!

Enter the following in python/ipython/jupyter-notebook to try calling a function with default parameter values:

>>> def my_fn(a, b=23):
...     print("a={} : b={}".format(a, b))
>>> my_fun(54)
>>> my_fun(54, 100)

Variable Parameter List

There are 2 special parameters that allow a function to take a variable number of parameters:

  • *args - A tuple scooping up the values for any remaining positional parameters after all formal positional parameters receive a value. The tuple is available inside the function by the name args. Anything after *args must be a keyword parameter or **kwargs.
  • **kwargs - A dict scooping up the value for any remaining keyword parameters after all formal keyword parameters recieve a value. The dict is available inside the function by the name kwargs.

Note

Any parameters after *args, or a single *, must be given values as keyword parameters at run-time.

Note

The actual name of *args and **kwargs doesn’t matter. You can call them anything you want. Like *gorf and **jarg. But, yeah, don’t. Don’t be that guy.

For example, consider the following function:

>>> def my_fn(a, *args, b, **kwargs):
...     print("a = {}".format(a))
...     if args:
...         print("args = {}".format(args))
...     print("b = {}".format(b))
...     if kwargs:
...         print("kwargs = {}".format(kwargs))

When called with only the required parameters a and b:

>>> my_fn(1, b=3)
a = 1
b = 3

When called with the required parameters and some extra parameters:

>>> my_fn(1, 99, 40, b=3, x=54, y=45, z=67)
a = 1
args = (99, 40)
b = 3
kwargs = {'x': 54, 'z': 67, 'y': 45}

Try it!

Enter the following in python/ipython/jupyter-notebook to try creating and calling a function with a variable parameter list:

>>> def my_fn(a, *args, b, **kwargs):
...     print("a = {}".format(a))
...     if args:
...         print("args = {}".format(args))
...     print("b = {}".format(b))
...     if kwargs:
...         print("kwargs = {}".format(kwargs))
>>> my_fn(1, b=3)
>>> my_fn(1, 99, 40, b=3, x=54, y=45, z=67)

Doc Strings

When defining a function, the first statement of the function body can be a string literal. Python does a few things with this literal if it is present:

  • It places it in the __doc__ member of the function.
  • It returns it as the description when you call help() on the function.

For example, consider the following function:

>>> def my_fn():
...     '''A test function
...
...     This function is just a test function.'''
...

Examining the __doc__ member by printing it, as in:

>>> print(my_fn.__doc__)

Will return:

A test function

This function is just a test function.

Getting help for the function returns:

>>> help(my_fn)

Will return:

Help on function my_fn in module __main__:

my_fn()
    A test function

    This function is just a test function.

There is a convention for writing doc strings which is as follows:

  • Use a triple quote string
  • The first line should be a short, concise summary of the functions purpose.
  • The second line should be blank.
  • The remaining lines should be a more verbose description of the function’s inputs/outputs and operation.

However, nothing enforces this convention.

Beyond just commenting the operation of the function and its IO, doc strings can also be parsed by external tools. It is common to see test scripts embedded in doc strings.

Try it!

Enter the following in python/ipython/jupyter-notebook to try creating and viewing doc strings for a function:

>>> def my_fn():
...     '''A test function
...
...     This function is just a test function.'''
...
>>> print(my_fn.__doc__)
>>> help(my_fn)

Returning Values

Functions can pass a value back to the caller using the return statement. For example:

>>> def mult(a, b):
...     return a*b
>>> mult(3, 4)
12

All functions return a value. Even if no explicit return is given, there is an implicit one that returns None (and is not normally displayed by the interpreter). For example:

>>> def append_to_list(a, b):
...     a.append(b)
>>> type(append_to_list(['cat'], 'dog'))
NoneType

Functions are objects. That means they can be passed around and assigned other names (i.e. aliases) just like other Python objects. For example:

>>> def mult(a, b):
...     return a*b
>>> x = mult
>>> x(5, 10)
50
>>> y = x
>>> y(36, 36)
1296
>>> id(mult)
140269203028032
>>> id(x)
140269203028032
>>> id(y)
140269203028032

Try it!

Enter the following in python/ipython/jupyter-notebook to try returning values from a function:

>>> def mult(a, b):
...     return a*b
>>> mult(3, 4)
>>> mult(12415213524513, 53514343141322425)

Did the last one scare you? Now that’s a big return value!

Function Annotations

Python allows you to optionally attach metadata to help describe the inputs and outputs of your function. This metadata is referred to as function annotations (see PEP 3107). Like doc strings, function annotations can also be used by external tools (for example, to do type checking).

For formal parameters, the annotation is created as follows:

param_annotation ::=  parameter : expression

For the return value, the annotation is created as follows:

return_annotation ::=  -> expression

The expression is usually the type name (like, int, or str). However, it can be anything that Python can evaluate.

For example:

>>> def my_fn(a:str, b:int) -> str:
...   return "{}->{}".format(a, b)
>>> my_fn('Guard', 7)
'Guard->7'

This metadata is stored in the __annotations__ member as a dictionary:

>>> my_fn.__annotations__
{'a': str, 'b': int, 'return': str}

Lambda Functions

Lambda functions are small anonymous functions (i.e. typically not assigned to a name).

They use a lightweight syntax that is a subset of the full function syntax:

  • They have a parameter list
  • The do not support function annotations
  • They can access variables from the surrounding scope
  • They are restricted to executing a single expression as part of their suite.
  • They cannot contain any statements

The general syntax is:

lambda_expr ::= : 'lambda' [parameter_list]: expression

For example:

>>> mult = lambda a,b : a*b
>>> mult(4, 5)
20

While the above example DOES assign the lambda to a name, it is not the typical use case.

It is more common that a lambda is created when a function being called requires a function object as one of it’s parameters. The map() method is one such method:

>>> list(map(lambda x: x + 100, [100, 500, 800]))
[200, 600, 900]

Try it!

Enter the following in python/ipython/jupyter-notebook to try creating and using a lambda function:

>>> mult = lambda a,b : a*b
>>> mult(4, 5)
>>> mult(12415213524513, 53514343141322425)