# Powerful functions

In this section we will take our first `fibonacci()`

function and enhance it little by little as we indulge ourselves with Python features.

### Functions are **objects**

We can treat our functions as if they were any other object like a list, a string or any other. In fact, everything in Python is an object! This means we can take our `fibonacci()`

function,

```
def fibonacci(n):
"""Returns the n-th Fibonacci number."""
a, b = 0, 1
for i in range(n):
a, b = b, a+b
return a
```

and assign it to another variable

```
>>> fib = fibonacci
>>> fib(8)
21
>>> fib is fibonacci
True
```

A good practice when writing functions is to add a documentation string in the first statement. It can be accessed by means of the `__doc__`

attribute.

```
>>> fibonacci.__doc__
'Returns the n-th Fibonacci number.'
```

### Functions can be **stored in data structures**

Given that Python functions are objects, they can be stored in data structures. Let’s take this simple function as well

```
def square(n):
"""Returns the squared number."""
return n**2
```

and build the following list:

```
my_functions = [fibonacci, square]
```

We can now do something like this

```
>>> my_functions
[<function __main__.fibonacci(n)>, <function __main__.square(n)>]
>>> for function in my_functions:
... print(function.__doc__)
Returns the n-th Fibonacci number.
Returns the squared number.
>>> my_functions[0](8)
21
>>> my_functions[1](8)
64
```

### Functions can be **passed to other functions**

This characteristic allows functions to be both the input and the output of other functions, empowering us to do something like evaluating the composition of the two functions that we have just defined

```
def eval_composition(f, g, n):
"""Returns f(g(n))."""
return f(g(n))
```

which works just as we would expect

```
>>> eval_composition(square, fibonacci, 8)
441
>>> eval_composition(square, fibonacci, 8) == square(fibonacci(8))
True
```

### Functions can be **nested**

Python allows functions to be defined inside other functions. This opens the door to taking our composition function even further. Instead of returning $f(g(n))$ evaluated, we can return $f \circ g$ itself:

```
def compose(f, g):
"""Returns the composition function of f and g."""
def nested(n):
"""Returns the evaluation of f(g(n))."""
return f(g(n))
return nested
```

The trick is that `nested()`

*does not exist* outside `compose()`

:

```
>>> compose
<function __main__.compose(f, g)>
>>> nested
NameError: name 'nested' is not defined
```

It turns out that we can now call $f(g(n))$ in this natural way:

```
>>> my_function = compose(square, fibonacci)
>>> my_function(8)
441
```

And last but not least, let’s make it even more beautiful. Why don’t we return the composition of an arbitrary number of functions $f_1 \circ f_2 \circ \dots \circ f_k$?

```
def compose(functions):
"""Returns the composition of a list of functions."""
def nested(n):
"""Magic happens."""
for f in reversed(functions):
n = f(n)
return n
return nested
```

In this upgraded version, we take a list of functions as the input and we loop over it in reverse order to do the composition.

```
>>> my_function = compose([fibonacci, square, square])
>>> my_function(2)
987
>>> fibonacci((2**2)**2)
987
```

**Lliçons.jutge.org**

Víctor Adell

Universitat Politècnica de Catalunya, 2023

Prohibit copiar. Tots els drets reservats.

*No copy allowed. All rights reserved.*