More Control Flow Tools (Part II)

Today, we’re discussing sections 4.8-4.9 of The Python Tutorial. These are

Next month, we’ll cover section 5: Data Structures

Intro

The goal of this section is to define functions (sections of code) and run them later on. You should do this when

  • You repeat the same (or similar) code more than twice
  • You want to use the same code in multiple files
  • You want your code to be modular
  • You want to test sections of your code

Remember that in Python you create a section of code by indenting it.

# Outside any section

if True:
    ...
    # Inside a section
    # See? It's indented!

# Outside any section

4.8 Defining Functions

The numbers of the Fibonacci sequence are obtained by (starting with \(0\) and \(1\)) adding the previous two numbers together,

\[ F_n = F_{n-1} + F_{n-2} \] \[F = \{0, 1, 1, 2, 3, 5, 8, 13, 21, ...\]

We can use Python to construct the sequence up to some maximum \(F_{\mathrm{max}}:\)

F_max = 20
a, b = 0, 1

Fs = []

while a < F_max:
    Fs.append(a)
    a, b = b, a+b

print(Fs)
[0, 1, 1, 2, 3, 5, 8, 13]
Notemethods vs functions

Notice that Fs.append(...) is a bit odd - it’s a function attached to Fs. Functions that live inside objects are called methods.

Methods are common to all objects of a particular type. The append method exists inside any list, for example.

In a few months, when we look at classes, we’ll be able to unpack this further.

What if you want to compute this sequence to a different F_max? We could just change it. But in general, we could write a function that takes in F_max and spits out the Fibonacci sequence.

Functions work in two sections:

  • Definition: Where you define what it does

def function_name(input_1, input_2, ...):
    """Description"""
    body_of_function
    return something

  • Call: Where you use it

function_name(...)

Let’s turn our Fibonacci sequence into a function

def fib(F_max):
    a, b = 0, 1

    Fs = []

    while a < F_max:
        Fs.append(a)
        a, b = b, a+b

    return Fs

The definition doesn’t do anything. We have to call the function to use it:

fib(5)
[0, 1, 1, 2, 3]
fib(10)
[0, 1, 1, 2, 3, 5, 8]
fib(20)
[0, 1, 1, 2, 3, 5, 8, 13]

4.9 More on Defining Functions

Now that we’ve laid the groundwork for defining functions, we can look at some more advanced features:

  • 4.9.1 Default Argument Values
  • 4.9.2 Keyword Arguments
  • 4.9.3 Special parameters
  • 4.9.4 Arbitrary Argument Lists
  • 4.9.5 Unpacking Argument Lists
  • 4.9.6 Lambda Expressions
  • 4.9.7 Document Strings
  • 4.9.8 Function Annotations

There’s a lot to all of this, so we’ll just dip into what we can.

Let’s start by working from a simple function:

def add(x, y, z):
    return x + y + z

add(1,1,1)
3

4.9.1 Default Argument Values

We can specify default values for arguments in the function signature:

def add(x, y = 0, z = 0):
    return x + y + z

add(1, 1)
2

This means that x is required but y and z are optional.

Once you start specifying defaults, you can’t go back to required (“positional”) arguments:

4.9.2 Keyword Arguments

When we call functions, we can specify the arguments explicitly:

add(x = 1, y = 2, z = 3)
6

Because they’re unambiguous, we can provide them in any order

add(z = 3, x = 1, y = 2)
6

This is the only way to provide optional parameters out of order. For example, if we want to specify x and z but not y:

add(3, z = 5)
8

As with definitions, once you start keyword arguments you can’t go back to positional (because the order could have changed).

4.9.3 Special parameters

We can use the special parameters / and * to restrict the way that parameters can be used.

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only
  • Anything before / must be positional (using f(pos1=5) will fail).
  • Anything after * must be keyword
  • Anything in between can be either.

Let’s see this in action:

def add(x, /, y=0, *, z=0):
    return x + y + z
  • x must be positional. Any call to add() must start with x.
  • y can be either.
  • z must be keyword. Any call to add() with z must be specified with z=....
# y positional
add(1,2,z=3)
6
# y keyword, order changed
add(1, z=3, y=2)
6
# Error: x not positional
add(x = 1)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[36], line 2
      1 # Error: x not positional
----> 2 add(x = 1)

TypeError: add() got some positional-only arguments passed as keyword arguments: 'x'
# Error: z not keyword
add(1,2,3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[37], line 2
      1 # Error: z not keyword
----> 2 add(1,2,3)

TypeError: add() takes 2 positional arguments but 3 were given

4.9.4 Arbitrary Argument Lists

What if you want the user to specify an arbitrary number of arguments? For example, add any number of arguments?

You can specify one argument in the form *args, which contains ‘everything else’ (positional). It’s stored as a tuple (a list that cannot be edited):

def add(*summands):
    print(summands)
    return sum(summands)
    
add(1,2,3,6)
(1, 2, 3, 6)
12

A similar feature exists for keyword arguments: use **kwargs to capture ‘everything else’ (keyword)

4.9.5 Unpacking Argument Lists

You can go the other way too. If you have a list (or similar) of arguments, you can ‘unpack’ them as separate function inputs:

x = [1,2,3]
print(*x)
1 2 3

We can do the same with keyword arguments using ** and dictionaries, which we’ll cover next month.

4.9.6 Lambda Expressions

Lambda expressions create anonymous functions on-the-fly. They’re useful when you need to use a simple function as an input itself.

lambda input1, input2, ... : some_expression

sum_f = lambda a,b: a+b

sum_f(1,2)
3

4.9.7 Document Strings

In the first line of your function definition you can provide the docstring enclosed between triple-quotes. It’s a single- or multi-line string usually reserved for documenting function behaviour.

def add(*summands):
    """Add and print `summands`."""
    print(summands)
    return sum(summands)
add(1,2,3,4,5)
(1, 2, 3, 4, 5)
15
help(add)
Help on function add in module __main__:

add(*summands)
    Add and print `summands`

Some conventions (taken verbatim from TPT)

  • The first line should always be a short, concise summary of the object’s purpose. For brevity, it should not explicitly state the object’s name or type, since these are available by other means (except if the name happens to be a verb describing a function’s operation). This line should begin with a capital letter and end with a period.

  • If there are more lines in the documentation string, the second line should be blank, visually separating the summary from the rest of the description. The following lines should be one or more paragraphs describing the object’s calling conventions, its side effects, etc.

4.9.8 Function Annotations

These are a completely optional way for making it clear to programmers what your function inputs should be and what the function should output.

Warning

Function annotations are not enforced. Whether or not the annotation actually reflects what the code does is not checked and not reported.

Nevertheless, the annotations are useful because most IDEs (e.g. VSCode) will flag if you’ve disobeyed the annotation.

  • Specify input type with input: type = default (default optional)
  • Specify return type with def func_name(...) -> out_type:

For example,

def add(x: float, y: float = 0, z: float = 0) -> float:
    return x + y + z
# Type checking is NOT enforced!

add("a", "b", "c")
'abc'

Can specify multiple types with |

def add(x: float | int, y: float | int = 0, z: float | int = 0) -> float | int:
    return x + y + z

Documentation will reflect this

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

add(x: float | int, y: float | int = 0, z: float | int = 0) -> float | int