1.4.2. Control Flows#

Python usual control flows of traditional language, with light variation.
In this section, we introduce the control flows used by Python for conditions, looping, and functions.

1.4.2.1. if Statements#

The if statement allow execution of a code section when a condition is met.

from random import randint

x: int = randint(-10, 10)

if x < 0:
    print(f"{x} is a negative number.")
elif x % 2 == 0:
    print(f"{x} is even.")
else:
    print(f"{x} is odds.")
-10 is a negative number.

When a value is compared to several constants, the match statements might be more appropriate for comparison.

1.4.2.2. match Statements#

The match statement compares a value with consecutive constant. Upon the first case matching, the case’s code branch is executed.
Note: the variable name _ act as a wildcard and can always be executed. If no patterns match, none of the branches are executed.

import math
from random import randint

point: tuple[int, int] = (randint(-2, 2), randint(-2, 2))
print(f"{point=}")

match point:
    case (0, 0):
        print("Distance from origin: 0")
    case (0, x) | (x, 0):
        print(f"Distance from origin: {x}")
    case (x, y):
        print(f"Distance from origin: {math.dist((x, y), (0, 0))}")
    case _:
        raise ValueError("Not a point")
point=(1, 2)
Distance from origin: 2.23606797749979

1.4.2.3. while and for Statements#

The while statement iterates as long as its condition evaluation is truthy.

# Fibonacci series
# The sum of two elements defines the next
a: int = 0
b: int = 1
while a < 10:
    print(a)
    a, b = b, a+b
0
1
1
2
3
5
8

The for statement iterates over a sequence of elements in the order they appear.

words: list[str] = ["banana", "apple", "kiwi"]

for word in words:
    print(word, len(word))
banana 6
apple 5
kiwi 4

1.4.2.3.1. The range() function#

To iterate over a sequence of numbers, the range functions is useful.

for n in range(5):
    print(n)
0
1
2
3
4

1.4.2.3.2. break and continue Statements, and else Clauses on Loops#

The break statement, breaks out the innermost for or while loop.
The else clause on loops executes when the sequence is exhausted (with for) or the condition becomes false (with while), but not when a break statement terminates the loop.

