CLASS-XII

COMPUTER SCIENCE

FUNCTIONS AND RECURSION

NOTES

MUTABLE AND IMMUTABLE PROPERTIES OF DATA OBJECT :

In Python, almost everything is an object. Numbers, strings, functions, classes, modules and even Python compiled code,all are objects.

Python treats all variables as references to the object. This means all variables store the memory address of the actual object. This concept is much like “Pointer” in and C++ . This means the address of the actual object is stored in the Python named variable, not the value itself.

Furthermore, immutable and mutable objects or variables are handled differently while working with function arguments. We will discuss them in below section. In the following diagram, variables a, b and name point to their memory locations where the actual value of the object is stored.

Immutable Objects or Variables in Python

In Python, immutable objects are those whose value cannot be changed in place after assignment or initialization. They allocate new memory whenever their value is changed. Examples of immutable data types or objects or variables in Python are numbers (integers, floats), strings and tuples.

In above diagram, the value of variable b is changed from 11 to 8 after its assignment or initialization. b is an integer and it is an immutable variable. Hence, new memory is allocated for it and old is discarded. In other words, you can never overwrite the value of immutable objects. Every time you change the value of an , a new object is created in result. Old object is discarded and memory is cleaned up by garbage collector automatically for later use. So in first look, this seems inefficient but in practical it is not. This technique is also used in other very popular platforms like and .NET.

Immutable Object or Variable Example with Explanation in Python

