> For the complete documentation index, see [llms.txt](https://curropb.gitbook.io/python-notes/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://curropb.gitbook.io/python-notes/python-lesson-6.md).

# Python Lesson 6

## Lesson outline

1. Lambda functions
2. Errors and exception handling
3. Composing functions
4. Recursive functions
5. Iterators and Generators
6. Function decorators
7. Currying arguments
8. Exercises

## Lambda functions

Python encourages a *functional* programming approach, it treats the functions as objects and facilitates the use of functions as arguments. If you have three different functions

```
def f1(x):
    return x**2
def f2(x):
    import numpy as np
    return np.log10(x)
def f3(x):
    return x**2/(x**2 + 1.0)
def ffunc(x, fa = f1, fb = f2, fc = f3):
    return fc(fb(fa(x)))
######
######
print(ffunc(2.0))
print(ffunc(2.0, fa=f3, fb=f1, fc=f2))
##
```

In this case we can introduce the so called Python *anonymous* or *lambda* functions, to avoid defining functions `f1`, `f2`, and `f3`. These are oneliners consisting of a single statement whose result is the value returned. They are defined using the `lambda` keyword that implies the definition of an anonymous function. The syntax is `lambda` /argumentlist/\~: expression. These are called anonymous functions because, lacking the `def` keyword, they have no name. We can use the previous example and introduce as arguments anonymous functions

```
print(ffunc(2.0, fa=lambda z: z**2/(z**2 + 1.0) , fb= lambda z: z**2, fc=lambda z: np.log10(z)))
print(ffunc(2.0, fa=lambda z: z**2/(z**2 + 1.0) , fb= lambda z: z**4, fc=lambda z: np.log(z)))
##
```

The use of *lambda* functions is most often done together with the `map` or `filter` functions (Beware that in most, if not all cases, these constructions can be replaced by a list comprehension). Let's see how to combine anonymous functions with the `map` function.

The `map` function syntax is `map(` *ffunction* `,` *sequence* `)`, and it applies *ffunction* to each one of the *sequence* elements. The output of this function is an iterator (however, before Python 3, a list was returned). We can illustrate the use of map using the function that converts from degrees Farenheit to Kelvin defined in the previous lesson.

```
def Fahren_2_Kelvin(Temp):
    ''' 
    Function to transform from degrees Fahrenheit to degrees Kelvin.

    Input: 

	  Temp   ::   Temperature expressed in degrees Fahrenheit.
    '''     
    return ((Temp - 32.) * (5./9.)) + 273.15 # Notice that 5/9 and 5./9. are not necessarily equal... (Python 2.7)
####
temperatures_in_F = [32, 22, 100, 23, 231, 86]
temperatures_in_K = list(map(Fahren_2_Kelvin, temperatures_in_F))
print(temperatures_in_F)
print(temperatures_in_K)
```

The use of a *lambda* function makes unnecessary to define the `Fahren_2_Kelvin` function

```
temperatures_in_F = [32, 22, 100, 23, 231, 86]
temperatures_in_K = list(map(lambda Temp: ((Temp - 32.) * (5./9.)) + 273.15, temperatures_in_F))
print(temperatures_in_F)
print(temperatures_in_K)
```

The `lambda` function can have several arguments and this can be combined with the possibility of applying `map` to several lists.

```
list_A = [10,11,12,13]
iter_B = range(4)
list_C = [0,2,4,6,8,10]
list(map(lambda x, y, z: x - 10 + y + z, list_A, iter_B, list_C))
```

Note that if the lists have unequal lengths, the resulting iterator ends at the final point of the shortest list.

The `filter` function allows for a simple and elegant way of selecting which elements in a sequence evaluate to `True` under a given conditional statement. The syntax is `filter(` *function* `, ~ /sequence/ ~)`. In the following example we filter the even values in the 20-th row of the Pascal triangle

```
pt20 = [1,
 19,
 171,
 969,
 3876,
 11628,
 27132,
 50388,
 75582,
 92378,
 92378,
 75582,
 50388,
 27132,
 11628,
 3876,
 969,
 171,
 19,
 1]
list(filter(lambda x: (x+1)%2, pt20))
```

The result of `filter` is also an iterator.

## Errors and exception handling

A possible tool to avoid errors in your programs, making them more foolproof, is `assert`. The syntax of this command is `assert (condition), "Warning message string"`. When the condition evaluates to `True` the program continues, however if it is `False` the warning message is print and the program stops with and `AssertionError` message. The following line of code check whether the `time` variable is positive or zero before the program continues running

```
assert (time >= 0), "Negative time value. Not allowed."
```

This allows for an easy check of your program input to test if the values are sound.

Sometimes, specially when user input is involved, the input may be not what the program expects and you can make the program digest the input and not die miserably. Imagine you expect the user to provide a float or integer as `time` value. Notice that when you read with `input` a value it is recorded as a string.

```
time_string = input(prompt="time parameter value = ")
time = float(time_string)
print("time = {0}".format(time))
```

If the user provides a non numerical value the code crashes with a *ValueError: could not convert string to float*. If we want to avoid this we can use a `try/except` block

```
def try_float(string_value):
    try:
	return float(string_value)
    except:
	return string_value
#
time_string = input(prompt="time parameter value = ")
#
time = try_float(time_string)
print("time = {0}".format(time))
```

You can specify wich kind of exception are you trapping with the syntax `except ValueError`. You can also trap several exception types including them in a tuple.

You can make use of this to keep asking for a value until it is of the correct type.

```
while (1):
    #
    time_string = input(prompt="time parameter value = ")
    #
    try:
	time = float(time_string)
	break
    except:
	print("Not a number. Try again.")
#
print("time = {0}".format(time))
```

You can have also some code block run independently of the success or failure of the `try` block using `finally` or code that runs if the `try` block is successful using `else`.

```
trials = 0
while (1):
    #
    time_string = input(prompt="time parameter value = ")
    #
    try:
	time = float(time_string)
	break
    except:
	print("Not a number. Try again.")
    else:
	print("Okay. That was a valid number.")
    finally:
	trials += 1
#
print("time = {0}. You needed {1} trials.".format(time, trials))
```

If needed, you can stop your `Python` script raising an exception *e.g.* \~Exception("Argument ", x, " is not a float.")

```
def Try_Float(value):
    #
    try: 
	return float(value)
    except:
	raise Exception("Argument ", value, " is not a positiver integer.")
```

A `Python` script can also be forced to end using the `sys` library

```
def Try_Float(value):
    import sys
    try: 
	return float(value)
    except:
	print("Error. Not a valid input.")
	sys.exit(1)
```

## Composing functions in Python

We can define the composition of two functions of a single argument as follows

```
def compose(g, f):
    def h(x):
	return g(f(x))
    return h
```

In this way, we can combine the `Fahren_2_Kelvin` and `Try_Float` functions

```
h = compose(Fahren_2_Kelvin, Try_Float)
#
print(1, h("10"))
print(2, h(10))
print(3, h("10.0e2"))
print(4, h("10a"))
```

If the function `f` has several variables, this can be coped with using tuple references

```
def compose(g, f):
    def h(*args, **kwargs):
	return g(f(*args, **kwargs))
    return h
##
```

If both functions `f` and `g` have several variables, then

```
def compose(g, f):
    def h(*args, **kwargs):
	return g(*f(*args, **kwargs))
    return h
##
```

For example, the following function computes the parametric *dumbell curve*

```
def Dumbell_Curve_Parametric(a,t):
    x = t
    y = a*t**2*np.sqrt(1-t**2)
    return x,y
t_vals = np.linspace(0,1,1000)
x, y = Dumbell_Curve_Parametric(1.2, t_vals)
plt.plot(x,y)
plt.plot(x,-y)
```

The `dist` function computes the distance to the origin of a point in the plane

```
def dist(x,y):
    return np.sqrt(x**2 + y**2)
```

Therefore, we can compute the distance to the origin of the points in the dumbell curve

```
h = compose(dist, Campbell_Curve_Parametric)
##
t_vals = np.linspace(0,1,1000)
plt.plot(t_vals,h(1.2,t_vals))
```

## Recursive functions

A recursive function is a function that calls itself during its execution. This implies that, to avoid falling into pitfalls, the function ought to have a valid termination condition. A function that is often used to exemplify recursion is the factorial function *n! = n·(n-1)·…·2·1*. We can compute the factorial in a simple iterative way as follows (please, be aware that this is a simple implementation of the function and that is neither terribly efficient nor accurate)

```
def non_rec_factorial(n):
    result = 1
    for iteration in range(2,n+1):
	result *= iteration
    return result
```

This is a clear example where recursion can be used as *n! = n·(n-1)!* and the termination condition is *0! = 1*. Therefore a recursive implementation of the factorial is

```
def rec_factorial(n):
    if n==0:
	return 1
    else:
	return n*rec_factorial(n-1)
```

In this function the termination condition is always fullfilled if the *n* value is an integer. We can benchmark both functions using the `%timeit` magic function

```
%timeit non_rec_factorial(21)
# 997 ns ± 76.4 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
#
%timeit rec_factorial(21)
#2.2 µs ± 36.4 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```

The recursive function is roughly twice slower than the non-recursive one. We can better understand this using the Padovan sequence calculation. The Padovan sequence is the sequence of integers defined by the values *P(n) = P(n-2) + P(n-3)* with the initial values *P(0) = P(1) = P(2) = 1*. Therefore *P = 1, 1, 1, 2, 2, 3, 4, 5, 7,…* (sequence A000931 in the OEIS).\
We can program the *n*-th term of this sequence in an iterative and recurrent way

```
def rec_Padovan(n):
    '''Recursive implementation of the Padovan sequence.'''
    if n <= 3:
	return 1
    else:
	return rec_Padovan(n-2) + rec_Padovan(n-3)
#######
def iter_Padovan(n):
    result = [1, 1, 1]
    #
    for n_val in range(4,n+1):
	new_term = result[0] + result[1]
	result = [result[1], result[2], new_term]
    return result[2]
######
%timeit rec_Padovan(50)
# 94.2 ms ± 746 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
%timeit iter_Padovan(50)
# 5.72 µs ± 15 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
```

In this case the recursive function is clearly less efficient than the iterative one. The reason of this difference is that when we use the recursive implementation we need to compute several times the same values. For example, if we are computing *P(10)* one needs *P(8)* and *P(7)*, and if you check the different values of *P(n)* needed to get the final result it is obvious that some values are computed several times, imposing a significant computational burden. This can be improved using *memoization*, i.e. saving the already computed values for example in a dictionary.

```
Padovan_Sequence = {1:1, 2:1, 3:1}
def rec_save_Padovan(n):
    '''Recursive implementation of the Padovan sequence.'''
    if n not in Padovan_Sequence.keys():
	Padovan_Sequence[n] = rec_save_Padovan(n-2) + rec_save_Padovan(n-3)
    return Padovan_Sequence[n]
###
%timeit rec_save_Padovan(50)
# 123 ns ± 3.74 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
```

The new implementation is *10⁶* times faster than the previous recursive one and even roughly 50 times faster than the iterative implementation. This will improve for larger argument values.

## Iterators and iterables versus generators

The first thing to understand in this section is what is the difference between *iterables* and *iterators*. As we have already seen, we can iterate -for example with a `for` loop- over different objects, e.g. a list, an ndarray, a tuple, or a set. All these objects are *iterables*. However, not all *iterables* are also *iterators* though the reverse is true, an *iterator* is always an *iterable*. The difference lies in the fact that iterators have an associated `__next__` method that is run when the `next()` function is applied to the iterator and they can be created from iterables using the `iter()` function on them. Therefore, an iterator returns one piece of data at a time, when requested by the user.

```
# List :: iterable but not iterator
lst = [0,1,2,3,4,5]
next(lst)       # Nope
print(sum(lst)) # OK

# ndarray :: iterable but not iterator
ndarr = np.arange(6)
next(ndarr)       # Nope
print(sum(ndarr)) # OK

# tuple :: iterable but not iterator
tpl = (0,1,2,3,4,5)
next(tpl)       # Nope
print(sum(tpl)) # OK

# range :: iterable but not iterator
rng = range(6)
next(rng)       # Nope
print(sum(rng)) # OK

# map :: iterable and iterator
mp = map(lambda x: 2*x, range(6))
print(next(mp))       # Yeah
print(sum(mp)) # OK
```

When we use `for` or `sum` over an iterable, Python automatically transforms it into an iterator. We can also perform this transformation using `iter()` and adding the `__next__` method to the object.

```
# List :: iterable but not iterator
lst = [0,1,2,3,4,5]
iterator_lst = iter(lst)
print(next(iterator_lst)) 
print(sum(iterator_lst))

# ndarray ::
ndarr = np.arange(6)
iterator_ndarr = iter(ndarr)
print(next(iterator_ndarr))       
print(sum(iterator_ndarr)) 


# tuple ::
tpl = (1,2,3,4,5)
iterator_tpl = iter(tpl)
print(next(iterator_tpl))
print(sum(iterator_tpl))


# range :: iterable but not iterator
rng = range(6)
iterator_rng = iter(rng)
print(next(iterator_rng))
print(sum(iterator_rng))
```

You can check that calling `next` once more over these objects once the sum is performed will raise an `StopIteration` exception. You can also use the `iter` function to test if an object is an iterable as it can be applied only to iterables

```
def check_iterable(obj):
    try:
	iter(obj)
	return True
    except:
	return False
```

Another concept related to iterators is the concept of a *generator*. A generator is a type of function that helps creating iterators. We can see how can we transform lists or tuples or other iterables into iterators using `iter()`. This is done -in a transparent way for the user- everytime a `for` loop is run over a list or other iterables that are not iterators.

```
some_states = ["Connecticut", "California", "Washington", "Texas", "New York", "Ohio", "Vermont", "Florida", "Utah"]

state_iterator = iter(some_states)
while 1:
    try:
	state = next(state_iterator)
	print(state)
    except StopIteration:
	break
```

Doing the same with a hash results in iterating over the dict keys.

```
some_capitals = {"Connecticut": "Hartford", "California": "Sacramento", "Washington": "Olympia", "Texas": "Austin", "New York": "Albany", "Ohio": "Columbus", "Vermont": "Montpelier", "Florida": "Tallahassee", "Utah": "Salt Lake City", "Montana": "Helena"}

capital_iterator = iter(some_capitals)
while 1:
    try:
	state = next(capital_iterator)
	print(state, "\t-->   ", some_capitals[state])
    except StopIteration:
	break
```

The standard way to build iterators in Python is making use of *generators*. A generator is very similar to a function, with the difference that it inludes `yield` statements that is the statement that turns a function into a generator. The generator outputs a series of results instead of a single object as a regular function. The way to access the different results provided by the generator is by calling it repeteadly, as in a `for` loop. Everytime the generator is invoked the code in the generator is run until a `yield` statement is found and the corresponding output is returned. The execution stops here and proceeds once the generator is called again, until a new instance of `yield` is found. The local variables still exist and retain their previous values, which is utterly different to what happens with functions. There may be several yield instances in the generator code, and if a `return` statement is found the code will stop raising a `StopIteration` exception. The return value of a generator is an iterator, and as the `next` function is run for the generator it will provide, one-by-one, the expected output values. A simple generator that outputs the same state names that our previous list is

```
def state_names():
    yield "Connecticut"
    yield "California"
    yield "Washington"
    yield "Texas"
    yield "New York"
    yield "Ohio"
    yield "Vermont"
    yield "Florida"
    yield "Utah"
state = state_names() # state is a generator
print(next(state))
print(next(state))
print(next(state))
print(next(state))
print(next(state))
print(next(state))
print(next(state))
print(next(state))
print(next(state))
print(next(state))
```

Once the last `yield` statement has produced its ouput, a further `next` call finishes raising a `StopIteration` exception. Once you start retreiving values from a generator it cannot be reset, but you can creat a new one.

```
state = state_names() 
print(next(state))
print(next(state))
state = state_names()
print(next(state))
print(next(state))
```

A simple example of a generator that implements a counter follows

```
def counter(first_value = 0, step = 1):
    counter = first_value
    while 1:
	yield counter
	counter += step
#
#
# counter
cnt = counter(first_value=1, step=2)
for value in range(12):
    print(next(cnt), end=" ")
#
print()
# new counter
cnt = counter(first_value=0, step=0.25)
for value in range(12):
    print(next(cnt), end=" ")
```

In case the expression used in the generator is a simple one, you can use a *generator expression* which is quite similar to a list comprehension. The following example outputs sequentially the square root of odd numbers less than ten

```
genexample = (i**0.5 for i in range(1,10,2))
####
for result in genexample:
    print(result, end = ", ")
```

We can implement the Padovan sequence in a generator that will provide the corresponding term of the sequence upon invocation

```
def Padovan():
    '''Generator providing Padovan sequence terms.'''
    #
    counter = 0
    #
    terms = [1, 1, 1]
    #
    while 1:
	if counter < 3:
	    yield terms[0]
	else:
	    new_value = terms[0] + terms[1]
	    yield new_value
	    terms = [terms[1], terms[2], new_value]
	counter += 1
##############################################
padovan_gen = Padovan()
for value in range(12):
    print(next(padovan_gen), end=" ")
#
print()
# Note the termination condition
padovan_gen = Padovan()
for value in padovan_gen:
    print(value, end=" ")
    if value > 50:
	break
```

It is important to take care of including a termination condition in this case. We can do this in several ways. A possible option is to include this termination into the generator making use of a `return` statement.

```
def Padovan_n(n=10):
    '''Generator providing the first n terms of the Padovan sequence upon sequential calling.'''
    #
    counter = 0
    #
    terms = [1, 1, 1]
    #
    while 1:
	if counter < 3:
	    yield terms[0]
	elif counter == n:
	    return terms[0] + terms[1]
	else:
	    new_value = terms[0] + terms[1]
	    yield new_value
	    terms = [terms[1], terms[2], new_value]
	counter += 1
##############################################
padovan_gen = Padovan_n(15)
for value in padovan_gen:
    print(value, end=" ")
```

There is another possibility that consists of calling the original generator from another generator that implements the termination condition.

```
# Invoking a generator from a generator
def first_n_terms(generator, n = 10):
    gen = generator()
    for iteration in range(n):
	yield next(gen)
####
list(first_n_terms(Padovan, n=20))
```

You can also send a value to a generator, making use of the `send` method as in this simple example, making the generator act as a *coroutine*.

```
def coroutine_gen():
    print("Coroutine has been invoked for a first time")
    while 1:
	value = yield("thingy")
	print("Value received is", value)
####
corout = coroutine_gen()
#
print(next(corout))
print(next(corout))
print(corout.send("Howdy?"))
```

The variable `value` takes the value sent through the `send` method and it is `None` in case no value is sent. Applying the `send` method is equivalent to a `next` instance and the corresponding `yield` statement value is returned. A generator has to be started with a `next` statement before the `send` method is applied. We can make use of this method to program a generator that is a counter that can be modified at will

```
def counter(first_value = 0, step = 1):
    counter = first_value
    while 1:
	value_sent = yield counter
	#
	if value_sent == None:
	    counter += step
	else:
	    counter = value_sent
#
# counter
cnt = counter(first_value=0, step=2)
print(next(cnt))
print(next(cnt))
print(cnt.send(10))
for value in range(10):
    print(next(cnt), end=" ")
#
print()
print(cnt.send(100))
for value in range(10):
    print(next(cnt), end=" ")
#
```

From *Python 3.3* you can use the `yield from` *expr* statement, where *expr* evaluates to an iterable. This allows removing unnecessary `for` loops. For example, these two generators are completely equivalent

```
def gen_1(N = 10):
    for n in range(N):
	yield n
###################
def gen_2(N = 10):
    yield from range(N)
########################
g1 = gen_1()
g2 = gen_2()
##########################
for index in g1:
    print(index, end=", ")
print()
for index in g2:
    print(index, end=", ")
```

This allows for the combination of generators in an easy and direct way. Note that, as we remarked when noticing the difference between `range` and `arange`, a generator can provide substantial memory gains when facing intensive calculations and data reading.

## Function decorators

A decorator is a callable object able to modify a function (or a class), and upon its it returns a modified version of the function (or class). The defines and returns a function, called the wrapper. This is a simple example of a decorator

```
def example_decorator(func):
    '''
    This is a simple decorator that prepends and appends to a function a message 
    indicating the function name.
    '''
    def function_wrapper(x):
	print("Going to run function " + func.__name__ + " with argument " + str(x))
	func(x)
	print("Function " + func.__name__ + " has been executed.")
    return function_wrapper

def foo(x):
    print("Hi, function foo has been called with argument " + str(x))

print("Run undecorated foo ::")
foo("Howdy?")

print("Define decorated foo.")
foo = example_decorator(foo)  # two foo function versions!

print("Run decorated foo ::")
foo("Hello!")
```

In order to avoid having two different versions of the same function name *foo* as in our previous example, the usual syntax for decoration in Python is different. We can replace the statement `foo = example_decorator(foo)` by `@example_decorator` just in front of the decorated function.

```
@example_decorator
def foo(x):
    print("Hi, function foo has been called with argument " + str(x))

print("Run decorated foo ::")
foo("Hello!")
```

Using this syntax we can use our decorator with any single-argument function but not third-party functions that have been imported from modules. In this case you should use the previous syntax.

```
@example_decorator
def Fahren_2_Kelvin(Temp):
    ''' 
    Function to transform from degrees Fahrenheit to degrees Kelvin.

    Input: 

	  Temp   ::   Temperature expressed in degrees Fahrenheit.
    '''     
    return ((Temp - 32.) * (5./9.)) + 273.15 # Notice that 5/9 and 5./9. are not necessarily equal... (Python 2.7)
#################################
Fahren_2_Kelvin(222)
```

The previous decorator can be easily extended to accept functions with any number of arguments using tuple references

```
def example_decorator(func):
    '''
    This is a simple decorator that prepends and appends to a function a message 
    indicating the function name.
    '''
    def function_wrap(*posargs, **keywargs):
	print("Going to run function " + func.__name__)
	print("with positional arguments: ",*posargs)
	print("and keyword arguments: ", **keywargs )
	ffun = func(*posargs, **keywargs)
	print("Function result ", ffun)
	print("Function " + func.__name__ + " has been executed.")
    return function_wrap

@example_decorator
def Fahren_2_Kelvin(Temp):
    ''' 
    Function to transform from degrees Fahrenheit to degrees Kelvin.

    Input: 

	  Temp   ::   Temperature expressed in degrees Fahrenheit.
    '''     
    return ((Temp - 32.) * (5./9.)) + 273.15 # Notice that 5/9 and 5./9. are not necessarily equal... (Python 2.7)

@example_decorator
def bmi_range(weight, height):
    '''
    Body mass index

    Input: 
	weight (kg)
	height (m)
    '''
    def bmi_val(weight, height):
	return weight/height**2
    #
    bmi_value = bmi_val(weight, height)
    #
    if bmi_value < 15:
	bmi_r = "Very severely underweight"
    elif bmi_value < 16:
	bmi_r = "Severely underweight"
    elif bmi_value < 18.5:
	bmi_r = "Underweight"
    elif bmi_value < 25:
	bmi_r = "Normal(healthy weight)"
    elif bmi_value < 30:
	bmi_r = "Overweight"
    elif bmi_value < 35:
	bmi_r = "Obese Class I (Moderately obese)"
    elif bmi_value < 40:
	bmi_r = "Obese Class II (Severely obese)"
    else:
	bmi_r = "Obese Class III (Very severely obese)"
    #

Fahren_2_Kelvin(214)
bmi_range(78, 1.66)
```

A common use of decorators is to perform a test of the validity of the arguments. For example, the different implementations of the Padovan sequence we have introduced depend on a single positive integer argument. We can define a decorator that tests wether the argument is a natural number or not

```
def test_positive_integer(f):
    '''
    Decorator to test if the argument of a given function is a positive integer
    '''
    def tester(x):
	if type(x) == int and x > 0:
	    return f(x)
	else:
	    raise Exception("Argument ", x, " is not a positiver integer.")
    return tester
###############################################################################
@test_positive_integer
def rec_Padovan(n):
    '''Recursive implementation of the Padovan sequence.'''
    if n <= 3:
	return 1
    else:
	return rec_Padovan(n-2) + rec_Padovan(n-3)
#########
@test_positive_integer
def iter_Padovan(n):
    result = [1, 1, 1]
    #
    for n_val in range(4,n+1):
	new_term = result[0] + result[1]
	result = [result[1], result[2], new_term]
    return result[2]

print(rec_Padovan(10))
print(rec_Padovan(11.2))
####
print(iter_Padovan(10))
print(iter_Padovan(10.))
```

Another example where a decorator can be of interest is if we want to keep track of how many times a function has been called. This is a general case, that can cope with several positional and keyword arguments

```
def call_func_counter(f):
    def helper(*args, **kwargs):
	helper.counter += 1
	return f(*args, **kwargs)
    helper.counter = 0

    return helper
########################
@call_func_counter
def Fahren_2_Kelvin(Temp):
    ''' 
    Function to transform from degrees Fahrenheit to degrees Kelvin.

    Input: 

	  Temp   ::   Temperature expressed in degrees Fahrenheit.
    '''     
    return ((Temp - 32.) * (5./9.)) + 273.15 # Notice that 5/9 and 5./9. are not necessarily equal... (Python 2.7)
#################
@call_func_counter
def bmi_range(weight, height):
    '''
    Body mass index

    Input: 
	weight (kg)
	height (m)
    '''
    def bmi_val(weight, height):
	return weight/height**2
    #
    bmi_value = bmi_val(weight, height)
    #
    if bmi_value < 15:
	bmi_r = "Very severely underweight"
    elif bmi_value < 16:
	bmi_r = "Severely underweight"
    elif bmi_value < 18.5:
	bmi_r = "Underweight"
    elif bmi_value < 25:
	bmi_r = "Normal(healthy weight)"
    elif bmi_value < 30:
	bmi_r = "Overweight"
    elif bmi_value < 35:
	bmi_r = "Obese Class I (Moderately obese)"
    elif bmi_value < 40:
	bmi_r = "Obese Class II (Severely obese)"
    else:
	bmi_r = "Obese Class III (Very severely obese)"
    #
    return bmi_r
########################
Fahren_2_Kelvin(214)
Fahren_2_Kelvin(314)
bmi_range(78, 1.66)
print(Fahren_2_Kelvin.counter)
print(bmi_range.counter)

def farewell(expr):
    def farewell_decorator(func):
	def f_wrapper(*posargs, **kwargs):
	    print(expr)
	    print(func.__name__ + " returns:")
	    return func(*posargs, **kwargs)
	return f_wrapper
    return farewell_decorator

#########################
@farewell("Ciao")
def foo(x):
    return 0.5*((x)**2 + (x)**0.5)

foo(100)

#########################
@farewell("Sayonara baby")
def foo_2(x, y):
    return 0.5*((x)**2 + (y)**2)**0.5

foo_2(20, 22)
```

We can also avoid the use of the `@decorator` syntax as follows

```
def foo_3(x, y, z):
    return (x*y*z)**(1/3)
################
print(foo_3(20, 22, 25))
################
foo_3 = farewell("Bye")(foo_3)
print(foo_3(20, 22, 25))
```

A very interesting decorator purpose it to perform memoization in a transparent way. Let's assume that we have a function, *func*, that is computationally costly and it may be worth to save the obtained results for each evaluation. This can be easily done through a hash, and including the hash in a decorator allows for a clean and transparent implementation. We define a decorator called `memoize`

```
def memoize(func):
    memo_hash = {}
    def memoization(x):
	if x not in memo_hash:            
	    memo_hash[x] = func(x)
	return memo_hash[x]
    return memoization
```

We can combine two decorators and we will do this to check how this memoization decorator works in an example, combining it with the decorator `farewell` defined above

```
@memoize
@farewell("¡¡Hola!!")
def f(x):
    import numpy as np
    return np.sin(x)*np.exp(-x)/(x-0.5)
```

In this case we first apply to *f* the `farewell` decorator, followed by the `memoize` decorator. Therefore, everytime the function `f` is executed the `¡¡¡Hola!!!` message will be displayed. We can examine the memoization in action as follows

```
print(f(2))
print(f(2))
```

Notice how the order of the decorators is very relevant and changes the final output.

## Currying functions

*Currying* -named after the mathematician *Haskell Curry*- is a functional desing pattern. It is the technique of converting a function that takes multiple arguments into a sequence of functions that each takes a single argument. Therefore, currying a function *F = f(x,y,z)/￼ that takes three arguments, creates three functions such that /h = g(x)*, *j=h(y)* *F = j(z)* or *F = g(x)(y)(z)*.

We provide an approach to currying using the `partial` function from the `functools` library that allows for binding the arguments of a function and the `signature` function from the `inspect` library.

We first introduce `partial`. If we have a function that computed the distance of a point *(x,y,z)* given in Cartesian coordinates to the origin

```
def dist_3D(x,y,z):
    import numpy as np
    return np.sqrt(x**2+y**2+z**2)
##
```

Let's assume that we are limited to work in two dimensions, in the *z = 2* plane. Using `partial`, we can fix `z = 2` and use an anonymous function

```
from functools import partial
dist_2D = partial(dist_3D, z=2)
dist_2D(3,3)
##
```

You can also include new default values for existing keyword arguments.

Let's assume that we have a function *prdct(x,y,z) = xyz* and we intend to curry it into *cprod(x)(y)(z) = xyz*. We can perform this using `partial` as follows

```
def prodct(x,y,z):
    return x*y*z
#
prod_first = partial(prodct, 3) # binding first argument
prof_second = partial(prod_first, 5) # binding second argument
#
print(prodct(3,5,7), prof_second(7)) 
```

This has built a chain of functions but it is not a very elegant way. Let's improve it using `partial` in a recursive way as a decorator and the `signature` function that provides a list of arguments of the function

```
def prodct(x,y,z):
    return x*y**2*z**3
print(prodct(3,5,7))
```

We define and apply the decorator

```
from inspect import signature
#
def currying(function):
    #
    def inner_arg(argument):
	# check if there is only one argument
	if len(signature(function).parameters) == 1:
	    return function(argument)
	# 
	return currying(partial(function,argument))
    #
    return inner_arg
#
@currying
def prodct(x,y,z):
    return x*y**2*z**3
#
prodct(3)(5)(7)
```

## Exercises

* **Exercise 6.1:** Build a set of *N=100* random coordinate values *x*, *y*, and *z* with values between *-10* and *10*. Using an anonymous function, select which of those points are at a distance less than a given limit *dmin* to the origin.
* **Exercise 6.2:** Define a function that uses recursion to compute the n-th line of the Pascal triangle (1; 1, 1; 1, 2, 1; 1, 3, 3, 1;…).
* **Exercise 6.3:** Define a generator that, using the Eratosthenes sieve (see Exercises Lesson 4), outputs a prime number each time it is called up for prime numbers below a given integer value.
* **Exercise 6.4:** We have included memoization in the calculation of the Padovan sequence using a hash. Using a decorator make it transparent and also prepare a recursive implementation of the Tribonacci sequence, a third order sequence defined by *T\[0] = 0, T\[1] = T\[2] = 1* and *T\[n] = T\[n-1] + T\[n-2] + T\[n-3]* for *n>1*. Compare the speed of the implementations with and without the memoization decorator.
* **Exercise 6.5:** Use as a starting point the password generating function defined in Exercise 4.2. Add an argument that fix the generated parameter length. Then prepare a decorator that would (1) ensure that the argument is an integer or can be transformed into an integer; (2) check that the integer argument value is larger than or equal than six; and (3) perform the same task as in Exercise 4.2, check that there are at least two digits, two uppercase, and two lowcase characters and return the compliant password and the number of times the function has been run to get this password.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://curropb.gitbook.io/python-notes/python-lesson-6.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
