<jupyter_start><jupyter_text>FunctionsWe have already used functions.For example, we have used the `round` function:<jupyter_code>a = 3.1415
# Call the "round" function
b = round(a, 2)
b<jupyter_output><empty_output><jupyter_text>We often need to define our own functions. Before we do, we need to go intomore detail about what functions are for, and what they are.  Functions are like named recipesA function is a named recipe.  It is a name we give to a set of steps tofollow, a piece of code to run.Thanks to [the Berkeleyteam](https://www.inferentialthinking.com/chapters/08/Functions_and_Tables)for this metaphor.A recipe is the *procedure* to go from *ingredients* to a *meal*.A function is the *procedure* to go from the *arguments* to the*return value*.For example, I might have a recipe with the procedure to go from theingredients: two eggs; butter; and cheese \- to the meal \- a cheeseomelette.The function `round` has the procedure to go from the arguments - twonumbers, to the return value, which is the value of the first argumentrounded to the number of digits specified in the second.I could call my recipe "two egg cheese omelette", or "recipe number4".  Whatever I called it, it would be the same recipe.  I mightprefer a name that describes what the recipe makes, to help meremember.Likewise, the name `round` refers to a procedure above.  I could give itanother name, like `my_function`, but `round` is a good name, because it helpsme remember what the procedure does.I say `round` has a procedure, but we can't see what that procedure is, it'sburied inside the internal workings of Python.Now we are going to write our own function, where we can see theprocedure.  Revision on variablesHere is an *assignment statement*:<jupyter_code>a = 2<jupyter_output><empty_output><jupyter_text>As we know, we can read this as "The variable 'a' gets the value 2".We also know that we have, on the left, a variable name, 'a', and onthe right, an expression, that gives a value.In this case, the expression on the right is `2`. Python evaluatesthis expression, to make its own internal computer representation of theinteger 2.  Call this: Computer Representation (CR) of int 2.After Python executes this statement, the name "a" points to the CR of int 2.To continue the revision:<jupyter_code>b = a * 4<jupyter_output><empty_output><jupyter_text>The right side `a * 4` is an expression.  Python evaluates theexpression.  First it gets the value of `a`.  This is the CR of int 2.Next it gets the value of `4`.  This is the CR of int 4. Then itmultiplies these results to get an CR of int 8."b" now points to the CR of int 8.Finally:<jupyter_code>a = 3<jupyter_output><empty_output><jupyter_text>"a" no longer points the CR of int 2, it points to the CR of int 3.What value does "b" have now?The same value as it had before.  It pointed to the CR of int8 before.  Changing `a` has no effect on `b`. Defining a functionWe define our function called `double`.  It accepts one argument(ingredient), call that `x`.  It's procedure is to multiply theargument by 2.  The return value is the argument multiplied by 2.Here it is:<jupyter_code>def double(x):
    d = x * 2
    return d<jupyter_output><empty_output><jupyter_text>Let's look at the first line:```def double(x):```The first word `def` tells Python we are defining a function.The next word `double` is the name we will give to our function.Between the parentheses, we have the function signature.  This specifies how many arguments the function has.  In our case, there is only one argument, named `x`.Finally there is a colon `:` signifying the end of the signature.As in [for loops](../05/iteration), the colon signifies that the nextbit of code must be [indented](../03/indentation).Here is the indented part:```    d = x * 2    return d```This is the *body* of the function.   It gives the function procedure; it defines what the function will do to its arguments, and what result it should return.For example, here we *call* the function we just created:<jupyter_code>double(4)<jupyter_output><empty_output><jupyter_text>Notice that `double(4)` is a [call expression](../02/Calls).So, what just happened?1.  Python finds what `double` points to.  It points to internal    representation of our function (procedure).1.  Next it sees the parenthesis `(` and sees that we want to *call*    our function.1.  Now Python knows we want to call the function, it knows that    there are one or more *expressions* inside the parentheses.  In    our case there is one, `4`.  As usual, it evaluates this    expression to the CR of int 4.1.  Now Python does the *call*.  To do this it:    1. Puts itself into *function world* (more on this later).    2. Sets the new variable `x` to have the value CR of int 4, from       above.    3. Executes the code in the function body (procedure).    1. The first line `d = x * 2` is an assignment statement.  `x`       evaluates to CR of int 4, 2 evaluates to CR of int 2, so `d`       has the value CR of int 8.  This is how the statement would       work anywhere in Python, function body or not.    1. The next line starts with `return`.  This is a *return       statement*. When Python sees a `return` statement, it evaluates       the expression to the right, to get the *return value*, then    5. Pulls itself out of *function world*.    6. Gives the *return value* as the final result of the call       expression.  This is CR of int 8.We can run the function with any values for the argument.<jupyter_code>double(2)<jupyter_output><empty_output><jupyter_text>This time round, everything happened in the same way as before, exceptPython found the *argument* inside the parentheses evaluated to CR ofint 2.  Thus, in *function world*, `x` gets the value CR of 2, and the *return value* becomes CR of int 4. Function worldI cryptically used the term *function world* for the state that Python goes into when it calls a function.This state has two important features. Variables defined in functions have local scopeThe first feature of function world is that all variables definedinside function world, get thrown away when we leave function world.We can see this if we run the following code in a notebook cell.  Thiscode runs in our usual top-level world, and so, not inside a function.<jupyter_code>d<jupyter_output><empty_output><jupyter_text>Notice that, in the function, we set `d` to point to the result of `x * 2`.  We called the function a couple of times, so we executed this statement a couple of times.  But the `d` in the function, gets thrown away, when we come back from function world.In technical terms, this is called *scope*.   The *scope* ofa variable, is the pieces of code in which the variable is visible.`d` can only be seen inside the function.  Its scope is the function.  We can also say that its scope is piece of code where it is defined, that is, it has *local* scope.The same is true for `x`, the argument variable:<jupyter_code>x<jupyter_output><empty_output><jupyter_text>The function has limited access to variables outside the functionWe have not seen this yet, but function world has limited access to variables defined at the top level.We won't go into much detail here, but the summary is that functions can see the values of variables defined at the top level, but they can't change what top level variables point to.  For example, say you have a variable `a` at the top level.  A function can see and use the value of `a`, but it cannot change top-level `a` to point to a different value.   We will come back to this later. Python checks the function signatureThe signature for `double` is `(a)`.  That tells Python to expect oneand only one argument.  If we try to call it with no arguments(nothing inside the parentheses), we get an error:<jupyter_code>double()<jupyter_output><empty_output><jupyter_text>If we try and call it with more than one argument, we get an error.  We separate arguments with commas.<jupyter_code>double(2, 3)
double(2, 3, 4)<jupyter_output><empty_output><jupyter_text>Function arguments are expressionsRemember that Python knows that the arguments to a function areexpressions, and evaluates them, before running the function.For example:<jupyter_code>double(2 + 3)<jupyter_output><empty_output><jupyter_text>All the procedure is the same as above.  Python evaluates theexpression `2 + 3`, to get CR of int 5, then goes into function world,sets `x` to have the value CR of int 5, and continues from there. Functions can have many argumentsNow we define a new function:<jupyter_code>def multiply(a, b):
    return a * b<jupyter_output><empty_output><jupyter_text>The new thing here is that the function signature `(a, b)` has two arguments, separated by commas.  We need to give the function two values, when we call it:<jupyter_code>multiply(2, 3)<jupyter_output><empty_output><jupyter_text>If we do not give it exactly two arguments, we get an error.<jupyter_code>multiply(2)
multiply(2, 3, 4)<jupyter_output><empty_output><jupyter_text>Functions can have no argumentsPerhaps the recipe analogy breaks down here, but sometimes functions take no arguments.  For example:<jupyter_code>import numpy as np
# Notice - nothing between the parentheses

def biased_coin():
    # A single random number
    r = np.random.uniform()
    # A biased coin
    result = r < 0.45
    return result<jupyter_output><empty_output><jupyter_text>When we call the function, we have no arguments, so no expressions between the parentheses.<jupyter_code>biased_coin()<jupyter_output><empty_output><jupyter_text>As you would expect by now, if we try and send an argument, Python will complain:<jupyter_code>biased_coin(0.45)<jupyter_output><empty_output><jupyter_text>Without a return statement, functions return NoneOur functions so far all have a `return` statement.  This is not true of every function.If your function does not have a return statement, the function returns the value [None](none).<jupyter_code>def silent_addition(first, second):
    result = first + second<jupyter_output><empty_output><jupyter_text>Notice that the body of this function has no `return` statement.  When we call it, it returns `None`:<jupyter_code>result = silent_addition(10, 12)
result
result is None<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.06998050682261209, 'fraction_numerical': 0.008771929824561403, 'mean_word_length': 4.591825613079019, 'pattern_counts': {'":': 0, '<': 82, '<?xml version=': 0, '>': 81, 'https://': 1, 'lorem ipsum': 0, 'www.': 1, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '20', 'dir': 'jupyter-structured-clean-dedup', 'id': '320372', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 2961, 'n_tokens_neox': 2828, 'n_words': 1484, 'path': 'notebooks/07/functions.ipynb', 'repo_name': 'matthew-brett/cfd-uob', 'size': '10260'}