<jupyter_start><jupyter_text>Module 6: Functions and ModulesFebruary 26, 2021 Last time we talked about the implementation of repetitive behavior (loops). The lecture covered (pre-test) loops with the ```while``` and ```for``` statements, and the special behavior of the ```break``` and ```continue``` statements. We furthermore discussed the issue of post-test loops in Python.Today we will discuss functions, modules and packages, have a quick look at the Python’s standard library and the Python Package Index, and introduce the principle of recursion and recursive function definitions. When Python programs become complex, it is a good idea to structure the code using functions, modules, and packages. Proper modularization will keep the code base easier to understand and maintain, and is in fact one of the principles of good software development. Another important principle is the reuse of already existing functionality (rather than implementing it again), so familiarity with the standard libraries is important.Next time we will cover special data structures in Python, which will allow us to work with more powerful data items than just the individual numbers, strings and Booleans that we have used so far.  FunctionsWe have seen and used functions before (`print`, `input`, and `bin`, for example), now it is time to have a more systematic look at them and to define functions ourselves. Functions are useful whenever a piece of code forms a self-contained component and is (potentially) used repeatedly in the program. It simplifies coding, testing, debugging and maintenance if this code is only defined once (as a function) and just called when it is needed. Here is an example of code with unnecessary repetitions:<jupyter_code>player1 = input("Please enter name of player: ")
print(f"Hello {player1}, welcome to the game!")
player2 = input("Please enter name of player: ")
print(f"Hello {player2}, welcome to the game!")
player3 = input("Please enter name of player: ")
print(f"Hello {player3}, welcome to the game!")<jupyter_output>Please enter name of player:  a<jupyter_text>Essentially, this code does the same thing three times. This is impractical, for example, if we want to add more players (we have to duplicate a lot of code), or if we want to change the text of the outputs to the players, for instance translate it to Dutch (we would have to do that at all occurrences of the text). So, it might be useful to define functions that ask for a player's name and for displaying a personalized welcome message.Function definitions in Python have the following basic form:    def ():                return        That is, they define the name of the function, which parameters they take as input arguments at runtime (can be none), what they actually do and the value(s) that they return to the calling function (can be none). Functions have to be defined before they can be used. For example, we can define a function that takes a name as a parameter and welcomes a player accordingly:<jupyter_code>def greet_player(name):
    print(f"Hello {name}, welcome to the game!")
    
player1 = input("Please enter name of player: ")
greet_player(player1)
player2 = input("Please enter name of player: ")
greet_player(player2)
player3 = input("Please enter name of player: ")
greet_player(player3)<jupyter_output><empty_output><jupyter_text>This already looks better, but we still have redundant code for asking the players to enter their names. We can define another function for that, that has no input parameters, but returns a value to the caller:<jupyter_code>def ask_name():
    name = input("Please enter name of player: ")
    return name

def greet_player(name):
    print(f"Hello {name}, welcome to the game!")

player1 = ask_name()
greet_player(player1)
player2 = ask_name()
greet_player(player2)
player3 = ask_name()
greet_player(player3)<jupyter_output>Please enter name of player:  a<jupyter_text>Note that one of our functions contains a return statement (```return name```), and the other does not. When a function does not explicitly define a return value, the ```None``` value (representing “nothingness”) is returned by default. Thus, the return statement can be omitted, as implicitly an (empty) return to the caller will still happen at the end of the function. A return can however also be used to return to the caller at any another point of the function if desired.  Alternatively, if we think that we will never need the actions separately, we could simply define one function that carries out the input and the printout:<jupyter_code>def ask_name_and_greet_player():
    name = input("Please enter name of player: ")
    print(f"Hello {name}, welcome to the game!")
    return name

player1 = ask_name_and_greet_player()
player2 = ask_name_and_greet_player()
player3 = ask_name_and_greet_player()<jupyter_output><empty_output><jupyter_text>The functions in the example above are quite short, only one or two lines, and you might wonder if that is actually worth the effort. Well, they were only simple examples to illustrate how functions work. You will soon see in the homework exercises and also in your projects that functions are usually longer and more complex. Below is another example (still small), where the function comprises five lines of code and even a loop:<jupyter_code># function for computing the factorial of a number n
def fac(n):
    fac = 1
    while n > 0:
        fac = fac * n
        n = n - 1
    return fac

