Functions

Introduction

In mathematics, a function is an object that accepts one or more inputs and produces one or more outputs. For example, ( f(x) = x^2 ) is a function that accepts a number and returns the square of that number. Functions in Python play a similar role, but are much richer than their mathematical counterparts. Let us quickly convert the mathematical function ( f ) into a Python function:

def f(x):
    y = x ** 2
    return y

The code given above is called the definition of function `f`. The keyword `def` is used to define functions. `f` is the name of the function. `x` is a parameter of the function. Lines 2 and 3 make up the body of the function and are indented. The body of a function is a collection of statements that describe what the function does. At line 3, the value stored in variable `y` is returned. The keyword `return` is used for this purpose.

If we run the above code, we will not get any output. Functions are not executed unless they are called. The following code demonstrates what a function call looks like:

```python
def square(x):
    y = x ** 2
    return y

print(square(2))
```

The output is:

```
4
```

`square(2)` is a function call. We use the name of the function, `square`, and pass the number `2` as an argument to it. The `x` in the function definition is called the parameter. The value that is passed to the function in the call is called the argument. This is a convention that we will follow throughout this lesson.

### A mental model to understand functions:

- **Parameters** can be thought of as the function's inputs.
- The **body of the function** can be pictured as the sequence of steps that transform the input into the output.
- The **return statement** can be thought of as a means of communicating the output to the rest of the code.

## Examples

We will look at a wide variety of function definitions. The focus will be on the syntactical aspects of function definitions.

### Functions with Multiple Parameters

```python
# This function computes the area of a rectangle.
# Length and breadth are the parameters
def area(l, b):
    return l * b
```

### Functions with No Parameters

```python
def foo():
    return "I don't like arguments visiting me!"
```

### Functions with No Return Value

```python
def foo():
    print("I don't like talking to the outside world!")

foo()
```

When the code given above is executed, we get the following output:

```
I don't like talking to the outside world!
```

Note that we didn't have to type `print(foo())`. We just had to call the function — `foo()` — since it already has the print statement inside it. But what happens if we type `print(foo())`? We get the following output:

```
I don't like talking to the outside world!
None
```

If no explicit return statement is present in a function, `None` is the default value returned by it. When the interpreter comes across the `print(foo())` statement, first the function `foo()` is evaluated. This results in the first line of the output. Since `foo()` has no explicit return statement, it returns `None` by default. That is why the second line in the output is `None`.

### A Minimal Python Function

```python
def foo():
    pass
```

`pass` is a keyword in Python. When the interpreter comes across a `pass` statement, it doesn't perform any computation and moves on to the next line. The reason this is minimal is that it has only those features that are absolutely essential for a function definition to be syntactically valid: function name and at least one statement in the body.

Such functions might seem useless at first sight, but they do have their place in programming. While writing a complex piece of code, a coder may realize that she needs to define a function to perform a specific task. But she may not know the exact details of the implementation, or it may not be an urgent requirement. In such a scenario, she will add a minimal function like the one given above in her code and name it appropriately. Implementing this function will become a task on her to-do list and will be taken up as and when the need arises.

### Functions with Multiple Return Statements

Functions could have multiple return statements, but the moment the first return is executed, control exits from the function:

```python
def foo():
    return 1
    return 2
```

`foo()` will always return `1`. Line 3 is redundant. An example of a function having multiple returns that are not redundant:

```python
def evenOrOdd(n):
    if n % 2 == 0:
        return 'even'
    else:
        return 'odd'
```

```python
print(evenOrOdd(10))
print(evenOrOdd(11))
```

The output is:

```
even
odd
```

When `evenOrOdd` is called with an even number as an argument, the return statement in line 3 is executed. When the same function is called with an odd number as an argument, the return statement in line 5 is executed.

### Functions that Return Multiple Values

```python
# Accept only positive floating point numbers
def bound(x):
    lower = int(x)
    upper = lower + 1
    return lower, upper
```

```python
y = 7.3
l, u = bound(y)
print(f'{l} < {y} < {u}')
```

The exact mechanism of what happens here will become clear when we come to the lesson on tuples. In line 8, the first value returned by `bound` is stored in `l`, and the second value returned by `bound` is stored in `u`.

### Function Definition Order

Functions have to be defined before they can be called. The function call cannot come before the definition. For example:

```python
##### Alarm! Wrong code snippet! #####
print(f(5))

def f(x):
    return x ** 2
##### Alarm! Wrong code snippet! #####
```

When the above code is executed, it throws a `NameError`. Why does this happen? The Python interpreter executes the code from top to bottom. At line 2, `f` is a name that the interpreter has never seen before, and therefore it throws a `NameError`. Recall that `NameError` occurs when we try to reference a name that the interpreter has not seen before.

### Function Calls in Expressions

```python
def square(a):
    return a ** 2

x, y, z = int(input()), int(input()), int(input())
if square(x) + square(y) == square(z):
    print(f'{x}, {y} and {z} form the sides of a right triangle with {z} as the hypotenuse')
```

### Function Calls Cannot Be Assigned Values

```python
##### Alarm! Wrong code snippet! #####
def foo():
    return True

foo() = 1
##### Alarm! Wrong code snippet! #####
```

The above code throws a `SyntaxError`.

### Nested Function Calls

Functions can be called from within other functions:

```python
def foo():
    print('I am inside foo')

def bar():
    print('I am inside bar')
    print('I am going to call foo')
    foo()

print('I am outside both foo and bar')
bar()
print('I am outside both foo and bar')
```

### Defining Functions Inside Other Functions

Functions can be defined inside other functions:

```python
def foo():
    def bar():
        print('bar is inside foo')
    bar()

foo()
```

Try calling `bar()` outside `foo`. What do you observe?

## Docstrings

Consider the following function:

```python
def square(x):
    """Return the square of x."""
    return x ** 2
```

The string immediately below the function definition is called a docstring. From the Python docs:

A docstring is a string literal that occurs as the first statement in a module, function, class, or method definition. Such a docstring becomes the `__doc__` special attribute of that object.

Ignore unfamiliar terms such as "module" and "class." For now, it is sufficient to focus on functions. Adding the docstring to functions is a good practice. It may not be needed for simple and obvious functions like the one defined above. As the complexity of the functions you write increases, docstrings can be a lifesaver for other programmers reading your code.

The docstring associated with a given function can be accessed using the `__doc__` attribute:

```python
print(square.__doc__)
```

This gives `'Return the square of x.'` as output.