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[ ]:

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)
```

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[ ]:

**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())

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[ ]:

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

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)
```

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)
```

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))
```

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))
```

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)
```

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'
```

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

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)
```

In [ ]:

```
def outer():
x = "local"
def inner():
nonlocal x
x = "nonlocal"
print("inner:", x)
inner()
print("outer:", x)
outer()
```

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:])