for n in range(2, 10):
    for x in range(2, n):
        if n % x == 0:
            print(n, "equals", x, "*", n//x)
            break
    else:
        print(n, "is a prime number")
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3

The continue statement goes directly to the next iteration of the loop.

for n in range(2, 10):
    if n % 2 == 0:
        print(f"{n} is an even number.")
        continue
    print(f"{n} is an odd number.")
2 is an even number.
3 is an odd number.
4 is an even number.
5 is an odd number.
6 is an even number.
7 is an odd number.
8 is an even number.
9 is an odd number.

1.4.2.4. pass Statements#

The pass statement does nothing. It can be used when the syntax requires a statement, but no action is required.
This is often used to implement minimal classes.

class TargetNotFound(Exception):
    pass

1.4.2.5. Defining Functions with def Statements#

A Python function is defined with a name, and optionally parameters and return statements.
Note: When the return statement is omitted, the None value is returned.

def fib_print(n: int) -> None:
    """Print the Fibonacci series up to `n`. 

    Parameters
    ----------
    n : int
        Maximum value for the Fibonacci series.
    """
    a: int = 0
    b: int = 1
    while a < n:
        print(a, end=" ")
        a, b = b, a+b
    print()

fib_print(2000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 
def fib(n: int) -> list[int]:
    """Return the Fibonacci series up to `n`. 

    Parameters
    ----------
    n : int
        Maximum value for the Fibonacci series.

    Returns
    -------
    series: list[int]
        Fibonacci series.
    """
    series: list[int] = []

    a: int = 0
    b: int = 1
    while a < n:
        series.append(a)
        a, b = b, a+b
    
    return series

fib(2000)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]

1.4.2.5.1. Unpacking arguments#

Sequence can be unpacked into multiple variable. This can come in handy to:

  • Pass a list of arguments to a function

  • Assign function return values to multiple variables

import math
from random import uniform

def rotation(x: float, y: float, r: float) -> tuple[float, float]:
    """Rotate a pair of coordinate in Euclidean space by a `r` factor.

    Parameters
    ----------
    x : float
        x coordinate.
    y : float
        y coordinate.
    r : float

    Returns
    -------
    (x_, y_) : tuple[float, float]
        Rotated coordinates.
    """
    x_ = x*math.cos(r) - y*math.sin(r)
    y_ = y*math.sin(r) + x*math.cos(r)
    return x_, y_

point: tuple[float, float] = (uniform(-1, 1), uniform(-1, 1))
print(f"Original: {point=}")

# Unpack the value of point with `*`.
# Then the function return values are unpacked into x and y respectively.
x_, y_ = rotation(*point, r=90)

point_: tuple[float, float] = (x_, y_)
print(f"Rotated: {point_=}")
Original: point=(0.2993129412057509, -0.2851203460877807)
Rotated: point_=(0.12078240620679381, -0.38901087004743085)

1.4.2.5.2. More on function parameters#

A function can define its parameters with default values or to be either:

  • Positional only

  • Positional or keyword

  • Keyword only

Example:

def f(pos1, pos2, /, pos_or_kwd, *, kwd1, kwd2):
      -----------    ----------     ----------
        |             |                  |
        |        Positional or keyword   |
        |                                - Keyword only
         -- Positional only

As guidance:

  • Use positional-only if you want the name of the parameters to not be available to the user. This is useful when parameter names have no real meaning, if you want to enforce the order of the arguments when the function is called or if you need to take some positional parameters and arbitrary keywords.

  • Use keyword-only when names have meaning and the function definition is more understandable by being explicit with names or you want to prevent users relying on the position of the argument being passed.

  • For an API, use positional-only to prevent breaking API changes if the parameter’s name is modified in the future.

from random import randint
from typing import TypeVar

T = TypeVar("T")

def sample(
    l: list[T],
    /, 
    n: int,
    *,
    resample: bool = False,
) -> list[T]:
    """Sample `n` elements from a list.

    Notes
    -----
    When `resample` is False and the element to sample is greater than the number of element in the list, the values of the list are returned in random order. That is, we cannot sample more elements that the list has.

    Parameters
    ----------
    l : list[T]
        List of elements to sample from.
    n : int
        Number of elements to sample.
    resample : bool, optional
        Whether resampling is allowed, by default False.

    Returns
    -------
    rv: list[T]
        Sample of elements.
    """

    rv: list[T] = []
    l_: list[T] = l.copy()

    # Limit max samples when resampling is disabled.
    if not resample:
        n = min(len(l_), n)

    for _ in range(n):
        index: int = randint(0, len(l_) - 1)
        rv.append(l_[index])
        if not resample:
            del l_[index]

    return rv


arr: list = [1, 2, 3, 4, 5]
sample(arr, 7, resample=False)
[5, 3, 1, 2, 4]

1.4.2.5.3. Arbitrary arguments#

A function can define a parameter *args wrapping subsequent arguments in a tuple. Parameters occurring after the *args parameter must be keyword-only arguments.
Similarly, a function can define a keyword-only parameter **kwargs wrapping subsequent arguments in a dict. Not other parameters can follow the **kwargs parameter.

def concat(*args: tuple[str], sep="/") -> str:
    """Concatenate a sequence of string.

    Parameters
    ----------
    sep : str, optional
        Separator to use when concatenating, by default "/"

    Returns
    -------
    str
        Concatenated string.
    """
    return sep.join(args)

path: list[str] = ["/usr", "bin", "python"]
print(concat(*path))
/usr/bin/python

1.4.2.5.4. lambda Statements#

Anonymous functions can be declared using the lambda keyword. They are restricted to a single expression.

fma = lambda a, b, c: a*b + c

print(fma(1, 2, 3))
5

It is often used to pass a one-time use function as an argument.

# Sort list of point by distance to origin
import math

points: list[tuple[int, int]] = [(1, 1), (-1, 3), (3, 1), (2, 1)]
points.sort(
    key=lambda point: math.dist(point, (0,)*len(point))
    )
print(points)
[(1, 1), (2, 1), (-1, 3), (3, 1)]