Workshop 9

Functions and recursion. Lambda-functions. Named parameters. Namespaces.

Functions

Definition: A function is a rule of taking zero and more inputs, performing some actions with the inputs and returning a corresponding output.

In Python, we typically define functions using def:

In [ ]:
def double(x):
    """this is where you can put an optional docstring
    that explains what the function does.
    for example, this function multiplies its input by 2"""
    return x * 2
    
y = double(10) # doubles integer value 10
y # prints it
Out[ ]:
20

A function may or may not return an output value. For example, the fib function below calculates and prints the Fibonacci series. It does not return any output values.

In [ ]:
def fib(n): # write Fibonacci series up to n
    """Print a Fibonacci series up to n."""
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a + b
    print()

# Now call the function we just defined:
fib(100)
0 1 1 2 3 5 8 13 21 34 55 89 

When a functions needs to return an output value, it uses the return operator. In the example below, the fib2 function returns a list of the numbers of the Fibonacci series instead of printing them:

In [ ]:
def fib2(n): # return Fibonacci series up to n
    """Return a list containing the Fibonacci series up to n."""
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a) # see below
        a, b = b, a + b
    return result


f100 = fib2(100) # call it
f100 # write the result
Out[ ]:
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Task 1 Write a Python function to check
whether a number is in a given range

Input
number_in_range(x, start, until)

Output
YES, 4 is in the range from 1 to 6

Task 2 Write a Python function that accepts
a string and calculate the number of upper case
letters and lower case letters.

Input
The quick Brown Fox

Output
No. of Upper case characters : 3
No. of Lower case Characters : 13

A Hint: use .isalpha() method (also .islower() and .isupper())

Recursion

Python allows functions to call themselves to loop. This technique is known as recursion.

For example, here is how we can use recursion to write a function that sums a list of numbers:

In [ ]:
def mysum(L):
    if not L:
        return 0
    else:
        return L[0] + mysum(L[1:]) # Call mysum recursively

x = mysum([1,2,3,4,5])
x
Out[ ]:
15

Here is how we can calculate a factorial using recursion:

In [ ]:
def factorial(n):
    if n < 1:   # base case
        return 1
    else:
        return n * factorial(n - 1)  # recursive call


for i in range(10):
    print(f"{i}! = {factorial(i)}")
    print("{}! = {}".format(i, factorial(i)))
    

Task 3 Write a Python program to calculate the value of 'a' to the power 'b'
Use recursion.

Input:
power_func(3,4)

Output
81

Lambdas

Python functions are first-class, which means that we can assign them to variables and pass them into functions just like any other arguments:

In [ ]:
def double(x):
    return x * 2

def apply_to_one(f):
    """calls the function f with 1 as its argument"""
    return f(1)

print(double(2))
my_double = double          # refers to the previously defined 
print(my_double)

x = apply_to_one(my_double) # equals 2

print(x)
4
<function double at 0x7f5d6e6ed598>
2

It is also easy to create short anonymous functions, or lambdas:

In [ ]:
def add_four(x):
  return x + 4


def apply_to_one(f):
    return f(1)

def apply_to_two(f):
    return f(2)

a = 4
"4"
y = apply_to_one(add_four) # equals 5


print(y)
5

You can assign lambdas to variables, although most people will tell you that you should just use def instead:

In [ ]:
another_double1 = lambda x: 2 * x     # don't do this
def another_double2(x): return 2 * x  # do this instead

print(another_double1(4))
print(another_double2(6))
8
12

We can pass lambdas to other functions as arguments and return them from functions as return values.

In [ ]:
def doubler(f):
    return lambda x: 2 * f(x)


g = doubler(lambda x: x + 1)
print(g)
print(g(-1))
print(g(0))

# it's ok to have a little headache at this point, don't give up

In practice, using functions as variables can be helpful for optimization.

In [ ]:
import numpy as np
from scipy.optimize import fmin
import math

def simple_function(x):
  # minimum is at x = 2 (-b/2a)
  return x*x - 4*x + 1

def complex_function(x):
  # x is a vector
  # the coordinates of a point are (x[0], x[1])
  exp = (math.pow(x[0], 2) + math.pow(x[1], 2)) * -1
  return math.exp(exp) * math.cos(x[0] * x[1]) * math.sin(x[0] * x[1])


(xopt, fopt, *info) = fmin(simple_function,np.array([0]), full_output=1)
print('The minimum is achieved at the point x = {}. The minimum value is {}'.format(xopt[0], fopt))

(xopt, fopt, *info) = fmin(complex_function,np.array([0,0]), full_output=1)
print('The minimum is achieved at the point x = {}, y = {}. The minimum value is {}'.format(xopt[0], xopt[1], fopt))
Optimization terminated successfully.
         Current function value: -3.000000
         Iterations: 27
         Function evaluations: 54
The minimum is achieved at the point x = 2.000000000000002. The minimum value is -3.0
Optimization terminated successfully.
         Current function value: -0.161198
         Iterations: 60
         Function evaluations: 113
The minimum is achieved at the point x = 0.6266570069062677, y = -0.6266309521432152. The minimum value is -0.16119847068945856

Function parameters

A function can has from zero to many arguments.

In [ ]:
def func0(): # Zero parameters
    pass

def func1(x): # One parameter
    pass

def func5(a, b, c, d, e): # Five parameters
    pass

Sometimes you may want to pass an arbitrary number of arguments:

In [ ]:
print(1, 2, 3, 4, 5, 6 ,7, 8)
def funcx(*args):  
    print(type(args))
    for arg in args:  
        print(arg) 
    
funcx(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
1 2 3 4 5 6 7 8
<class 'tuple'>
1
2
3
4
5
6
7
8
9
10

Function parameters can be given default arguments, which only need to be specified when you want a value other than the default:

In [ ]:
def my_print(message="my default message"):
    print(message)


my_print("hello") # prints 'hello'
my_print(message="keyword message")        # prints 'my default message'
hello
keyword message

It is sometimes useful to specify arguments by name:

In [ ]:
def subtract(a=0, b=0):
    return a - b

# Passing arguments without specifying their names:
print(subtract())           # prints 0
print(subtract(10, 5))      # prints 5
print(subtract(0, 5))       # prints -5

# Arguments with names"
print(subtract(b=5))        # prints -5
print(subtract(b=5, a=10))  # prints 5
print(subtract(a=10, b=5))  # prints 5
0
5
-5
-5
5
5

Scopes and Namespaces

Scope rules:

  • If a variable is assigned inside a def, it is local to that function.
  • If a variable is assigned in an enclosing def, it is nonlocal to nested functions.
  • If a variable is assigned outside all defs, it is global to the entire file.

This is an example demonstrating how to reference the different scopes and namespaces, and how global and nonlocal affect variable binding:

In [ ]:
def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)
After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam
In [ ]:
def outer():
    x = "local"

    def inner():
        nonlocal x
        x = "nonlocal"
        print("inner:", x)

    inner()
    print("outer:", x)


outer()
inner: nonlocal
outer: nonlocal

Note how the local assignment (which is default) did not change scope_test’s binding of spam. The nonlocal assignment changed scope_test’s binding of spam, and the global assignment changed the module-level binding.

You can also see that there was no previous binding for spam before the global assignment.

x = mysum([1,2,3,4,5]) if not L: return L[0] + mysum(L[1:]) if not L: return L[0] + mysum(L[1:])