# reading a number from input
n = int(input("Please enter an integer number: "))

# loop computing the factorials for all number from n to 1
while n > 0:
    print(f"{n}! is {fac(n)}")
    n = n - 1<jupyter_output>Please enter an integer number:  12<jupyter_text>This example illustrates another feature of functions: variable names are local to the function. That is, argument names and variables declared inside a function are not visible outside of the function. The area in a program where variables are visible is also called their scope. As a general rule, a variable’s scope is the block that it has been declared in, starting from the point where its name was defined. In the example above, this means that the ```n``` in the ```fac``` function is another ```n``` than the one in the program below the function. The function does not change the value of the n in the calling (part of the) program. This behavior makes a lot of sense sense: If you use a function from a library, where you cannot or do not want to look into its implementation, it is good to know that it will not interfere with your own variables. Generally, data should only be passed to and retrieved from functions through their parameters and return values, respectively, as this is the safest way to avoid undesired side effects from e.g. variables that overwrite each other unintentionally.Note here that the `global` statement can be used to make variables known across scopes. Functions would have access to its value also without it being passed as a parameter, and they could write results to it without returning it explicitly. The use of the `global` statement is however strongly discouraged for the reasons given above, so we will not discuss it further.The functions in the examples above have no or only one parameter, but if several inputs are needed, they can simply be defined as comma-separated lists of parameters. For example, a function `add(a,b)` for adding to numbers. When calling the function, the arguments have to passed in the order of the parameters.Furthermore, functions can have named parameters. They are identified by their name, so their order does not matter when calling the function. They are also given a default value when they are declared, so that when the calling function does not specify them, there is a standard value to work with. Here is an example of a function with one “normal” and two named parameters with default values:<jupyter_code>def rectangle_print(letter, columns=3, rows=2):
    for i in range(1,rows+1):
        print(letter*columns)
        
rectangle_print("a")
rectangle_print("b", 5, 2)
rectangle_print("c", rows=3)<jupyter_output><empty_output><jupyter_text>The above example illustrates nicely how named parameters can or can not be defined by the caller, depending on what is needed. Note that also the `print` function, which we have used a lot, has named parameters, for example to specify the separator between the string arguments that are passed to print:<jupyter_code>print("a", "b", "c", sep="\n")<jupyter_output>a
b
c<jupyter_text>In fact, the arbitrary number of strings that can be passed to the print function to be printed to the screen is an example of yet another kind of parameters, the VarArgs (for variable number of arguments) parameters. Parameters become VarArgs by defining them with a single (\*) or double (\*\*) star in front of them. For example, we can use a starred parameter to define a sum function that adds all of its arguments:<jupyter_code>def sum_up(*numbers):
    s = 0
    for n in numbers:
        s += n
    return s

print(sum_up(1,2,3,4,5))
print(sum_up(1,2,3,4,5,6,7))<jupyter_output>15
28<jupyter_text>Technically, the arguments for a single-starred parameter form a tuple, and the arguments of a double-starred parameter form a dictionary, hence the latter have to be defined as key-value pairs. What this means will become clear in the next lecture, where we will talk about those data structures in detail.  ModulesFunctions make pieces of code in our program file reusable. Modules make it possible to reuse code across different program files. To use the content of another module (file) in a Python program, it has to be imported. We have already seen a module import in the  number-guessing game example in the lecture on loops:```import randomnumber = random.randint(1,10)[...]```The `import` statement in the first line imports the `random` module from Python’s standard library. Afterwards we can use the functions that it provides, for example the `random()` function that generates a random number within a specified range.Note that import statements can in principle be placed at any point in the program, as long as they happen before using their functions. This is not considered good coding style, however, as imports scattered all over the program make it difficult to see what has already been imported, and might even lead to redundant and conflicting imports. To avoid such problems, all imports that are used in a .py file should be placed at its beginning.We can also create modules ourselves. There are different forms in which modules can exist. The simplest is a .py file that contains different functions. (In fact, any .py file we create is a module that can potentially be used by other programs.) Generally, it makes sense to create modules for sets of functions that somehow belong together and where it would make sense to distribute them (to other programmers) as a unit. For example, we might provide the `ask_name()` and `greet_player()` functions from above in a `player_management.py` module, along with other related functions such as, e.g., `show_score()` or `show_game_over()`:    def ask_name():        name = input("Please enter name of player: ")        return name            def greet_player(name):        print(f"Hello {name}, welcome to the game!")            def show_score(name, score):        print(f"Hello {name}, your current score is {score}.")    def show_game_over(name):        print(f"GAME OVER! Sorry, {name}...")The module and its functions might then be used by a game as follows:<jupyter_code>import lib.player_management
import random

