{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "(chpt-functions)=\n", "# Functions\n", "\n", "Leverage the power of code recycling with functions. For interactive reading and executing code blocks [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/hydro-informatics/jupyter-python-course/main) and find *b04-pyfun.ipynb*, or install {ref}`Python ` and {ref}`JupyterLab ` locally.\n", "\n", "```{admonition} Requirements\n", "Make sure to understand data types and loops introduced the section on {ref}`Loops and Conditional Statements `.\n", "```\n", "\n", "```{admonition} Watch this section as a video\n", ":class: tip, dropdown\n", "\n", "

Watch this section as a video on the @Hydro-Morphodynamics channel on YouTube.

\n", "```\n", "\n", "## What are functions?\n", "\n", "Functions are a convenient way to divide code into handy, reusable, and better readable blocks, which help to structure code. Function blocks can accept parametric arguments and are reusable. Thus, functions are a key element for sharing code and working in teams. The basic structure of a Python function involves:\n", "\n", "* A `def` keyword followed by a function name with *arguments* in parentheses and a code block.\n", "* The type of *arguments* that a function can receive are:\n", " - Required arguments: `arg`\n", " - Default keyword arguments (with default values): `arg=value`\n", " - Optional arguments: `*args`\n", " - Optional keyword arguments: `**kwargs`\n", "\n", "Using optional (keyword) arguments makes functions more robust and flexible. The code block of a function is indented, similar to loops: " ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "def my_function(argument1, *args, **kwargs):\n", " something = [DO-SOMETHING-WITH-argument1-*args-**kwargs] \n", " return something" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## A Basic Example\n", "\n", "Three countries on Earth use imperial units, while most other countries use the *Système International* (*French: International System*) of units (SI units). Let's write a simple function to help imperial unit users convert *feet* (imperial) to *meters* (SI).\n", "\n", "In the following example, the function name is `feet_to_meter` and the function accepts one argument, which is `feet`. The function returns the `feet` argument multiplied with a `conversion_factor` of 0.3048, which corresponds to the conversion factor from feet to meters. In this simple example, the `conversion_factor` variable cannot be modified externally and only exists in the *namespace* of the function.\n", "\n", "```{note}\n", "Internal variables (i.e., variables defined within a function), such as `conversion_factor`, are not accessible outside (the namespace) of the function.\n", "```" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "def feet_to_meter(feet):\n", " conversion_factor = 0.3048\n", " return conversion_factor * feet" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Function Calls\n", "\n", "To call a function, it must be defined before the call. The function may be defined in the same script or in another script, which can then be imported as a module ({ref}`read more about modules and packages in the next section `). Then we can call, for example, the above-defined `feet_to_meter` function as follows:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "10 feet are 3.048 meters.\n" ] } ], "source": [ "feet_value = 10\n", "print(\"{0} feet are {1} meters.\".format(feet_value, feet_to_meter(feet_value)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Optional Arguments *args\n", "\n", "Replacing the non-optional `feet` argument in the above function with an optional argument `*args` enables the conversion of as many length values as the function receives. The following lines explain step-by-step how that works.\n", "\n", "1. Make sure that anyone understands the input and output parameters of the function by adding inline *docstrings* with a pair of triple double-apostrophes (`\"\"\"`) that embraces input parameters (`:params parameter_name: definition`) and the function return (`:output: definition`).\n", "1. By default, we will assume that multiple values are provided. Therefore, a list called `value_list` is intantiated at the beginning of the function, while `conversion_factor` remains the same as before.\n", "1. A for-loop over `*args` identifies and processes the arguments provided. Why a for-loop?
Python recognizes `*args` automatically as a list, and therefore, we can iterate over `*args`, even though the provided range of values was not a list type.\n", "1. The for-loop in the `try` code block includes a `try` - `except` statement to verify if the provided values (arguments) are numeric and can be converted to meters. If the `try` block runs successfully, the expression `arg * conversion_factor` appends the converted argument `arg` to `value_list`.\n", "1. Eventually, the `return` keyword returns the value list." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "def feet_to_meter(*args):\n", " \"\"\" \n", " :param *args: numeric values in feet\n", " :output: returns list of values in meter\n", " \"\"\"\n", " value_list = []\n", " conversion_factor = 0.3048\n", " for arg in args:\n", " try:\n", " value_list.append(arg * conversion_factor)\n", " except TypeError:\n", " print(str(arg) + \" is not a number.\")\n", " return value_list" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With the newly defined and more flexible function, we can now call `feet_to_meter` with as many arguments as needed:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Function call with 3 values: \n", "[0.9144000000000001, 0.3048, 3.048]\n", "Function call with no value: \n", "[]\n", "Function call with non-numeric values:\n", "just is not a number.\n", "words is not a number.\n", "[]\n", "Function call with mixed numeric and non-numeric values:\n", "just is not a number.\n", "words is not a number.\n", "[0.6096]\n" ] } ], "source": [ "print(\"Function call with 3 values: \")\n", "print(feet_to_meter(3, 1, 10))\n", "\n", "print(\"Function call with no value: \")\n", "print(feet_to_meter())\n", "\n", "print(\"Function call with non-numeric values:\")\n", "print(feet_to_meter(\"just\", \"words\"))\n", "\n", "print(\"Function call with mixed numeric and non-numeric values:\")\n", "print(feet_to_meter(\"just\", \"words\", 2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(kwargs)=\n", "## Optional Keyword Arguments **kwargs\n", "\n", "\n", "In the last paragraphs, we made the `feet_to_meter` function more flexible so that it can now receive as many arguments as needed. Until now, the internal `conversion_factor` variable cannot be modified from outside of the function with little flexibility. For instance, imagine we are writing this function for a historian. In the past, imperial units were widespread in many cultures (e.g., Greek, Roman, or Chinese) with varying length definitions between 0.250 m and 0.335 m. That means the historian will need flexibility regarding the conversion factor, while we still want to use 0.3048 m as the default value. This requirement can be implemented with optional keyword arguments `**kwargs` and this is how it works in the code block below:\n", "\n", "1. Add `**kwargs` after `*args` in the function `def` parentheses (the order of `*args, **kwargs` is important).\n", "1. Keep `conversion_factor = 0.3048` as the default value (we want the function to be functional also without any keyword argument provided).\n", "1. Similar to the `*args` statement, Python automatically identifies variables beginning with `**` as optional keyword arguments (actually, the name *args* and *kwargs* does not matter - the `*` signs are important). The difference to `*args` is that Python identifies `**kwargs` as a dictionary.\n", "1. A for-loop iterates over the *kwargs*-dictionary and the `if` statement identifies any optional keyword argument that contains the string `\"conv\"` as conversion_factor.\n", "1. A `try`- `except` statement tests if the provided value for the keyword argument is numeric by attempting a conversion to `float()`.\n", "\n", "The rest of the function remains unchanged." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "def feet_to_meter(*args, **kwargs):\n", " \"\"\" \n", " :param *args: numeric values in feet\n", " :output: returns list of values in meter\n", " \"\"\"\n", " value_list = []\n", " conversion_factor = 0.3048\n", " for k in kwargs.items():\n", " if \"conv\" in k[0]:\n", " try:\n", " conversion_factor = float(k[1])\n", " print(\"Using conversion factor = \" + str(k[1]))\n", " except:\n", " print(str(k[1]) + \" is not a number (using default value 0.3048).\") \n", " \n", " for arg in args:\n", " try:\n", " value_list.append(arg * conversion_factor)\n", " except TypeError:\n", " print(str(arg) + \" is not a number.\")\n", " return value_list" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Test different conversion factors with the newly defined flexibility of the `feet_to_meter` function:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Function call with 3 values and a conversion factor of 0.25: \n", "Using conversion factor = 0.25\n", "[0.75, 0.25, 2.5]\n", "Function call with 3 values and a conversion factor of 1/7 with slightly different name: \n", "Using conversion factor = 0.14285714285714285\n", "[0.42857142857142855, 0.14285714285714285, 1.4285714285714284]\n", "Function call with 2 values with default conversion factor: \n", "[7.62, 3.048]\n" ] } ], "source": [ "print(\"Function call with 3 values and a conversion factor of 0.25: \")\n", "print(feet_to_meter(3, 1, 10, conv_factor=0.25))\n", "\n", "print(\"Function call with 3 values and a conversion factor of 1/7 with slightly different name: \")\n", "print(feet_to_meter(3, 1, 10, conversion_factor=1/7))\n", "\n", "print(\"Function call with 2 values with default conversion factor: \")\n", "print(feet_to_meter(25, 10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Default Keyword Arguments\n", "\n", "Keyword arguments can also be defined by default. The below example shows how the `conversion_factor` can be default-defined in the `def` function parentheses. Note that `conversion_factor` must be defined after any optional arguments `*args`." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def feet_to_meter(*args, conversion_factor=0.3048):\n", " \"\"\" \n", " :param *args: numeric values in feet\n", " :output: returns list of values in meter\n", " \"\"\"\n", " value_list = []\n", " \n", " for arg in args:\n", " try:\n", " value_list.append(arg * conversion_factor)\n", " except TypeError:\n", " print(str(arg) + \" is not a number.\")\n", " return value_list" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now we can use `feet_to_meter` with or without or with a conversion factor and after a list of values:" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Function call with a conversion factor of 0.313 and two values: \n", "[0.313, 3.13]\n", "Function call with 3 values without any conversion factor: \n", "[0.9144000000000001, 0.3048, 3.048]\n" ] } ], "source": [ "print(\"Function call with a conversion factor of 0.313 and two values: \")\n", "print(feet_to_meter(1, 10, conversion_factor=0.313))\n", " \n", "print(\"Function call with 3 values without any conversion factor: \")\n", "print(feet_to_meter(3, 1, 10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(wrappers)=\n", "## Function Wrappers and Decorators \n", "\n", "If multiple functions contain similar lines, chances are that those functions can be further factorized by using function wrappers and decorators. A typical example is a license checkout (e.g. to use a commercial Python module/package, such as Esri's `arcpy`) or if we want to use a recurring error statement with `try` - `except` statements. \n", "\n", "For instance, consider two or more functions that should receive, process, and produce numerical output from user input. These functions may look like this:" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "def multiply_arguments(*args):\n", " result = 1.0\n", " try:\n", " for arg in args:\n", " result *= arg\n", " print(\"The result is: \" + str(result))\n", " except TypeError:\n", " print(\"ERROR: The calculation could not be performed failed (input arguments: %s)\" % \", \".join(args))\n", " except ValueError:\n", " print(\"ERROR: The calculation could not be performed failed (input arguments: %s)\" % \", \".join(args))\n", " return result\n", "\n", "def sum_up_arguments(*args):\n", " result = 0.0\n", " try:\n", " for arg in args:\n", " result += arg\n", " except TypeError:\n", " print(\"ERROR: The calculation could not be performed failed (input arguments: %s)\" % \", \".join(args))\n", " except ValueError:\n", " print(\"ERROR: The calculation could not be performed failed (input arguments: %s)\" % \", \".join(args)) \n", " return result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Both functions involve the statement `print(\"The result is: \" + str(result))` to print the results to the Python console (e.g., to ensure get some intermediate information) and to run only on valid (i.e., numeric) input with the help of exception (`try` - `except`) statements. However, we want our functions to focus on the calculation only and this is where a wrapper function helps.\n", "\n", "A wrapper function can be defined by first defining a standard function (e.g., `def verify_result`) and then passing another function (`func`) as an argument. In this function (`verify_result`), we can then place a nested `def wrapper()` function that will embrace `func`. It is important to use both optional `*args` and optional keyword `**kwargs` in the wrapper function and the call to `func` to make the wrapper as flexible as possible." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "def verify_result(func):\n", " def wrapper(*args, **kwargs):\n", " try:\n", " result = func(*args, **kwargs)\n", " print(\"Success. The result is %1.3f.\" % float(result))\n", " return result\n", " except TypeError:\n", " print(\"ERROR: The calculation could not be performed because of at least one non-numeric input (input arguments: %s)\" % str(args))\n", " return 0.0\n", " except ValueError:\n", " print(\"ERROR: The calculation could not be performed because of non-nmumeric input (input arguments: %s)\" % str(args))\n", " return 0.0\n", " return wrapper" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Now, we can use an `@`-decorator to wrap the above math functions in the `verify_result(fun)` function. When Python reads the beautiful, code-decorating `@` sign, it automatically looks for the wrapper function defined after the `@` sign to wrap the following function." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "@verify_result\n", "def multiply_arguments(*args):\n", " result = 1.0\n", " for arg in args:\n", " result *= arg\n", " return result\n", "\n", "@verify_result\n", "def sum_up_arguments(*args):\n", " result = 0.0\n", " for arg in args:\n", " result += arg\n", " return result" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The two functions (`multiply_arguments` and `sum_up_arguments`) can be called as usual, for example:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Success. The result is 12.000.\n", "ERROR: The calculation could not be performed because of at least one non-numeric input (input arguments: (3, 4, 'not a number'))\n", "Success. The result is 7.000.\n", "ERROR: The calculation could not be performed because of at least one non-numeric input (input arguments: ('absolutely', 'no', 'valid', 'input'))\n" ] }, { "data": { "text/plain": [ "0.0" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "multiply_arguments(3, 4)\n", "multiply_arguments(3, 4, \"not a number\")\n", "sum_up_arguments(3, 4)\n", "sum_up_arguments(\"absolutely\", \"no\", \"valid\", \"input\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The above wrapper function returns the wrapped function results, too. However, to use built-in function attributes (e.g., the function's name with `__name__`, the function's docstring with `__doc__`, or the module in which the function is defined with `__module__`) outside of the wrapper, we need the wrapper function to return the wrapped (decorated) function itself. This can be done as follows:" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [], "source": [ "def error_func(*args, **kwargs):\n", " return 0.0\n", "\n", "def verify_result(func):\n", " def wrapper(*args, **kwargs):\n", " try:\n", " return func(*args, **kwargs)\n", " except TypeError:\n", " print(\"ERROR: The calculation could not be performed because of at least one non-numeric input (input arguments: %s)\" % str(args))\n", " return error_func(*args, **kwargs)\n", " except ValueError:\n", " print(\"ERROR: The calculation could not be performed because of non-nmumeric input (input arguments: %s)\" % str(args))\n", " return error_func(*args, **kwargs)\n", " return wrapper" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note the difference: the `wrapper` function now returns `func(*arg, **kwargs)` instead of the numeric variables as result. If the function cannot be executed because of invalid input, the wrapper will return an error function (`error_func`), which ensures the consistency of the wrapper function. One may think that the error function returning 0.0 is obsolete because the exception statements could directly return 0.0. However, 0.0 is a *float* variable, while `error_func` is a function and the function wrapper should always return the same data type, regardless of an exception raise (error) or a successful execution. This is what makes code consistent.\n", "\n", "This paragraph showed examples of using the decorators in the shape of an `@` sign to wrap (embrace) a function. Decorators are also a useful feature in Python classes, for example, when a class function returns static values. Read more about decorators in classes later in the chapter on {ref}`object orientation and classes `." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Iterators and Generators\n", "\n", "A characteristic of *list*, *tuple*, and *dictionary* data types is their iterability, which is provided by their `__iter__` built-in method. Thus, iterability is the reason why we can write:" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1\n", "2\n", "3\n" ] } ], "source": [ "for e in [1, 2, 3]: print(e)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Besides iterations, Python also enables the creation of generators (i.e., generator functions). Instead of using a `return` statement, a generator function ends with a `yield` statement, that returns data as long as a `next()` function (inherent step in iterations) is called. An application of a generator is, for example, the flattening of nested lists (i.e., remove sub-lists and write all variables directly into a non-nested list):" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[1, 2, 3, 'a', 'b', 'c']\n" ] } ], "source": [ "from collections.abc import Iterable\n", "\n", "def flatten(nested_list):\n", " for e in nested_list:\n", " if isinstance(e, Iterable) and not isinstance(e, str):\n", " for x in flatten(e):\n", " yield x\n", " else:\n", " yield e\n", " \n", "a_nested_list = [[1, 2, 3], [\"a\", \"b\", \"c\"]]\n", "flattened_list = list(flatten(a_nested_list))\n", "print(flattened_list)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{note}\n", "The above example uses `Iterable` from the standard module `collections.abc`. More about importing packages and modules is discussed in the [Modules & Packages](pypckg) section.\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "(lambda)=\n", "## Lambda Functions\n", "\n", "[Lambda (*λ*) calculus](https://en.wikipedia.org/wiki/Lambda_calculus) is a formal language for expressing computation-based function abstraction and was introduced in the 1930s by Alonzo Church and Stephen Cole Kleene. Lambda functions originate from functional programming and represent short, anonymous (i.e, without a name) functions. Although Python is not inherently a functional programming language, functional concepts were implemented early in Python, for example with the `map()`, `filter()`, and deprecated `reduce()` functions and also the `lambda` operator. \n", "\n", "In Python, an anonymous (nameless) lambda function can take any number of arguments, but can only have one expression. The arguments consist of a comma-separated list of variables and the expression uses these arguments. The **syntax** of `lambda` functions is:\n", "\n", "`lambda arguments : expression`\n", "\n", "The following example illustrates a `lambda` function with one argument and adds 1 to the argument:" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "2\n" ] } ], "source": [ "add_one = lambda number : number + 1\n", "print(add_one(1))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "That was nice but quite useless. Here is an example of a slightly more useful lambda function that sums up two input arguments:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "6\n" ] } ], "source": [ "sum_up = lambda x, y : x + y\n", "print(sum_up(1, 5))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The above-shown function for converting feet to meters can also be written as a lambda function:" ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "3.048\n" ] } ], "source": [ "feet_to_meter = lambda ft_value : ft_value * 0.3048\n", "print(feet_to_meter(10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using a `lambda` function made the code shorter and more efficient. In addition, to evaluate the `feet_to_meter` `lambda` function for multiple values, we can use the `map()` function. The syntax of a `map()` function is: \n", "\n", "`result = map(function, sequence)`\n", "\n", "where `sequence` can be a *list* or a *tuple*. Thus, to evaluate a *tuple* of four values, we can write:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[1.2192, 2.95656, 2.1336, 0.6096]\n" ] } ], "source": [ "four_ft_values = (4, 9.7, 7, 2)\n", "print(list(map(feet_to_meter, four_ft_values)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `print` statement converts the `map()` output into a *list* to evaluate the `map()` function (otherwise, the result would be something like ``).\n", "\n", "If the `feet_to_meter` function is not needed at another place in the code, one can also write:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[1.2192, 2.95656, 2.1336, 0.6096]\n" ] } ], "source": [ "print(list(map(lambda x : x * 0.3048, (4, 9.7, 7, 2))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Another feature of Python is the `filter(function, list)` function that represents an elegant solution to filter out those elements from a list for which the function returns `True`. The following code block illustrates a `filter` that eliminates all numbers from a `some_numbers` list, which can be divided by three. " ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[1, 2, 4, 5, 7, 8]\n" ] } ], "source": [ "some_numbers = list(range(1, 10))\n", "print(list(filter(lambda x: x % 3, some_numbers)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Formerly, the `reduce()` function for merging down list input into one value was implemented in Python. However, Python's original author *Guido van Rossum* successfully banned it from *Python3* ([read his post](https://www.artima.com/weblogs/viewpost.jsp?thread=98196)), which is also why it is not featured here." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "```{admonition} Exercise\n", "Get familiar with functions in the {ref}`Hydraulics (1d) ` exercise.\n", "```" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.6" } }, "nbformat": 4, "nbformat_minor": 4 }