# Python Lesson 7

## 1 Lesson outline

- Lambda functions
- Errors and exception handling
- Composing functions
- Recursive functions
- Iterators and Generators
- Function decorators
- Currying arguments
- Exercises

## 2 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`

/argument_{list}/~: 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.

Exercise 6.1 |
Build a set of N=100 random coordinate values x, y, and z with values in the range -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. |

## 3 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
instead of dying miserably. Imagine you expect the user to provide a
float or integer as `time`

value. Notice that whenever you read a
value using the `input`

statement, the input 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)

## 4 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, Dumbell_Curve_Parametric) ## t_vals = np.linspace(0,1,1000) plt.plot(t_vals,h(1.2,t_vals))

## 5 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 hence 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.

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;…). |

We can benchmark the previous two factorial implementations with 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(1) = 1*,
*P(2) = 0*, and *P(3) = 0*. Therefore, *P = 1, 0, 0, 1, 0, 1, 1, 1, 2, 2, 3, 4, 5, …*
(Sequence A000931 in the OEIS). We can program the *n*-th term of
this sequence in iterative and recurrent ways

def iter_Padovan(n): # # result = [1, 0, 0] # if n < 4: return result[n-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] ########### ########### def rec_Padovan(n): '''Recursive implementation of the Padovan sequence.''' if n == 1: return 1 elif n == 2: return 0 elif n == 3: return 0 else: return rec_Padovan(n-2) + rec_Padovan(n-3) ## %timeit rec_Padovan(50) %timeit iter_Padovan(50)

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:0, 3:0} 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)

The new implementation is a million times faster than the previous recursive one and roughly 50 times faster than the iterative implementation. This difference will grow for larger argument values.

Exercise 6.3 |
The Euclid’s algorithm for the calculation of the greatest common divisor (GCD) of two integers, (a, b with a > b) can be programmed in a recursive way: (1) Divide a by b and obtain the quotient q and the remainder r; (2) If r is zero then b is the GCD, else calculate GCD(b,/r/). Taking into account that the least common multiple (LCM) of two integers can be computed as the quotient of a b and the GCD(a,/b/), prepare a recursive function to compute the GCD and LCM of two given integers, checking that both arguments are integers and that a > b. |

## 6 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*. An *iterator* is what does the actual iterating and they 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

Other iterators in `Python 3`

are provided by the built-in functions `enumerate`

, `zip`

, and `reversed`

.

When we use `for`

or `sum`

over an iterable, Python automatically
transforms it into an iterator. We can also perform this
transformation using `iter()`

, that adds 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, 0, 0] # while 1: if counter < 3: yield terms[counter] 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, 0, 0] # while 1: if counter < 3: yield terms[counter] 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.

Exercise 6.4 |
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. |

## 7 Function decorators

A decorator is a callable object that returns a modified version of the function (or class). A decorator 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 ################################# 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 @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 ################# @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.

Exercise 6.5 |
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.6 |
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. |

## 8 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)

## 9 Exercises

- Exercise 6.1
- Build a set of
*N=100*random coordinate values*x*,*y*, and*z*with values in the range*-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
- The Euclid’s algorithm for the calculation of the greatest common divisor (GCD) of two integers, (
*a*,*b*with*a > b*) can be programmed in a recursive way: (1) Divide*a*by*b*and obtain the quotient*q*and the remainder*r*; (2) If*r*is zero then*b*is the GCD, else calculate GCD(*b*,/r/). Taking into account that the least common multiple (LCM) of two integers can be computed as the quotient of*a b*and the GCD(*a*,/b/), prepare a recursive function to compute the GCD and LCM of two given integers, checking that both arguments are integers and that*a > b*. - Exercise 6.4
- 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.5
- 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.6
- 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.

Created: 2022-10-01 Sat 23:07