x = 10 print("Value of x:", x) print("ID of x before modification

1 x = 10 2 print("Value of x:", x) 3 print("ID of x before modification:", id(x)) 4 5 # add 20 to the value of x 6 x += 20 7 8 print("Value of x:", x) 9 print("ID of x after modification:", id(x))

Result: Value of x: 10 ID of x before modification: 1528383264

Value of x: 20 ID of x after modification: 1528383584

Explanation:

• As you can see memory address of x is changed after modification of value of x variable. This is clear that new memory location is allocated after modification of value of variable x

Mutable Objects or Variables in Python

In Python, list, dictionary and set are examples of mutable data types. Their values can be changed in place after their assignment or creation. When the value of a mutable variable is changed its memory is not reallocated.

In the above diagram, individual string objects in the my_list are immutable but my_list itself is mutable. If we add, remove or modify individual string elements in my_list, the my_list variable points to same object.

Mutable Object or Variable Example with Explanation in Python

my_list = ["apple", "pear"]

print("my_list:", my_list) print("ID of x before modification

1 my_list = ["apple", "pear"] 2 3 print("my_list:", my_list) 4 print("ID of x before modification:", id(my_list)) 5 6 # append "banana" to my_list 7 my_list.append("banana") 8 9 print("my_list:", my_list) 10 print("ID of x after modification:", id(my_list))

Result: my_list: ["apple", "pear"] ID of x before modification: 19925552 my_list: ["apple", "pear", "banana"] ID of x after modification: 19925552

Explanation:

• As you can see, memory address of variable is not changed after modifying the object my_list • This has been discussed in detail with examples in this article below.

Major Concepts of Function Argument Passing in Python

Arguments are always passed to functions by reference in Python. The caller and the function code blocks share the same object or variable. When we change the value of a function argument inside the function code block scope, the value of that variable also changes inside the caller code block scope regardless of the name of the argument or variable. This concept behaves differently for both mutable and immutable arguments in Python.

In Python, integer, float, string and tuple are immutable objects. list, dict and set fall in the mutable object category. This means the value of integer, float, string or tuple is not changed in the calling block if their value is changed inside the function or method block but the value of list, dict or set object is changed. Consider the mutable and immutable as states of the function arguments in Python. Let’s discuss it in detail with examples in the following section.

Python Immutable Function Arguments

Python immutable objects, such as numbers, tuple and strings, are also passed by reference like mutable objects, such as list, set and dict. Due to state of immutable (unchangeable) objects if an integer or string value is changed inside the function block then it much behaves like an . A local new duplicate copy of the caller object inside the function block scope is created and manipulated. The caller object will remain unchanged. Therefore, caller block will not notice any changes made inside the function block scope to the immutable object. Let’s take a look at the following example.

Python Immutable Function Argument – Example and Explanation

def foo1(a): # function block a += 1 print('id of a:', id(a)) # id of y

1 def foo1(a): 2 # function block 3 a += 1 4 print('id of a:', id(a)) # id of y and a are same 5 return a 6 7 # main or caller block 8 x = 10 9 y = foo1(x) 10 11 # value of x is unchanged 12 print('x:', x) 13 14 # value of y is the return value of the function foo1 15 # after adding 1 to argument 'a' which is actual variable 'x' 16 print('y:', y) 17 18 print('id of x:', id(x)) # id of x 19 print('id of y:', id(y)) # id of y, different from x

Result: id of a: 1456621360 x: 10 y: 11 id of x: 1456621344 id of y: 1456621360

Explanation:

Parameter Passing

"call by value" and "call by name"

The most common when passing arguments to a function has been call by value and call by reference:

• Call by Value The most common strategy is the call-by-value evaluation, sometimes also called pass-by-value. This strategy is used in C and C++, for example. In call-by-value, the argument expression is evaluated, and the result of this evaluation is bound to the corresponding variable in the function. So, if the expression is a variable, a local copy of its value will be used, i.e. the variable in the caller's scope will be unchanged when the function returns. • Call by Reference In call-by-reference evaluation, which is also known as pass-by-reference, a function gets an implicit reference to the argument, rather than a copy of its value. As a consequence, the function can modify the argument, i.e. the value of the variable in the caller's scope can be changed. The advantage of call-by-reference consists in the advantage of greater time- and space- efficiency, because arguments do not need to be copied. On the other hand this harbors the disadvantage that variables can be "accidentally" changed in a function call. So special care has to be taken to "protect" the values, which shouldn't be changed. Many programming language support call-by-reference, like C or C++, but uses it as default.

If you pass immutable arguments like integers, strings or tuples to a function, the passing acts like call-by-value. The object reference is passed to the function parameters. They can't be changed within the function, because they can't be changed at all, i.e. they are immutable. It's different, if we pass mutable arguments. They are also passed by object reference, but they can be changed in place in the function. If we pass a list to a function, we have to consider two cases: Elements of a list can be changed in place, i.e. the list will be changed even in the caller's scope. If a new list is assigned to the name, the old list will not be affected, i.e. the list in the caller's scope will remain untouched.

If you pass immutable arguments like integers, strings or tuples to a function, the passing acts like call-by-value. The object reference is First, let's have a look at the integer variables. The parameter inside of the function remains a reference to the arguments variable, as long as the parameter is not changed. As soon as a new value will be assigned to it, Python creates a separate local variable. The caller's variable will not be changed this way: def ref_demo(x): print "x=",x," id=",id(x) x=42 print "x=",x," id=",id(x)

In the example above, we used the id() function, which takes an object as a parameter. id(obj) returns the "identity" of the object "obj". This identity, the return value of the function, is an integer which is unique and constant for this object during its lifetime. Two different objects with non-overlapping lifetimes may have the same id() value.

If you call the function ref_demo() - like we do in the green block further down - we can check with the id() function what happens to x. We can see that in the main scope, x has the identity 41902552. In the first print statement of the ref_demo() function, the x from the main scope is used, because we can see that we get the same identity. After we have assigned the value 42 to x, x gets a new identity 41903752, i.e. a separate memory location from the global x. So, when we are back in the main scope x has still the original value 9.

This means that Python initially behaves like call-by-reference, but as soon as we are changing the value of such a variable, Python "switches" to call-by-value. >>> x = 9 >>> id(x) 41902552 >>> ref_demo(x) x= 9 id= 41902552 x= 42 id= 41903752 >>> id(x) 41902552 >>>

PASSING STRING, LIST,TUPLE AND DICTIONARY AS PARAMETER TO FUNCTION

Python does pass a string by reference. Notice that two strings with the same content are considered identical: a = 'hello' b = 'hello' a is b # True

Since when b is assigned by a value, and the value already exists in memory, it uses the same reference of the string. Notice another fact, that if the string was dynamically created, meaning being created with string operations (i.e concatenation), the new variable will reference a new instance of the same string: c = 'hello' = 'he' d += 'llo' c is d # False

That being said, creating a new string will allocate a new string in memory and returning a reference for the new string, but using a currently created string will reuse the same string instance. Therefore, passing a string as a function parameter will pass it by reference, or in other words, will pass the address in memory of the string.

And now to the point you were looking for- if you change the string inside the function, the string outside of the function will remain the same, and that stems from string immutability. Changing a string means allocating a new string in memory. a = 'a' b = a # b will hold a reference to string a a += 'a' a is b # False

You cannot really change a string. When you pass the string as an argument, you pass a reference. When you change it's value, you change the variable to point to another place in memory. But when you change a variable's reference, other variables that points to the same address will naturally keep the old value (reference) they held.

We have to define a function that will accept string argument and print it. Here, we will learn how to pass string value to the function?

Example 1:

Input: str = "Hello world"

Function call: printMsg(str)

Output: "Hello world"

# Python program to pass a string to the function

# function definition: it will accept # a string parameter and print it def printMsg(str): # printing the parameter print str

# Main code # function calls printMsg("Hello world!") printMsg("Hi! I am good.") Output

Hello world! Hi! I am good.

EXAMPLE 2:Write function that will accept a string and return total number of vowels

# function definition: it will accept # a string parameter and return number of vowels def countVowels(str): count = 0 for ch in str: if ch in "aeiouAEIOU": count +=1 return count

# Main code # function calls str = "Hello world!" print ("No. of vowels are “,countVowels(str)) str = "Hi, I am good." print ("No. of vowels are”,countVowels(str))

Output

No. of vowels are 3 No. of vowels are 5

In Python, you can expand list, tuple, and dictionary (dict), and pass each element to function arguments.

Add * to a list or tuple and ** to a dictionary when calling a function, then elements are passed to arguments. Note the number of asterisks *.

• Expand list and tuple with * o With default arguments o With variable-length arguments • Expand the dictionary (dict) with ** o With default arguments o With variable-length arguments

Expand list and tuple with *

When specifying a list or tuple with * as an argument, it is expanded and each element is passed to each argument. def func(arg1, arg2, arg3): print(arg1) print(arg2) print(arg3) l = ['one', 'two', 'three'] func(*l) # one # two # three func(*['one', 'two', 'three']) # one # two # three t = ('one', 'two', 'three') func(*t) # one # two # three func(*('one', 'two', 'three')) # one # two # three

The following description is given in the case of a list, but the same applies to tuples. If the number of elements does not match the number of arguments, Type Error will occur.

# func(*['one', 'two']) # TypeError: func() missing 1 required positional argument: 'arg3'

# func(*['one', 'two', 'three', 'four']) # TypeError: func() takes 3 positional arguments but 4 were given

With default arguments

If the function has default arguments, the default arguments will be used if the number of elements is insufficient. If there are many elements, Type Error will occur. def func_default(arg1=1, arg2=2, arg3=3): print(arg1) print(arg2) print(arg3) func_default(*['one', 'two']) # one # two # 3 func_default(*['one']) # one # 2 # 3

# func_default(*['one', 'two', 'three', 'four']) # TypeError: func_default() takes from 0 to 3 positional arguments but 4 were given s

With variable-length arguments

If the function has a variable-length argument (*args), all elements after the positional argument are passed to the variable-length argument. def func_args(arg1, *args): print(arg1) for arg in args: print(arg) func_args(*['one', 'two']) # one # two func_args(*['one', 'two', 'three']) # one # two # three func_args(*['one', 'two', 'three', 'four']) # one # two # three # four

Expand the dictionary (dict) with **

When specifying a dictionary (dict) with ** as an argument, key will be expanded as an argument name and value as the value of the argument. Each element will be passed as keyword arguments. def func(arg1, arg2, arg3): print(arg1) print(arg2) print(arg3) d = {'arg1': 'one', 'arg2': 'two', 'arg3': 'three'} func(**d) # one # two # three func(**{'arg1': 'one', 'arg2': 'two', 'arg3': 'three'}) # one # two # three

If there is no key that matches the argument name, or if there is a key that does not match the argument name, Type Error will occur.

# func(**{'arg1': 'one', 'arg2': 'two'}) # TypeError: func() missing 1 required positional argument: 'arg3'

# func(**{'arg1': 'one', 'arg2': 'two', 'arg3': 'three', 'arg4': 'four'}) # TypeError: func() got an unexpected keyword argument 'arg4'

With default arguments

If the function has default arguments, only the value of the argument name matching the dictionary key is updated.

If there is a key that does not match the argument name, Type Error will occur. def func_default(arg1=1, arg2=2, arg3=3): print(arg1) print(arg2) print(arg3) func_default(**{'arg1': 'one'}) # one # 2 # 3 func_default(**{'arg2': 'two', 'arg3': 'three'}) # 1 # two # three

# func_default(**{'arg1': 'one', 'arg4': 'four'}) # TypeError: func_default() got an unexpected keyword argument 'arg4'

With variable-length arguments

If the function has a variable-length argument (**kwargs), all elements with keys that do not match the argument name are passed to the variable-length argument. def func_kwargs(arg1, **kwargs): print('arg1', arg1) for k, v in kwargs.items(): print(k, v) func_kwargs(**{'arg1': 'one', 'arg2': 'two', 'arg3': 'three'}) # arg1 one # arg2 two # arg3 three func_kwargs(**{'arg1': 'one', 'arg2': 'two', 'arg3': 'three', 'arg4': 'four'}) # arg1 one # arg2 two # arg3 three # arg4 four func_kwargs(**{'arg1': 'one', 'arg3': 'three'}) # arg1 one # arg3 three

EXAMPLES OF PASSING LIST, TUPLE AND DICTIONARY AS A PARAMETER TO FUNCTION: LIST AS PARAMETER TO FUNCTION: def list (l): print(l) print(“number of element = ” , len(l) ) for i in range (0,len(l)): print(l[i]) l.append(“last”) return l L=[10,20, “deepak”,30, “ravi”, “gagan”] R1=list(L) print(R1)

TUPLE AS PARAMETER TO FUNCTION: def product(my_tuple): for I in my_tuple : print(i) my_tuple =(1,2,3,4,5) product(my_tuple)

DICTIONARY AS A PARAMETER TO FUNCTION: def fun(dict): for k, v in dict.iteritems( ): print k, 2*v d={‘a’:1,’b’:2,’c’:3} fun(d)

Recursive Functions

Definition

Definition of Recursion

Recursion is a way of programming or coding a problem, in which a function calls itself one or more times in its body. Usually, it is returning the return value of this function call. If a function definition fulfills the condition of recursion, we call this function a recursive function. Recursion is not only a fundamental feature of natural language, but of the human cognitive capacity. Our way of thinking is based on recursive thinking processes. Even with a very simple grammar, like "An English sentence contains a subject and a predicate. A predicate contains a verb, an object and a complement". The adjective "recursive" originates from the Latin verb "recorder", which means "to run back". And this is what a recursive definition or a recursive function does: It is "running back" or returning to itself. Most people who have done some mathematics, computer science or read a book about programming will have encountered the factorial, which is defined in mathematical terms as n! = n * (n-1)!, if n > 1 and f(1) = 1

Termination condition: A recursive function has to terminate to be used in a program. A recursive function terminates, if with every recursive call the solution of the problem is downsized and moves towards a base case. A base case is a case, where the problem can be solved without further recursion. A recursion can lead to an infinite loop, if the base case is not met in the calls.

Example: 4! = 4 * 3! 3! = 3 * 2! 2! = 2 * 1 Replacing the calculated values gives us the following expression 4! = 4 * 3 * 2 * 1

Generally we can say: Recursion in computer science is a method where the solution to a problem is based on solving smaller instances of the same problem. A function is said to be recursive if it calls itself. Recursion can be : 1. Direct recursion –if a function calls itself directly from its function body. 2. Indirect recursion—if a function calls another function, which calls its caller function.

RECURSIVE FUNCTIONS IN PYTHON :

TO CALCULATE FACTORIAL :

Now we come to implement the factorial in Python. It's as easy and elegant as the mathematical definition. def factorial(n): if n == 1: return 1 else: return n * factorial(n-1) We can track how the function works by adding two print() functions to the previous function definition: def factorial(n): print("factorial has been called with n = " + str(n)) if n == 1: return 1 else: res = n * factorial(n-1) print("intermediate result for ", n, " * factorial(" ,n-1, "): ",res) return res print(factorial(5)) This Python script outputs the following results: factorial has been called with n = 5 factorial has been called with n = 4 factorial has been called with n = 3 factorial has been called with n = 2 factorial has been called with n = 1 intermediate result for 2 * factorial( 1 ): 2 intermediate result for 3 * factorial( 2 ): 6 intermediate result for 4 * factorial( 3 ): 24 intermediate result for 5 * factorial( 4 ): 120 120 Let's have a look at an iterative version of the factorial function. def iterative_factorial(n): result = 1 for i in range(2,n+1): result *= i return result

THE FIBONACCI SERIES:

The Fibonacci numbers are the numbers of the following sequence of integer values:

0,1,1,2,3,5,8,13,21,34,55,89, ...

The Fibonacci numbers are defined by: Fn = Fn-1 + Fn-2 with F0 = 0 and F1 = 1

The Fibonacci numbers are easy to write as a Python function. It's more or less a one to one mapping from the mathematical definition: def fib(n): if n == 0: return 0 elif n == 1: return 1 else: return fib(n-1) + fib(n-2) An iterative solution for the problem is also easy to write, though the recursive solution looks more like the mathematical definition: def fibi(n): a, b = 0, 1 for i in range(n): a, b = b, a + b return a If you check the functions fib() and fibi(), you will find out that the iterative version fibi() is a lot faster than the recursive version fib(). To get an idea of how much this "a lot faster" can be, we have written a script where we you the time it module to measure the calls: We can see that it is even faster than the iterative version. Of course, the larger the arguments the greater the benefit of our memorization:

Recursive Binary Search : In Binary Search the element are in sorted order either ascending or descending . The process of finding the element is same in all search segments, only the lower limit and higher limit of a segment changes if the element is not found at the middle position of the search-segment. The program is as follows: #binary recursive search def binsearch(ar, key,low,high) : if low>high : return -999 mid=int((low+high) / 2) if key==ar[mid] : return mid elif key < ar[mid] : high=mid-1 return binsearch(ar,key,low,high) else: low= mid + 1 return binsearch(ar,key,low,high) #_main_ ary=[12,15,21,25,28] item=int(input(“Enter search item”)) res=binsearch(ary,item,0,len(ary) -1 ) if res>0 : print(item, “found at index” , res) else: print(“sorry!!”,item, “ not found in array”)

Exercises

1. Think of a recursive version of the function f(n) = 3 * n, i.e. the multiples of 3 2. Write a recursive Python function that returns the sum of the first n integers. (Hint: The function will be similar to the factorial function!)

Solutions to our Exercises

1. Solution to our first exercise on recursion: Mathematically, we can write it like this: f(1) = 3, f(n+1) = f(n) + 3

A Python function can be written like this:

def mult3(n): if n == 1: return 3 else: return mult3(n-1) + 3

for i in range(1,10): print(mult3(i))

2. Solution to our second exercise:

def sum_n(n): if n== 0: return 0 else: return n + sum_n(n-1)

Summary

• All the data in a Python code is represented by objects or by relations between objects. • Every object has an identity, a type, and a value. • Python treats all variables as references to the object. • Arguments are always passed to functions by reference in Python. • The most common evaluation strategy when passing arguments to a function has been call by value and call by reference: • Python initially behaves like call-by-reference, but as soon as we are changing the value of such a variable, Python "switches" to call-by-value. • An object’s identity never changes once it has been created. You may think of it as the object’s address in memory. • An object’s type defines the possible values and operations. • Objects whose value can change are said to be mutable. Objects whose value is unchangeable once they are created are called immutable. • When we talk about the mutability of a container only the identities of the contained objects are implied • A function is said to be recursive if it calls itself. • An infinite recursion is when a recursive function calls itself endlessly. • Recursive function is relatively slower than their iterative counterparts.