Functions

Scopes

A variable defined in a function is local to this function. Accessing it outside the function leads to NameError.

A nested function can see a variable in the enclosing scope function, but can only modify it if the variable is declared nonlocal in the function, otherwise it leads to a UnboundLocalError.

Mutable parameters in the enclosing scope can be modified even without a nonlocal declaration.

Inputs

To capture respectively all positional and keyword arguments: def func(*args, **kwargs).

As an object

A function can be handled as any other object. More specifically, it can be passed as an argument to an other function. An example is a function which searches for the minimum of a function will receive the function to minimize as an input parameter.

Exercise (intermediate)

  1. Create a function def create_bank_account() -> tuple[ Callable[[int], int], Callable[[], list[int]] ] that returns two functions:

    • def transact(amount: int) -> int, deposits or withdraws money to the balance and returns the new balance.

    • def get_transactions() -> list[int], returns a list of all transactions.

  1. Test it.

Correction
  1. src
     1def create_bank_account() -> tuple[Callable[[int], int], Callable[[], list[int]]]:
     2    """returns two callable, transact to modify the balance, get_transactions for history.
     3    illustrates the use of non local, scopes etc"""
     4    balance = 0
     5    transactions = []
     6
     7    def transact(amount: int) -> int:
     8        nonlocal balance
     9        balance += amount
    10        transactions.append(amount)  # mutable modified, not nonlocal
    11        return balance
    12
    13    def get_transactions() -> list[int]:
    14        return transactions
    15
    16    return transact, get_transactions
    
    1. tests
      1def test_transact() -> None:
      2    transact, get_transactions = create_bank_account()
      3    transactions: list[int] = [2, -1]
      4    obtained_balance: int = 0
      5    for transaction in transactions:
      6        obtained_balance = transact(transaction)
      7    expected_balance: int = 1
      8    assert obtained_balance == expected_balance
      9    assert transactions == get_transactions()
      
      tests
      1def test_account_independence() -> None:
      2    transact_bob, get_transactions_bob = create_bank_account()
      3    _, get_transactions_alice = create_bank_account()
      4    transact_bob(1)
      5    assert get_transactions_bob() == [1]
      6    assert get_transactions_alice() == []
      

Generator

The yield keyword is used to define a generator function. Unlike regular functions that return a value and exit, a generator pauses execution and remembers its state between calls.

Each time the generator is iterated (e.g., with next() or a for loop), it resumes where it left off and continues until it hits another yield or finishes.

The goals are:

  • Maintains internal state without needing a class.

  • Saves memory by generating values on the fly instead of storing them all at once.

The statement yield from allows to yield from another generator or an iterable and can act as a shorthand for:

yield from generator()
# equivalent to
for result in generator():
    yield result

A first very trivial example:

src
1def my_generator():
2    for ind in range(4):
3        yield ind**2
src
1def other_generator():
2    yield from [4, 5, 6]
src
 1def demo_generator() -> None:
 2    gen = my_generator()
 3    print(next(gen))
 4    print(next(gen))
 5    print(next(gen))
 6    print(next(gen))
 7    # print(next(gen)) # StopIterationError
 8    for value in my_generator():
 9        print(value)
10
11    values = list(my_generator())
12    print(values)
13
14    for value in other_generator():
15        print(value)

An other example with an inifite generator of the Fibonnaci sequence:

src
1def yield_fibonacci(maximum):
2    before_previous, previous = 0, 1
3    while True:
4        yield before_previous
5        if previous > maximum:
6            break
7        before_previous, previous = previous, before_previous + previous
tests
 1def test_fibonnaci() -> None:
 2    maximum = 5
 3    fibonnaci = yield_fibonacci(maximum)
 4    assert next(fibonnaci) == 0
 5    assert next(fibonnaci) == 1
 6    assert next(fibonnaci) == 1
 7    assert next(fibonnaci) == 2
 8    obtained = list(yield_fibonacci(maximum))
 9    expected = [0, 1, 1, 2, 3, 5]
10    assert obtained == expected