Python Lesson 6
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
/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.
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:16