player = player_management.ask_name()
player_management.greet_player(player)
    
score = random.randint(0,10) # (replace by actual code for playing game)
    
if score > 0:
    player_management.show_score(player,score)
else:
    player_management.show_game_over(player)<jupyter_output>Please enter name of player:  aerew<jupyter_text>Also when loading modules the Python interpreter acts as usual and will step through the module when importing it. The function definitions in the module are read, and thus the functions are subsequently available for use. If the module contains other code, outside of function definitions, it will be executed, too.Note that when creating own modules, it is advisable to not use names that are already taken by modules from the standard library or other popular collections. Name clashes can lead to errors and unpredictable problem behaviour. So, if you observe weird behavior in relation to your modules and imports, check if maybe there is a name clash. There are ways to influence Python’s importing mechanism in more detail to circumvent problems, but that gets quite technical and furthermore tends to be unstable, so we will not discuss it in this course. There are two variations of the `import` statement that you will often see in Python code that is around: The `from … import …` statement can be used to import individual functions from a module, for example:<jupyter_code>from random import randint
print(randint(1,10))<jupyter_output>2<jupyter_text>This way, the function is available without using the module name as a prefix. It is not recommended to use this kind of import, however, as it comes with a risk of name clashes and unreliable behavior. The other variation is the ```import``` … ```as``` … statement, which defines an alias for the module name. For example:<jupyter_code>import random as rd
print(rd.randint(1,10))<jupyter_output><empty_output><jupyter_text>This is handy in particular for defining shorter aliases for long module names, but it should be used with care, too, to avoid name clashes.It is important to realize that all Python .py files are modules. However, not all of them are (just) made for being imported into other modules. Some also have code that should be executed when the module is executed as a script or with ```python -m```. Interestingly, a module can discover if it is being executed directly (in Python speech: running in the main scope) or as an import. A pattern that is often used in Python modules is to include such a check, and call a dedicated main function if the method is indeed running in the main scope:    if __name__ == "__main__":       execute only if running in main scope      main()      This allows to have a module providing functions that are useful to import by other modules, and at the same time, for example, offering a command line interface that is useful when starting the module as a script, but would only be a nuisance would it be run during a standard import.  PackagesPackages are used to organize sets of modules hierarchically. Technically, they are folders that contains modules and a special `__init__.py` file (to indicate that the modules in this folder form a package). We don’t go into further detail here, but note that when using modules from collections of functionality such as the Python Standard Library, they are distributed as packages.  The Standard Library and other Sources of FunctionalityThe Python Standard Library, which is distributed with the Python installation, contains ```random``` and a large number of other useful packages, such as the ```calendar```,  ```statistics``` or ```io``` libraries. We will see and use some of them in the following lectures. The website https://docs.python.org/3/library/index.html lists what is contained in the standard library of Python and is a good reference during development.In addition to the packages in the standard library, there are also many packages available from other sources. The Python Package index at https://pypi.python.org/pypi is the central repository for Python packages, currently ca. 290,000 of them. With the Anaconda platform a large number of popular packages has already been installed on your system, but sometimes packages are not yet installed on your machine, so you have to do that yourself before you can use them. How to do this within Anaconda is described at https://docs.anaconda.com/anaconda/navigator/tutorials/manage-packages/installing-a-package. Alternatively, the basic way to do install new packages is via the command line, using the ```pip``` tool that comes with the Python installation: ```pip install ```  RecursionSee Recursion.Just a joke, but it points to the basic idea of recursion: self-reference. You may recall recursive function definitions from mathematics, for instance:x$^n$ = x * x$^{n-1}$ if n > 0, and 1 if n = 0That is, the problem is solved by referring to a smaller instance of the same problem, until it is so small that it is trivial. For example, with this definition 35 is calculated as follows:3$^5$ = 3 * 3$^4$ = 3 * 3 * 3$^3$ = 3  * 3 * 3 * 3$^2$ = 3 * 3 * 3 * 3 * 3$^1$ = 3 * 3 * 3 * 3 * 3 * 3$^0$ =  3 * 3 * 3 * 3 * 3 * 1 =  3 * 3 * 3 * 3 * 3 = 3 * 3 * 3 * 9 = 3 * 3 * 27 = 3 * 81 = 243Recursions in Python (and other programming languages as well) follow the same principle, but the definition in code looks a bit different:<jupyter_code># recursive function to compute x**n
def pow(x,n):
    if n > 0:
        return x * pow(x,n-1)
    else:
        return 1
        
