<jupyter_start><jupyter_text>**Important**: Click on "*Kernel*" > "*Restart Kernel and Clear All Outputs*" *before* reading this chapter in [JupyterLab ](https://jupyterlab.readthedocs.io/en/stable/)  Chapter 2: Functions & Modularization In Chapter 1, we simply typed the code to calculate the average of the even numbers in a list of whole numbers into several code cells. Then, we executed them one after another. We had no way of *reusing* the code except for either executing cells multiple times. And, whenever we find ourselves doing repetitive manual work, we can be sure that there must be a way of automating what we are doing.This chapter shows how Python offers language constructs that let us **define** functions ourselves that we may then **call** just like the built-in ones. Also, we look at how we can extend our Python installation with functionalities written by other people.  [Built-in Functions ](https://docs.python.org/3/library/functions.html) Python comes with plenty of useful functions built in, some of which we have already seen before (e.g., [print() ](https://docs.python.org/3/library/functions.htmlprint), [sum() ](https://docs.python.org/3/library/functions.htmlsum), [len() ](https://docs.python.org/3/library/functions.htmllen), or [id() ](https://docs.python.org/3/library/functions.htmlid)). The [documentation ](https://docs.python.org/3/library/functions.html) has the full list. Just as core Python itself, they are mostly implemented in C and thus very fast.Below, [sum() ](https://docs.python.org/3/library/functions.htmlsum) adds up all the elements in the `numbers` list while [len() ](https://docs.python.org/3/library/functions.htmllen) counts the number of elements in it.<jupyter_code>numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
sum(numbers)
length
len(numbers)
a = "abcde"
len(a)<jupyter_output><empty_output><jupyter_text>`sum` and `len` are *no* [keywords ](https://docs.python.org/3/reference/lexical_analysis.htmlkeywords) like `for` or `if` but variables that reference *objects* in memory. Often, we hear people say that "everything is an object in Python" (e.g., this [question ](https://stackoverflow.com/questions/40478536/in-python-what-does-it-mean-by-everything-is-an-object)). While this phrase may sound abstract in the beginning, it simply means that the entire memory is organized with "bags" of $0$s and $1$s, and there are even bags for the built-in functions. That is *not* true for many other languages (e.g., C or Java) and often a source of confusion for people coming to Python from another language.The built-in [id() ](https://docs.python.org/3/library/functions.htmlid) function tells us where in memory a particular built-in function is stored.<jupyter_code>id(sum)
id(len)<jupyter_output><empty_output><jupyter_text>[type() ](https://docs.python.org/3/library/functions.htmltype) reveals that built-in functions like [sum() ](https://docs.python.org/3/library/functions.htmlsum) or [len() ](https://docs.python.org/3/library/functions.htmllen) are objects of type `builtin_function_or_method`.<jupyter_code>a = "abc"
type(a)
import datetime

x = datetime.datetime.now()
print(x) 
type(x)
type(sum)
type(len)<jupyter_output><empty_output><jupyter_text>Python's object-oriented nature allows us to have functions work with themselves. While seemingly not useful from a beginner's point of view, that enables a lot of powerful programming styles later on.<jupyter_code>id(id)
type(type)
#function_name(list_of_variables) variables = objects
numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
a = 5
sum(numbers)
callable(sum)<jupyter_output><empty_output><jupyter_text>To execute a function, we **call** it with the **call operator** `()` as shown many times in Chapter 1 and above.If we are unsure whether a variable references a function or not, we can verify that with the built-in [callable() ](https://docs.python.org/3/library/functions.htmlcallable) function.Abstractly speaking, *any* object that can be called with the call operator `()` is a so-called **callable**. And, objects of type `builtin_function_or_method` are just one kind of examples thereof. We will see another one already in the next sub-section.<jupyter_code>callable(sum)
callable(len)<jupyter_output><empty_output><jupyter_text>`list` objects, for example, are *not* callable.<jupyter_code>callable(numbers)<jupyter_output><empty_output><jupyter_text>Constructors The list of [built-in functions ](https://docs.python.org/3/library/functions.html) in the documentation should really be named a list of built-in *callables*.Besides the built-in functions, the list also features **constructors** for the built-in types. They may be used to **[cast ](https://en.wikipedia.org/wiki/Type_conversion)** (i.e., "convert") any object as an object of a given type.For example, to "convert" a `float` or a `str` into an `int` object, we use the [int() ](https://docs.python.org/3/library/functions.htmlint) built-in. Below, *new* `int` objects are created from the `7.0` and `"7"` objects that are *newly* created themselves before being processed by [int() ](https://docs.python.org/3/library/functions.htmlint) right away *without* ever being referenced by a variable.<jupyter_code>b = "7.97324973gcxcxfx264"
print(type(b))
a = float(b)
type(a)
type(int(float("7.0")))
a = int(float("7.6"))
a
int(7.0)
int("7")<jupyter_output><empty_output><jupyter_text>Casting an object as an `int` is different from rounding with the built-in [round() ](https://docs.python.org/3/library/functions.htmlround) function!<jupyter_code>int(-7.99)
round(-7.99)<jupyter_output><empty_output><jupyter_text>Notice the subtle difference compared to the behavior of the `//` operator in Chapter 1 that "rounds" towards minus infinity: [int() ](https://docs.python.org/3/library/functions.htmlint) always "rounds" towards `0`.<jupyter_code>int(-7.99)<jupyter_output><empty_output><jupyter_text>Not all conversions are valid and *runtime* errors may occur as the `ValueError` shows.<jupyter_code>int("seven")<jupyter_output><empty_output><jupyter_text>We may also cast in the other direction with the [float() ](https://docs.python.org/3/library/functions.htmlfloat)  or [str() ](https://docs.python.org/3/library/functions.htmlfunc-str) built-ins.<jupyter_code>float(7)
str(7)<jupyter_output><empty_output><jupyter_text>Constructors are full-fledged objects as well.<jupyter_code>id(int)
id(float)<jupyter_output><empty_output><jupyter_text>They are of type `type`, which is different from `builtin_function_or_method` above.<jupyter_code>type(int)
type(float)<jupyter_output><empty_output><jupyter_text>As already noted, constructors are *callables*. In that regard, they behave the same as built-in functions. We may call them with the call operator `()`.<jupyter_code>callable(int)
callable(float)<jupyter_output><empty_output><jupyter_text>The attentive student may already have discovered that we refer to `builtin_function_or_method` objects as "built-in functions" and `type` objects as just "built-ins." For a beginner, that difference is not so important. But, the ambitious student should already be aware that such subtleties exist.Next, let's look at a third kind of callables.   Function Definitions We may create so-called *user-defined* **functions** with the `def` statement (cf., [reference ](https://docs.python.org/3/reference/compound_stmts.htmlfunction-definitions)). To extend an already familiar example, we reuse the introductory example from Chapter 1 in its final Pythonic version and transform it into the function `average_evens()` below. We replace the variable name `numbers` with `integers` for didactical purposes in the first couple of examples.A function's **name** must be chosen according to the same naming rules as ordinary variables since Python manages function names like variables. In this book, we further adopt the convention of ending function names with parentheses `()` in text cells for faster comprehension when reading (i.e., `average_evens()` vs. `average_evens`). These are *not* part of the name but must always be written out in the `def` statement for syntactic reasons.Functions may define an arbitrary number of **parameters** as inputs that can then be referenced within the indented **code block**: They are listed within the parentheses in the `def` statement (i.e., `integers` below). The code block is also called a function's **body**, while the first line starting with `def` and ending with a colon is the **header**.Together, the name and the list of parameters are also referred to as the function's **[signature ](https://en.wikipedia.org/wiki/Type_signature)** (i.e., `average_evens(integers)` below).A function may specify an *explicit* **return value** (i.e., "result" or "output") with the `return` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.htmlthe-return-statement)): Functions that have one are considered **fruitful**; otherwise, they are **void**. Functions of the latter kind are still useful because of their **side effects**. For example, the built-in [print() ](https://docs.python.org/3/library/functions.htmlprint) function changes what we see on the screen. Strictly speaking, [print() ](https://docs.python.org/3/library/functions.htmlprint) and other void functions also have an *implicit* return value, namely the `None` object.A function should define a **docstring** that describes what it does in a short subject line, what parameters it expects (i.e., their types), and what it returns, if anything. A docstring is a syntactically valid multi-line string (i.e., type `str`) defined within **triple-double quotes** `"""`. Widely adopted standards for docstrings are [PEP 257 ](https://www.python.org/dev/peps/pep-0257/) and section 3.8 of [Google's Python Style Guide ](https://github.com/google/styleguide/blob/gh-pages/pyguide.md).<jupyter_code>def function_name(list_of_variables): = arguments
    """ 
    double quotes
    role of the function 
    abc
    sd
    """
    
    body of functions
    
    return values (optional)
def average_evens(integers):
    """Calculate the average of all even numbers in a list.

    Args:
        integers (list of int's): whole numbers to be averaged

    Returns:
        average (float)
    """
    evens = [n for n in integers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return average<jupyter_output><empty_output><jupyter_text>Once defined, a function may be referenced just like any other variable by its name (i.e., *without* the parentheses).<jupyter_code>average_evens<jupyter_output><empty_output><jupyter_text>This works as functions are full-fledged *objects*. So, `average_evens` is just a name referencing an object in memory with an **identity**, a **type**, namely `function`, and a **value**. In that regard, `average_evens` is *no* different from the variable `numbers` or the built-ins' names.<jupyter_code>id(average_evens)
type(average_evens)<jupyter_output><empty_output><jupyter_text>Its value may seem awkward at first: It consists of a location showing where the function is defined (i.e., `__main__` here, which is Python's way of saying "in this notebook") and the signature wrapped inside angle brackets ``. The angle brackets are a convention to indicate that the value may *not* be used as a *literal* (i.e., typed back into another code cell). [Chapter 10 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/10_classes_00_content.ipynb) introduces the concept of a **text representation** of an object, which is related to the *semantic* meaning of an object's value as discussed in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/01_elements_00_content.ipynbValue-/-"Meaning"), and the angle brackets convention is one such way to represent an object as text. When executed, the angle brackets cause a `SyntaxError` because Python expects the `](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/03_conditionals_00_content.ipynbRelational-Operators)).<jupyter_code><function __main__.average_evens(numbers)><jupyter_output><empty_output><jupyter_text>`average_evens` is, of course, callable. So, the `function` type is the third kind of callable in this chapter.<jupyter_code>callable(average_evens)<jupyter_output><empty_output><jupyter_text>The built-in [help() ](https://docs.python.org/3/library/functions.htmlhelp) function shows a function's docstring.Whenever we use code to analyze or obtain information on an object, we say that we **[introspect ](https://en.wikipedia.org/wiki/Type_introspection)** it.<jupyter_code>help(average_evens)<jupyter_output><empty_output><jupyter_text>In a Jupyter notebook, we can just as well add a question mark to a function's name to achieve the same.<jupyter_code>average_evens?<jupyter_output><empty_output><jupyter_text>Two question marks show a function's source code.<jupyter_code>average_evens??<jupyter_output><empty_output><jupyter_text>[help() ](https://docs.python.org/3/library/functions.htmlhelp) and the `?`s also work for built-ins.<jupyter_code>help(sum)<jupyter_output><empty_output><jupyter_text>Function Calls Once defined, we may call a function with the call operator `()` as often as we wish. The formal parameters are then filled in by **passing** *expressions* (e.g., literals or variables) as **arguments** to the function within the parentheses.<jupyter_code>average_evens([7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4])
average_evens(numbers)<jupyter_output><empty_output><jupyter_text>A function call's return value is commonly assigned to a variable for subsequent use. Otherwise, we lose access to the returned object right away.<jupyter_code>result = average_evens(numbers)
result<jupyter_output><empty_output><jupyter_text>Scoping Rules  Local Scope disappears The parameters listed in a function's definition (i.e., `integers` in the example) and variables created *inside* it during execution (i.e., `evens` and `average`) are **local** to that function. That means they only reference an object in memory *while* the function is being executed and are dereferenced immediately when the function call returns. We say they go out of **scope**. That is why we see the `NameError`s below.<jupyter_code>integers
evens
average<jupyter_output><empty_output><jupyter_text>[PythonTutor ](http://pythontutor.com/visualize.htmlcode=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0A%0Adef%20average_evens%28integers%29%3A%0A%20%20%20%20evens%20%3D%20%5Bn%20for%20n%20in%20integers%20if%20n%20%25%202%20%3D%3D%200%5D%0A%20%20%20%20average%20%3D%20sum%28evens%29%20/%20len%28evens%29%0A%20%20%20%20return%20average%0A%0Aresult%20%3D%20average_evens%28numbers%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) visualizes what happens in memory: To be precise, in the exact moment when the function call is initiated and `numbers` passed in as the `integers` argument, there are *two* references to the *same* `list` object (cf., steps 4-5 in the visualization). We also see how Python creates a *new* **frame** that holds the function's local scope (i.e., "internal names") in addition to the **global** frame. Frames are nothing but [namespaces ](https://en.wikipedia.org/wiki/Namespace) to *isolate* the names of different **scopes** from each other. The list comprehension `[n for n in integers if n % 2 == 0]` constitutes yet another frame that is in scope as the `list` object assigned to `evens` is *being* created (cf., steps 6-20). When the function returns, only the global frame is left (cf., last step).  Global Scope is everywhere On the contrary, while a function is *being* executed, it may reference the variables of **enclosing scopes** (i.e., "outside" of it). This is a common source of *semantic* errors. Consider the following stylized and incorrect example `average_wrong()`. The error is hard to spot with eyes: The function never references the `integers` parameter but the `numbers` variable in the **global scope** instead.<jupyter_code>def average_wrong(integers):
    """Calculate the average of all even numbers in a list.

    Args:
        integers (list of int's): whole numbers to be averaged

    Returns:
        average (float)
    """
    evens = [n for n in numbers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return average<jupyter_output><empty_output><jupyter_text>`numbers` in the global scope is, of course, *not* changed by merely defining `average_wrong()`.<jupyter_code>numbers<jupyter_output><empty_output><jupyter_text>Sometimes a function may return a correct solution for *some* inputs ...<jupyter_code>average_wrong(numbers)  # correct by accident<jupyter_output><empty_output><jupyter_text>... but still be wrong *in general*.<jupyter_code>average_wrong([123, 456, 789])<jupyter_output><empty_output><jupyter_text>[PythonTutor ](http://pythontutor.com/visualize.htmlcode=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0A%0Adef%20average_wrong%28integers%29%3A%0A%20%20%20%20evens%20%3D%20%5Bn%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%5D%0A%20%20%20%20average%20%3D%20sum%28evens%29%20/%20len%28evens%29%0A%20%20%20%20return%20average%0A%0Aresult%20%3D%20average_wrong%28%5B123,%20456,%20789%5D%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) is again helpful at visualizing the error interactively: Creating the `list` object `evens` eventually references takes *16* computational steps, namely two for managing the list comprehension, one for setting up an empty `list` object, *twelve* for filling it with elements derived from `numbers` in the global scope (i.e., that is the error), and one to make `evens` reference it (cf., steps 6-21).The frames logic shown by PythonTutor is the mechanism with which Python not only manages the names inside *one* function call but also for *many* potentially *simultaneous* calls, as revealed in [Chapter 4 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/04_iteration_00_content.ipynbTrivial-Example:-Countdown). It is the reason why we may reuse the same names for the parameters and variables inside both `average_evens()` and `average_wrong()` without Python mixing them up. So, as we already read in the [Zen of Python ](https://www.python.org/dev/peps/pep-0020/), "namespaces are one honking great idea" (cf., `import this`), and a frame is just a special kind of namespace.  Shadowing Code gets even more confusing when variables by the *same* name from *different* scopes collide. In particular, what should we expect to happen if a function "changes" a globally defined variable in its body?`average_evens()` below works like `average_evens()` above except that it rounds the numbers in `integers` with the built-in [round() ](https://docs.python.org/3/library/functions.htmlround) function before filtering and averaging them. [round() ](https://docs.python.org/3/library/functions.htmlround) returns `int` objects independent of its argument being an `int` or a `float` object. On the first line in its body, `average_evens()` introduces a *local* variable `numbers` whose name collides with the one defined in the global scope.<jupyter_code>def average_evens(integers):
    """Calculate the average of all even numbers in a list.

    Args:
        integers (list of int's/float's): numbers to be averaged;
            if non-whole numbers are provided, they are rounded

    Returns:
        average (float)
    """
    numbers = [round(n) for n in integers]
    evens = [n for n in numbers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return average<jupyter_output><empty_output><jupyter_text>As a good practice, let's first "verify" that `average_evens()` is "correct" by calling it with inputs for which we can calculate the answer in our heads. Treating a function as a "black box" (i.e., input-output specification) when testing is also called [unit testing ](https://en.wikipedia.org/wiki/Unit_testing) and plays an important role in modern software engineering.<jupyter_code>average_evens([40.0, 41.1, 42.2, 43.3, 44.4])<jupyter_output><empty_output><jupyter_text>Such tests are often and conveniently expressed with the `assert` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.htmlthe-assert-statement)): If the expression following `assert` evaluates to `True`, nothing happens.<jupyter_code>assert average_evens([40.0, 41.1, 42.2, 43.3, 44.4]) == 42.0<jupyter_output><empty_output><jupyter_text>However, if the expression evaluates to `False`, an `AssertionError` is raised.<jupyter_code>assert average_evens([40.0, 41.1, 42.2, 43.3, 44.4]) == 87.0<jupyter_output><empty_output><jupyter_text>Calling `average_evens()` leaves `numbers` in the global scope unchanged.<jupyter_code>numbers<jupyter_output><empty_output><jupyter_text>To add to the confusion, let's also pass the global `numbers` list as an argument to `average_evens()`. The return value is the same as before.<jupyter_code>average_evens(numbers)<jupyter_output><empty_output><jupyter_text>In summary, Python is smart enough to keep all the involved `numbers` variables apart. So, the global `numbers` variable is still referencing the *same* `list` object as before.<jupyter_code>numbers<jupyter_output><empty_output><jupyter_text>The reason why everything works is that *every* time we (re-)assign an object to a variable *inside* a function's body with the `=` statement, this is done in the *local* scope by default. There are ways to change variables existing in an outer scope from within a function, but this is a rather advanced topic.[PythonTutor ](http://pythontutor.com/visualize.htmlcode=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0A%0Adef%20average_evens%28integers%29%3A%0A%20%20%20%20numbers%20%3D%20%5Bround%28n%29%20for%20n%20in%20integers%5D%0A%20%20%20%20evens%20%3D%20%5Bn%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%5D%0A%20%20%20%20average%20%3D%20sum%28evens%29%20/%20len%28evens%29%0A%20%20%20%20return%20average%0A%0Aresult%20%3D%20average_evens%28%5B40.0,%2041.1,%2042.2,%2043.3,%2044.4%5D%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) shows how *two* `numbers` variables exist in *different* scopes referencing *different* objects (cf., steps 14-25) when we execute `average_evens([40.0, 41.1, 42.2, 43.3, 44.4])`.Variables whose names collide with the ones of variables in enclosing scopes - and the global scope is just the most enclosing scope - are said to **shadow** them.While this is not a problem for Python, it may lead to less readable code for humans and should be avoided if possible. But, as the software engineering wisdom goes, "[naming things](https://skeptics.stackexchange.com/questions/19836/has-phil-karlton-ever-said-there-are-only-two-hard-things-in-computer-science)" is often considered a hard problem as well, and we have to be prepared to encounter shadowing variables.Shadowing also occurs if a parameter in the function definition goes by the same name as a variable in an outer scope. Below, `average_evens()` is identical to the first version in this chapter except that the parameter `integers` is now called `numbers` as well.<jupyter_code>def average_evens(numbers):
    """Calculate the average of all even numbers in a list.

    Args:
        numbers (list of int's): whole numbers to be averaged

    Returns:
        average (float)
    """
    evens = [n for n in numbers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return average
average_evens(numbers)
numbers<jupyter_output><empty_output><jupyter_text>[PythonTutor ](http://pythontutor.com/visualize.htmlcode=numbers%20%3D%20%5B7,%2011,%208,%205,%203,%2012,%202,%206,%209,%2010,%201,%204%5D%0A%0Adef%20average_evens%28numbers%29%3A%0A%20%20%20%20evens%20%3D%20%5Bn%20for%20n%20in%20numbers%20if%20n%20%25%202%20%3D%3D%200%5D%0A%20%20%20%20average%20%3D%20sum%28evens%29%20/%20len%28evens%29%0A%20%20%20%20return%20average%0A%0Aresult%20%3D%20average_evens%28numbers%29&cumulative=false&curInstr=0&heapPrimitives=nevernest&mode=display&origin=opt-frontend.js&py=3&rawInputLstJSON=%5B%5D&textReferences=false) reveals that in this example there are *two* `numbers` variables in *different* scope referencing the *same* `list` object in memory (cf., steps 4-23).  Positional vs. Keyword Arguments So far, we have specified only one parameter in each of our user-defined functions. In Chapter 1, however, we saw the built-in [divmod() ](https://docs.python.org/3/library/functions.htmldivmod) function take two arguments. And, the order in which they are passed in matters! Whenever we call a function and list its arguments in a comma separated manner, we say that we pass in the arguments *by position* or refer to them as **positional arguments**.<jupyter_code>divmod(42, 10.2)
divmod(10, 42)<jupyter_output><empty_output><jupyter_text>For many functions, there is a natural order to the arguments: For example, for any kind of division passing the dividend first and the divisor second seems intuitive. But what if that is not the case in another setting? For example, let's create a close relative of the above `average_evens()` function that also scales the resulting average by a factor. What is more natural? Passing in `numbers` first? Or `scalar`? There is no obvious way and we continue with the first alternative for no concrete reason.<jupyter_code>def scaled_average_evens(numbers, scalar):
    """Calculate the scaled average of all even numbers in a list.

    Args:
        numbers (list of int's/float's): numbers to be averaged;
            if non-whole numbers are provided, they are rounded
        scalar (float): multiplies the average

    Returns:
        scaled_average (float)
    """
    numbers = [round(n) for n in numbers]
    evens = [n for n in numbers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return scalar * average<jupyter_output><empty_output><jupyter_text>As with [divmod() ](https://docs.python.org/3/library/functions.htmldivmod), we may pass in the arguments by position.<jupyter_code>scaled_average_evens(numbers, 2)
scaled_average_evens(numbers = numbers, scalar = 2)
scaled_average_evens(scalar = 2, numbers = numbers)<jupyter_output><empty_output><jupyter_text>Now, this function call is a bit harder to understand as we always need to remember what the `2` means. This becomes even harder with more parameters.Luckily, we may also pass in arguments *by name*. Then, we refer to them as **keyword arguments**.<jupyter_code>scaled_average_evens(numbers=numbers, scalar=2)<jupyter_output><empty_output><jupyter_text>When passing all arguments by name, we may do so in any order.<jupyter_code>scaled_average_evens(scalar=2, numbers=numbers)<jupyter_output><empty_output><jupyter_text>We may even combine positional and keyword arguments in the same function call.<jupyter_code>scaled_average_evens(numbers, scalar=2)<jupyter_output><empty_output><jupyter_text>Unfortunately, there are ways to screw this up with a `SyntaxError`: If positional and keyword arguments are mixed, the keyword arguments *must* come last.<jupyter_code>scaled_average_evens(numbers=numbers, 2)<jupyter_output><empty_output><jupyter_text>Similarly, we must always pass in the right number of arguments. Otherwise, a `TypeError` is raised.<jupyter_code>scaled_average_evens(numbers)
scaled_average_evens(numbers, 2, 3)<jupyter_output><empty_output><jupyter_text>Modularization Defining `average_evens()` and `scaled_average_evens()` as above leads to a repetition of most of their code. That is *not* good as such a redundancy makes a code base hard to maintain in the long run: Whenever we change the logic in one function, we must *not* forget to do so for the other function as well. And, most likely, we forget about such issues in larger projects.Below, three of four lines in the functions' bodies are identical!<jupyter_code>def average_evens(numbers):
    """ ... ... ... """
    numbers = [round(n) for n in numbers]
    evens = [n for n in numbers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return average
def scaled_average_evens(numbers, scalar):
    """ ... ... ... """
    return scalar * average_evens(numbers)<jupyter_output><empty_output><jupyter_text>A better way is to design related functions in a **modular** fashion such that they reuse each other's code.For example, as not scaling an average is just a special case of scaling it with `1`, we could redefine the two functions like below: In this version, the function resembling the *special* case, `average_evens()`, **forwards** the call to the more *general* function, `scaled_average_evens()`, passing a `scalar` argument of `1`. As the name `scaled_average_evens` within the body of `average_evens()` is looked up each time the function is *being* executed, we may define `average_evens()` before `scaled_average_evens()`.<jupyter_code>def average_evens(numbers):
    """ ... ... ... """
    return scaled_average_evens(numbers, scalar=1)
def scaled_average_evens(numbers, scalar):
    """ ... ... ... """
    numbers = [round(n) for n in numbers]
    evens = [n for n in numbers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return scalar * average<jupyter_output><empty_output><jupyter_text>After **refactoring** the functions, it is a good idea to test them again.<jupyter_code>assert average_evens(numbers) == 7.0
assert scaled_average_evens(numbers, 2) == 14.0<jupyter_output><empty_output><jupyter_text>Default Arguments *Assuming* that scaling the average occurs rarely, it may be a good idea to handle both cases in *one* function definition by providing a **default argument** of `1` for the `scalar` parameter.<jupyter_code>numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
def average_evens(numbers, scalar = 1):
    """Calculate the average of all even numbers in a list.

    Args:
        numbers (list of int's/float's): numbers to be averaged;
            if non-whole numbers are provided, they are rounded
        scalar (float, optional): multiplies the average; defaults to 1

    Returns:
        scaled_average (float)
    """
    numbers = [round(n) for n in numbers]
    evens = [n for n in numbers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return scalar * average<jupyter_output><empty_output><jupyter_text>Now, we call the function with or without passing a `scalar` argument.If `scalar` is *not* passed in, it automatically takes the value `1`.If `scalar` is passed in, this may be done as either a positional or a keyword argument. Which of the two calls where `scalar` is `2` is faster to understand in a larger program?<jupyter_code>average_evens(numbers,2)
average_evens(numbers,scalar = 1)
average_evens(scalar = 1, numbers = numbers)
average_evens(numbers)
average_evens(numbers, 2)
average_evens(numbers, scalar=2)
average_evens(scalar = 2, numbers =  numbers)<jupyter_output><empty_output><jupyter_text>Keyword-only Arguments Because we *assumed* that scaling occurs rarely, we would prefer that our new version of `average_evens()` be called with a *keyword argument* whenever `scalar` is passed in. Then, a function call is never ambiguous when reading the source code.Python offers a **keyword-only** syntax when defining a function that *forces* a caller to pass the `scalar` argument *by name* if it is passed in at all: To do so, we place an asterisk `*` before the arguments that may only be passed in by name. Note that the keyword-only syntax also works *without* a default argument.<jupyter_code>def average_evens(numbers, *, scalar=1):
    """Calculate the average of all even numbers in a list.

    Args:
        numbers (list of int's/float's): numbers to be averaged;
            if non-whole numbers are provided, they are rounded
        scalar (float, optional): multiplies the average; defaults to 1

    Returns:
        scaled_average (float)
    """
    numbers = [round(n) for n in numbers]
    evens = [n for n in numbers if n % 2 == 0]
    average = sum(evens) / len(evens)
    return scalar * average<jupyter_output><empty_output><jupyter_text>As before, we may call `average_evens()` without passing in an argument for the `scalar` parameter.<jupyter_code>average_evens(numbers)<jupyter_output><empty_output><jupyter_text>If we call `average_evens()` with a `scalar` argument, we *must* use keyword notation.<jupyter_code>average_evens(numbers, scalar=2)<jupyter_output><empty_output><jupyter_text>If instead we pass in `scalar` as a positional argument, we get a `TypeError`.<jupyter_code>average_evens(numbers, 2)<jupyter_output><empty_output><jupyter_text>Anonymous Functions The `def` statement is a statement because of its *side effect* of creating a *new* name that references a *new* `function` object in memory.We can thus think of it as doing *two* things **atomically** (i.e., either both of them happen or none). First, a `function` object is created that contains the concrete $0$s and $1$s that resemble the instructions we put into the function's body. In the context of a function, these $0$s and $1$s are also called **[byte code ](https://en.wikipedia.org/wiki/Bytecode)**. Then, a name referencing the new `function` object is created.Only this second aspect makes `def` a statement: Merely creating a new object in memory without making it accessible for later reference does *not* constitute a side effect because the state the program is *not* changed. After all, if we cannot reference an object, how do we know it exists in the first place?Python provides a `lambda` expression syntax that allows us to *only* create a `function` object in memory *without* making a name reference it (cf., [reference ](https://docs.python.org/3/reference/expressions.htmllambda)). It starts with the keyword `lambda` followed by an optional listing of comma separated parameters, a mandatory colon, and *one* expression that serves as the return value of the resulting `function` object. Because it does *not* create a name referencing the object, we effectively create "anonymous" functions with it.In the example, we create a `function` object that adds `3` to the only argument passed in as the parameter `x` and returns that sum.<jupyter_code>lambda x: x + 3
def add_three_a(x):
    return (x+3)<jupyter_output><empty_output><jupyter_text>If you think this is rather pointless to do, you are absolutely correct!We created a `function` object, dit *not* call it, and Python immediately forgot about it. So what's the point?To inspect the object created by a `lambda` expression, we use the simple `=` statement and assign it to the variable `add_three`, which is really `add_three()` as per our convention from above.<jupyter_code>add_three = lambda x: x + 3  # we could and should use def instead<jupyter_output><empty_output><jupyter_text>[type() ](https://docs.python.org/3/library/functions.htmltype) and [callable() ](https://docs.python.org/3/library/functions.htmlcallable) confirm that `add_three` is indeed a callable `function` object.<jupyter_code>type(add_three)
#call operator()
#add_three is a function
#add_three(variable)
a = 5
print(type(a))
callable(a)
callable(add_three)<jupyter_output><empty_output><jupyter_text>Now we may call `add_three()` as if we defined it with the `def` statement.<jupyter_code>add_three(39)
add_three_a(39)<jupyter_output><empty_output><jupyter_text>Alternatively, we could call an `function` object created with a `lambda` expression right away (i.e., without assigning it to a variable), which looks quite weird for now as we need *two* pairs of parentheses: The first one serves as a delimiter whereas the second represents the call operator.<jupyter_code>(lambda abc: len(abc) ) ("hello")
functional programming
abc -> cde -> cfg<jupyter_output><empty_output><jupyter_text>The main point of having functions without a reference to them is to use them in a situation where we know ahead of time that we use the function only *once*.Popular applications of lambda expressions occur in combination with the **map-filter-reduce** paradigm (cf., [Chapter 8 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/08_mfr_00_content.ipynbLambda-Expressions)).  Extending Core Python<jupyter_code>#buil-in function:
len
int
float
a = 5
float(a)
# user-defined function
def add_three(x):
    return x+3
# square root number
# library = package(Python)
import math
math.sqrt(2)<jupyter_output><empty_output><jupyter_text>So far, we have only used what we refer to as **core** Python in this book. By this, we mean all the syntactical rules as specified in the [language reference ](https://docs.python.org/3/reference/) and a minimal set of about 50 built-in [functions ](https://docs.python.org/3/library/functions.html). With this, we could already implement any algorithm or business logic we can think of!However, after our first couple of programs, we would already start seeing recurring patterns in the code we write. In other words, we would constantly be "reinventing the wheel" in each new project.Would it not be smarter to pull out the reusable components from our programs and put them into some project independent **library** of generically useful functionalities? Then we would only need a way of including these **utilities** in our projects.As all programmers across all languages face this very same issue, most programming languages come with a so-called **[standard library ](https://en.wikipedia.org/wiki/Standard_library)** that provides utilities to accomplish everyday tasks without much code. Examples are making an HTTP request to some website, open and read popular file types (e.g., CSV or Excel files), do something on a computer's file system, and many more.  The Standard Library Python also comes with a [standard library ](https://docs.python.org/3/library/index.html) that is structured into coherent modules and packages for given topics: A **module** is just a plain text file with the file extension *.py* that contains Python code while a **package** is a folder that groups several related modules.The code in the [standard library ](https://docs.python.org/3/library/index.html) is contributed and maintained by many volunteers around the world. In contrast to so-called "third-party" packages (cf., the next section below), the Python core development team closely monitors and tests the code in the [standard library ](https://docs.python.org/3/library/index.html). Consequently, we can be reasonably sure that anything provided by it works correctly independent of our computer's operating system and will most likely also be there in the next Python versions. Parts in the [standard library ](https://docs.python.org/3/library/index.html) that are computationally expensive are often rewritten in C and, therefore, much faster than anything we could write in Python ourselves. So, whenever we can solve a problem with the help of the [standard library ](https://docs.python.org/3/library/index.html), it is almost always the best way to do so as well.The [standard library ](https://docs.python.org/3/library/index.html) has grown very big over the years, and we refer to the website [PYMOTW](https://pymotw.com/3/index.html) (i.e., "Python Module of the Week") that features well written introductory tutorials and how-to guides to most parts of the library. The same author also published a [book](https://www.amazon.com/Python-Standard-Library-Example-Developers/dp/0134291050/ref=as_li_ss_tl?ie=UTF8&qid=1493563121&sr=8-1&keywords=python+3+standard+library+by+example) that many Pythonistas keep on their shelf for reference. Knowing what is in the [standard library ](https://docs.python.org/3/library/index.html) is quite valuable for solving real-world tasks quickly.Throughout this book, we look at many modules and packages from the [standard library ](https://docs.python.org/3/library/index.html) in more depth, starting with the [math ](https://docs.python.org/3/library/math.html) and [random ](https://docs.python.org/3/library/random.html) modules in this chapter.  [math ](https://docs.python.org/3/library/math.html) Module The [math ](https://docs.python.org/3/library/math.html) module provides non-trivial mathematical functions like $sin(x)$ and constants like $\pi$ or $\text{e}$.To make functions and variables defined "somewhere else" available in our current program, we must first **import** them with the `import` statement (cf., [reference ](https://docs.python.org/3/reference/simple_stmts.htmlimport)).<jupyter_code>import math<jupyter_output><empty_output><jupyter_text>This creates the variable `math` that references a **[module object ](https://docs.python.org/3/glossary.htmlterm-module)** (i.e., type `module`) in memory.<jupyter_code>math
id(math)
# module = package = library
type(math)<jupyter_output><empty_output><jupyter_text>`module` objects serve as namespaces to organize the names inside a module. In this context, a namespace is nothing but a prefix that avoids collision with the variables already defined at the location where we import the module into.Let's see what we can do with the `math` module.The [dir() ](https://docs.python.org/3/library/functions.htmldir) built-in function may also be used with an argument passed in. Ignoring the dunder-style names, `math` offers quite a lot of names. As we cannot know at this point if a listed name refers to a function or an ordinary variable, we use the more generic term **attribute** to mean either one of them.<jupyter_code>dir()
import math
dir(math)<jupyter_output><empty_output><jupyter_text>Common mathematical constants and functions are now available via the dot operator `.` on the `math` object. This operator is sometimes also called the **attribute access operator**, in line with the just introduced term.<jupyter_code>math.pi
math.e
math.sqrt
help(math.sqrt)
math.sqrt(2)
math.sqrt(2) * math.sqrt(2)<jupyter_output><empty_output><jupyter_text>Observe how the arguments passed to functions do not need to be just variables or simple literals. Instead, we may pass in any *expression* that evaluates to a *new* object of the type the function expects.So just as a reminder from the expression vs. statement discussion in [Chapter 1 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/01_elements_00_content.ipynbExpressions): An expression is *any* syntactically correct combination of variables and literals with operators. And the call operator `()` is yet another operator. So both of the next two code cells are just expressions! They have no permanent side effects in memory. We may execute them as often as we want *without* changing the state of the program (i.e., this Jupyter notebook).So, regarding the very next cell in particular: Although the `2 ** 2` creates a *new* object `4` in memory that is then immediately passed into the [math.sqrt() ](https://docs.python.org/3/library/math.htmlmath.sqrt) function, once that function call returns, "all is lost" and the newly created `4` object is forgotten again, as well as the return value of [math.sqrt() ](https://docs.python.org/3/library/math.htmlmath.sqrt).<jupyter_code>math.sqrt(2 ** 2)<jupyter_output><empty_output><jupyter_text>Even the **composition** of several function calls only constitutes another expression.<jupyter_code>math.sqrt(average_evens([99, 100, 101])*4)
math.sqrt(average_evens([99, 100, 101]))<jupyter_output><empty_output><jupyter_text>If we only need one particular function from a module, we may also use the alternative `from ... import ...` syntax.This does *not* create a module object but only makes a variable in our current location reference an object defined inside a module directly.<jupyter_code>from math import sqrt
sqrt(16)<jupyter_output><empty_output><jupyter_text>[random ](https://docs.python.org/3/library/random.html) Module Often, we need a random variable, for example, when we want to build a simulation. The [random ](https://docs.python.org/3/library/random.html) module in the [standard library ](https://docs.python.org/3/library/index.html) often suffices for that.<jupyter_code>import random
random<jupyter_output><empty_output><jupyter_text>Besides the usual dunder-style attributes, the built-in [dir() ](https://docs.python.org/3/library/functions.htmldir) function lists some attributes in an upper case naming convention and many others starting with a *single* underscore `_`. To understand the former, we must wait until [Chapter 10 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/10_classes_00_content.ipynb), while the latter is explained further below.<jupyter_code>dir(random)<jupyter_output><empty_output><jupyter_text>The [random.random() ](https://docs.python.org/3/library/random.htmlrandom.random) function generates a uniformly distributed `float` number between $0$ (including) and $1$ (excluding).<jupyter_code>random.random
help(random.random)
random.random()
numbers<jupyter_output><empty_output><jupyter_text>While we could build some conditional logic with an `if` statement to map the number generated by [random.random() ](https://docs.python.org/3/library/random.htmlrandom.random) to a finite set of elements manually, the [random.choice() ](https://docs.python.org/3/library/random.htmlrandom.choice) function provides a lot more **convenience** for us. We call it with, for example, the `numbers` list and it draws one element out of it with equal chance.<jupyter_code>random.choice
help(random.choice)
numbers
random.choice(numbers)<jupyter_output><empty_output><jupyter_text>To reproduce the *same* random numbers in a simulation each time we run it, we set the **[random seed ](https://en.wikipedia.org/wiki/Random_seed)**. It is good practice to do that at the beginning of a program or notebook. It becomes essential when we employ randomized machine learning algorithms, like the [Random Forest ](https://en.wikipedia.org/wiki/Random_forest), and want to obtain **reproducible** results for publication in academic journals.The [random ](https://docs.python.org/3/library/random.html) module provides the [random.seed() ](https://docs.python.org/3/library/random.htmlrandom.seed) function to do that.<jupyter_code>random.seed(42)
random.random()
random.seed(42)
random.random()<jupyter_output><empty_output><jupyter_text>Third-party Packages As the Python community is based around open source, many developers publish their code, for example, on the Python Package Index [PyPI](https://pypi.org) from where anyone may download and install it for free using command-line based tools like [pip](https://pip.pypa.io/en/stable/) or [conda](https://conda.io/en/latest/). This way, we can always customize our Python installation even more. Managing many such packages is quite a deep topic on its own, sometimes fearfully called **[dependency hell ](https://en.wikipedia.org/wiki/Dependency_hell)**.The difference between the [standard library ](https://docs.python.org/3/library/index.html) and such **third-party** packages is that in the first case, the code goes through a much more formalized review process and is officially endorsed by the Python core developers. Yet, many third-party projects also offer the highest quality standards and are also relied on by many businesses and researchers.Throughout this book, we will look at many third-party libraries, mostly from Python's [scientific stack](https://scipy.org/about.html), a tightly coupled set of third-party libraries for storing **big data** efficiently (e.g., [numpy](http://www.numpy.org/)), "wrangling" (e.g., [pandas](https://pandas.pydata.org/)) and visualizing them (e.g., [matplotlib](https://matplotlib.org/) or [seaborn](https://seaborn.pydata.org/)), fitting classical statistical models (e.g., [statsmodels](http://www.statsmodels.org/)), training machine learning models (e.g., [sklearn](http://scikit-learn.org/)), and much more.Below, we briefly show how to install third-party libraries.  [numpy](http://www.numpy.org/) Library [numpy](http://www.numpy.org/) is the de-facto standard in the Python world for handling **array-like** data. That is a fancy word for data that can be put into a matrix or vector format.As [numpy](http://www.numpy.org/) is *not* in the [standard library ](https://docs.python.org/3/library/index.html), it must be *manually* installed, for example, with the [pip](https://pip.pypa.io/en/stable/) tool. As mentioned in [Chapter 0 ](https://nbviewer.jupyter.org/github/webartifex/intro-to-python/blob/master/00_intro_00_content.ipynbMarkdown-vs.-Code-Cells), to execute terminal commands from within a Jupyter notebook, we start a code cell with an exclamation mark.If you are running this notebook with an installation of the [Anaconda Distribution](https://www.anaconda.com/distribution/), then [numpy](http://www.numpy.org/) is probably already installed. Running the cell below confirms that.<jupyter_code>!pip install numpy<jupyter_output><empty_output><jupyter_text>[numpy](http://www.numpy.org/) is conventionally imported with the shorter **idiomatic** name `np`. The `as` in the import statement changes the resulting variable name. It is a shortcut for the three lines `import numpy`, `np = numpy`, and `del numpy`.<jupyter_code>import numpy
dir(numpy)
# C++, C, Java -performance
# data-science
import numpy as np
np.zeros(1)
#alias<jupyter_output><empty_output><jupyter_text>`np` is used in the same way as `math` or `random` above.<jupyter_code>numbers = [7, 11, 8, 5, 3, 12, 2, 6, 9, 10, 1, 4]
type(numbers)
numbers<jupyter_output><empty_output><jupyter_text>Let's convert the above `numbers` list into a vector-like object of type `numpy.ndarray`.<jupyter_code>vec = np.array(numbers)
vec
type(vec)<jupyter_output><empty_output><jupyter_text>[numpy](http://www.numpy.org/) somehow magically adds new behavior to Python's built-in arithmetic operators. For example, we may now [scalar-multiply ](https://en.wikipedia.org/wiki/Scalar_multiplication) `vec`.[numpy](http://www.numpy.org/)'s functions are implemented in highly optimized C code and, therefore, are fast, especially when dealing with bigger amounts of data.<jupyter_code>2 * vec<jupyter_output><empty_output><jupyter_text>This scalar multiplication would "fail" if we used a plain `list` object like `numbers` instead of an `numpy.ndarray` object like `vec`. The two types exhibit different **behavior** when used with the same operator, another example of **operator overloading**.<jupyter_code>2 * numbers  # surprise, surprise<jupyter_output><empty_output><jupyter_text>[numpy](http://www.numpy.org/)'s `numpy.ndarray` objects integrate nicely with Python's built-in functions (e.g., [sum() ](https://docs.python.org/3/library/functions.htmlsum)) or functions from the [standard library ](https://docs.python.org/3/library/index.html) (e.g., [random.choice() ](https://docs.python.org/3/library/random.htmlrandom.choice)).<jupyter_code>sum(vec)
import random
random.choice(vec)<jupyter_output><empty_output><jupyter_text>Local Modules and Packages For sure, we can create local modules and packages. In the repository's main directory, there is a [*sample_module.py* ](https://github.com/webartifex/intro-to-python/blob/master/sample_module.py) file that contains, among others, a function equivalent to the final version of `average_evens()`. To be realistic, this sample module is structured in a modular manner with several functions building on each other. It is best to skim over it *now* before reading on.To make code we put into a *.py* file available in our program, we import it as a module just as we did above with modules in the [standard library ](https://docs.python.org/3/library/index.html) or third-party packages.The *name* to be imported is the file's name except for the *.py* part. For this to work, the file's name *must* adhere to the *same* rules as hold for [variable names ](https://docs.python.org/3/reference/lexical_analysis.htmlidentifiers) in general.What happens during an import is as follows. When Python sees the `import sample_module` part, it first creates a *new* object of type `module` in memory. This is effectively an *empty* namespace. Then, it executes the imported file's code from top to bottom. Whatever variables are still defined at the end of this, are put into the module's namespace. Only if the file's code does *not* raise an error, will Python make a variable in our current location (i.e., `mod` here) reference the created `module` object. Otherwise, it is discarded. In essence, it is as if we copied and pasted the file's code in place of the import statement. If we import an already imported module again, Python is smart enough to avoid doing all this work all over and does nothing.<jupyter_code>import sample_module
dir(sample_module)
import sample_module as mod
dir(mod)<jupyter_output><empty_output><jupyter_text>Disregarding the dunder-style attributes, `mod` defines the five attributes `_default_scalar`, `_scaled_average`, `average`, `average_evens`, and `average_odds`, which are exactly the ones we would expect from reading the [*sample_module.py* ](https://github.com/webartifex/intro-to-python/blob/master/sample_module.py) file.A convention when working with imported code is to *disregard* any attributes starting with an underscore `_`. These are considered **private** and constitute **implementation details** the author of the imported code might change in a future version of his software. We *must* not rely on them in any way.In contrast, the three remaining **public** attributes are the functions `average()`, `average_evens()`, and `average_odds()` that we may use after the import.<jupyter_code>dir(mod)<jupyter_output><empty_output><jupyter_text>We use the imported `mod.average_evens()` just like `average_evens()` defined above. The advantage we get from **modularization** with *.py* files is that we can now easily reuse functions across different Jupyter notebooks without redefining them again and again. Also, we can "source out" code that distracts from the storyline told in a notebook.<jupyter_code>mod.average_evens
help(mod.average_evens)
mod.average_evens(numbers)
mod.average_evens(numbers, scalar=2)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1068247626727266, 'fraction_numerical': 0.02102041564344097, 'mean_word_length': 5.551446752311202, 'pattern_counts': {'":': 0, '<': 334, '<?xml version=': 0, '>': 337, 'https://': 108, 'lorem ipsum': 0, 'www.': 14, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '83', 'dir': 'jupyter-structured-clean-dedup', 'id': '100154', 'license': 'MIT', 'n_tokens_mistral': 17249, 'n_tokens_neox': 15836, 'n_words': 6035, 'path': 'Class_Contents/Classes/02_functions_00_content.ipynb', 'repo_name': 'lehotcd/intro-to-python', 'size': '54566'}