.. role:: python(code) :language: python .. index:: function scope generator .. `bank account function `_ Functions --------- Scopes ^^^^^^ A variable defined in a function is **local** to this function. Accessing it outside the function leads to :python:`NameError`. A nested function can see a variable in the **enclosing** scope function, but can only modify it if the variable is declared :python:`nonlocal` in the function, otherwise it leads to a :python:`UnboundLocalError`. Mutable parameters in the enclosing scope can be modified even without a :python:`nonlocal` declaration. Inputs ^^^^^^ To capture respectively all positional and keyword arguments: :python:`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. .. admonition:: Exercise (intermediate) 1. Create a **function** :python:`def create_bank_account() -> tuple[ Callable[[int], int], Callable[[], list[int]] ]` that returns two functions: + :python:`def transact(amount: int) -> int`, deposits or withdraws money to the balance and returns the new balance. + :python:`def get_transactions() -> list[int]`, returns a list of all transactions. 2. Test it. .. _`ex:bank_account`: .. collapse:: Correction 1. .. literalinclude:: ../../../../src/tools_methods/basics/functions.py :pyobject: create_bank_account :name: create_bank_account :linenos: :caption: src 2. .. literalinclude:: ../../../../tests/tools_methods/basics/test_functions.py :pyobject: test_transact :name: test_transact :linenos: :caption: tests .. literalinclude:: ../../../../tests/tools_methods/basics/test_functions.py :pyobject: test_account_independence :name: test_account_independence :linenos: :caption: tests Generator ^^^^^^^^^ The :python:`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 :python:`next()` or a :python:`for` loop), it resumes where it left off and continues until it hits another :python:`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 :python:`yield from` allows to yield from another generator or an iterable and can act as a shorthand for: .. code-block:: python yield from generator() # equivalent to for result in generator(): yield result A first very trivial example: .. literalinclude:: ../../../../src/tools_methods/basics/functions.py :pyobject: my_generator :name: my_generator :linenos: :caption: src .. literalinclude:: ../../../../src/tools_methods/basics/functions.py :pyobject: other_generator :name: other_generator :linenos: :caption: src .. literalinclude:: ../../../../src/tools_methods/basics/functions.py :pyobject: demo_generator :name: demo_generator :linenos: :caption: src An other example with an inifite generator of the Fibonnaci sequence: .. literalinclude:: ../../../../src/tools_methods/basics/functions.py :pyobject: yield_fibonacci :name: yield_fibonacci :linenos: :caption: src .. literalinclude:: ../../../../tests/tools_methods/basics/test_functions.py :pyobject: test_fibonnaci :name: test_fibonnaci :linenos: :caption: tests