# main program calling the function
x = int(input("Please enter x: "))
n = int(input("Please enter n: "))
x_to_the_power_of_n = pow(x,n)
print(f"{x} ** {n} = {x_to_the_power_of_n}")<jupyter_output>Please enter x:  3
Please enter n:  6<jupyter_text>Internally, the following calls and returns of pow(x,n) happen at runtime:```pow(3,5)    pow(3,4)        pow(3,3)            pow(3,2)                pow(3,1)                    pow(3,0)                    return 1                return 3            return 9        return 27    return 81return 243``` Now let us look at an example for which there is not already an operator in Python. For example, the Fibonacci number for an integer n is defined as:fib(n) = fib(n-1) + fib(n-2)  if n > 1, 1 if n = 1 and 0 if n = 0In Python:<jupyter_code>def fib(n):
    if n > 1:
        return fib(n-1) + fib(n-2)
    elif n == 1:
        return 1
    else:
        return 0
    
n = int(input("Please enter an integer number: "))
print(f"fib({n}) = {fib(n)}")<jupyter_output>Please enter an integer number:  34<jupyter_text>The call stack here is a bit more complex than above (only shown for fib(4) to keep it short): ```fib(4)      fib(3)        fib(2)            fib(1)            return 1            fib(0)            return 0        return 1        fib(1)        return 1    return 2    fib(2)        fib(1)        return 1        fib(0)        return 0    return 1return 3```Here is another, non-mathematical example:<jupyter_code>def walk_up_and_down(floors):
    if floors > 0:
        print("Walk one floor up.")
        walk_up_and_down(floors-1)
        print("Walk one floor down.")
    else:
        print("Reached the top floor!")
        
walk_up_and_down(3)<jupyter_output>Walk one floor up.
Walk one floor up.
Walk one floor up.
Reached the top floor!
Walk one floor down.
Walk one floor down.
Walk one floor down.<jupyter_text>The call stack here is straightforward again. Note that also without an explicit "return value" statement, the function returns control to its caller when it is finished, but then without returning a value:```walk_up_and_down(3)    walk_up_and_down(2)        walk_up_and_down(1)            walk_up_and_down(0)            (return)        (return)    (return)(return)```Each recursion can be implemented by an iterative loop, and vice versa. For the walking-up-and-down example, a corresponding iterative version is:<jupyter_code>def walk_up_and_down_iteratively(floors):
    i = 0
    while i < floors:
        print("Walk one floor up.")
        i = i + 1
    print("Reached the top floor!")
    while i > 0:
        print("Walk one floor down.")
        i = i - 1
        
walk_up_and_down_iteratively(3)<jupyter_output>Walk one floor up.
Walk one floor up.
Walk one floor up.
Reached the top floor!
Walk one floor down.
Walk one floor down.
Walk one floor down.
{'fraction_non_alphanumeric': 0.06607819668860962, 'fraction_numerical': 0.010572511470177539, 'mean_word_length': 4.152363823227133, 'pattern_counts': {'":': 1, '<': 51, '<?xml version=': 0, '>': 59, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '15', 'dir': 'jupyter-structured-clean-dedup', 'id': '494201', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 5336, 'n_tokens_neox': 5075, 'n_words': 2877, 'path': 'Lecturenotes/M06_Functions_and_Modules.ipynb', 'repo_name': 'wooshrow/course-python', 'size': '20052'}