DS Unit 1 (1) (1)
DS Unit 1 (1) (1)
Abstract Data Types (ADTs) – ADTs and classes – introduction to OOP – classes
in Python – inheritance – namespaces – shallow and deep copying Introduction to
analysis of algorithms – asymptotic notations – recursion – analyzing recursive
algorithms
class CreditCard:
def init (self, customer, bank, acnt, limit):
self. customer = customer
self. bank = bank
self. account = acnt
self. limit = limit
self. balance = 0
def get customer(self):
return self. customer
def get bank(self):
return self.
bank
def get account(self):
return self. account
def get limit(self):
return self. limit
def get balance(self):
return self. Balance
def charge(self,
price):
if price + self. balance > self. limit:
return False
else:
self. balance += price
return True
def make payment(self,
amount): self. balance −=
amount
The Constructor
A user can create an instance of the CreditCard class using a syntax as:
cc = CreditCard( John Doe, 1st Bank , 5391 0375 9387 5309 , 1000)
Internally, this results in a call to the specially named init method that serves
as the constructor of the class.
Introduction to OOP
Classes in Python
• Class- Classes are defined by the user. The class provides the basic structure
for an object. It consists of data members and method members that are used
by the instances(object) of the class.
• Class variable: A variable that is defined in the class and can be used by all the
instance of that class.
• Method: They are functions that are defined in the definition of class and are
used by various instances of the class.
• Function Overloading: A function defined more than one time with different
behavior. (different arguments)
Classes-
• Python is OOP language. Almost everything in python is an object with its
properties and methods.
Creating Classes:
A class is a block of statement that combine data and operations, which are
performed on the data, into a group as a single unit and acts as a blueprint for the
creation of objects.
Syntax:
class ClassName:
„ Optional class documentation string
#list of python class variables
# Python class constructor
#Python class method definitions
• The self parameter is a reference to the class itself and is used to access
variables that belongs to the class.
Example: Creating class in .py file
class student:
def display(self): # defining method in class
print("Hello Python")
• So we can use self to refer to all the instance variable and instance methods.
Example:
class student:
def display(self): # defining method in class
print("Hello Python")
s1=student() #creating object of class
s1.display() #calling method of class using
object Output:
Hello Python
• Instance variable is defined in a method and its scope is only within the object
that defines it.
• Every object of the class has its own copy of that variable. Any changes made
to the variable don‟t reflect in other objects of that class.
• Class variable is defined in the class and can be used by all the instances
of that class.
• Instance variables are unique for each instance, while class variables
are shared by all instances.
Inheritance:
Multilevel Inheritance:
In multilevel inheritance, features of the base class and the derived class are further
inherited into the new derived class. This is similar to a relationship representing a
child and grandfather.
Syntax:
Class A:
# Properties of class A
Class B(A):
# Class B inheriting property of class A
# more properties of class B
Class C(B):
# Class C inheriting property of class B
# thus, Class C also inherits properties of class A
# more properties of class C
Example 1: Python program to demonstrate multilevel inheritance
#Mutilevel Inheritance
class c1:
def display1(self):
print("class c1")
class c2(c1):
def display2(self):
print("class c2")
class c3(c2):
def display3(self):
print("class c3")
s1=c3()
s1.display3()
s1.display2()
s1.display1()
Output:
class c3
class c2
class c1
Example 2: Python program to demonstrate multilevel inheritance
# Base class
class Grandfather:
grandfathername =""
def grandfather(self):
print(self.grandfathername)
# Intermediate class
class Father(Grandfather):
fathername = ""
def father(self):
print(self.fathername)
# Derived class
class Son(Father):
def parent(self):
print("GrandFather :", self.grandfathername)
print("Father :", self.fathername)
# Driver's code
s1 = Son()
s1.grandfathername = "Srinivas"
s1.fathername = "Ankush"
s1.parent()
Output:
GrandFather : Srinivas
Father : Ankush
Multiple Inheritance:
When a class can be derived from more than one base classes this type of
inheritance is called multiple inheritance. In multiple inheritance, all the features of
the base classes are inherited into the derived class.
Syntax:
Class A:
# variable of class A
# functions of class
A Class B:
# variable of class B
# functions of class
B Class C(A,B):
# Class C inheriting property of both class A and B
# more properties of class C
Example: Python program to demonstrate multiple inheritance
# Base class1
class Father:
def display1(self):
print("Father")
# Base class2
class Mother:
def display2(self):
print("Mother")
# Derived class
class Son(Father,Mother):
def display3(self):
print("Son")
s1 = Son()
s1.display3()
s1.display2()
s1.display1()
Output:
Son
Mother
Father
Hierarchical Inheritance:
When more than one derived classes are created from a single base this type of
inheritence is called hierarchical inheritance. In this program, we have a parent
(base) class and two child (derived) classes.
Namespaces
A namespace is an abstraction that manages all of the identifiers that are
defined in a particular scope, mapping each name to its associated value.
A name in Python is a way to access a variable like in any other languages. Python
is more flexible when it comes to the variable declaration. We can declare a
variable by assigning a name to it. We can use names to reference values.
Example:
num =
5 str =
'Z'
seq = [0, 1, 1, 2, 3, 5]
#We can even assign a name to a function.
def function():
print('It is a function.')
foo = function
foo()
Types of Namespaces
1. Local Namespace
Creation of local functions creates the local namespace. This namespace covers the
local names inside a function. Python creates this namespace for every function
called in a program. It remains active until the function returns.
2. Global Namespace
When a user creates a module, a global namespace gets created. This namespace
covers the names from various imported modules used in a project. Python creates
this namespace for every module included in your program. It‟ll last until the
program ends.
3. Built-in Namespace
This namespace covers the built-in functions and built-in exception names. Python
creates it as the interpreter starts and keeps it until you exit. The built-in namespace
encompasses global namespace and
global namespace encompasses local namespace. Some functions like print(), id()
are examples of built in namespaces.
Lifetime of a namespace:
A lifetime of a namespace depends upon the scope of objects, if the scope of an
object ends, the lifetime of that namespace comes to an end. It is not possible to
access inner namespace‟s objects from an outer namespace.
Scopes
A scope refers to a region of a program where a namespace can be directly
accessed, i.e. without using a namespace prefix. The scope of a name is the area of
a program where this name can be unambiguously used, for example, inside of a
function. A name's namespace is identical to its scope. Scopes are defined
statically, but they are used dynamically.
During program execution there are the following nested scopes available:
the innermost scope is searched first and it contains the local names
the scopes of any enclosing functions, which are searched starting with the
nearest enclosing scope
the next-to-last scope contains the current module's global names
the outermost scope, which is searched last, is the namespace containing the
built-in names.
A shallow copy is one which makes a new object stores the reference of another
object. While, in deep copy, a new object stores the copy of all references of
another object making it another list separate from the original one.
Thus, when you make a change to the deep copy of a list, the old list doesn‟t get
affected and vice-versa. But shallow copying causes changes in both the new as
well as in the old list.
import copy
copy.copy(object_name)
import copy
copy.deepcopy(object_name)
Example:
import copy
a = [ [1, 2, 3], [4, 5, 6] ]
b = copy.copy(a)
c = [ [7, 8, 9], [10, 11, 12] ]
d = copy.deepcopy(c)
print(a)
print(b) a[1]
[2] = 23
b[0][0] = 98
print(a)
print(b)
print("\n")
print(c)
print(d) c[1]
[2] = 23
d[0][0] = 98
print(c)
print(d)
Output:
Shallow Copy [1,2,3]
[4,5,6]
[1,2,3][4,5,6]
[98,2,3][4,5,23]
[98,2,3][4,5,23]
Deep Copy
[7, 8, 9], [10, 11, 12]
[7, 8, 9], [10, 11, 12]
[7, 8, 9], [10, 11, 23]
[98, 8, 9], [10, 11, 12]
Introduction to analysis of algorithms
Efficiency of Algorithms:
The performances of algorithms can be measured on the scales of time and space.
The performance of a program is the amount of computer memory and time needed
to run a program. We use two approaches to determine the performance of a
program. One is analytical and the other is experimental.
In performance analysis we use analytical methods, while in performance
measurement we conduct experiments.
Time Complexity: The time complexity of an algorithm or a program is a function
of the running time of the algorithm or a program. In other words, it is the amount
of computer time it needs to run to completion.
Space Complexity: The space complexity of an algorithm or program is a function
of the space needed by the algorithm or program to run to completion. The time
complexity of an algorithm can be computed either by an empirical or theoretical
approach.
The empirical or posteriori testing approach calls for implementing the complete
algorithms and executing them on a computer for various instances of the problem.
The time taken by the execution of the programs for various instances of the
problem are noted and compared. The algorithm whose implementation yields the
least time is considered as the best among the candidate algorithmic solutions.
Analyzing Algorithms
Suppose M is an algorithm, and suppose n is the size of the input data. Clearly the
complexity f(n) of M increases as n increases. It is usually the rate of increase of
f(n) with some standard functions.
The most common computing times are O(1), O(log2 n), O(n), O(n log2 n),
O(n2), O(n3), O(2n)
Example:
The total frequency counts of the program segments A, B and C given by 1, (3n+1)
and (3n2+3n+1) respectively are expressed as O(1), O(n) and O(n2). These are
referred to as the time complexities of the program segments since they are
indicative of the running times of the program segments. In a similar manner space
complexities of a program can also be expressed in terms of mathematical
notations, which is nothing but the amount of memory they require for their
execution.
Asymptotic Notations:
It is often used to describe how the size of the input data affects an algorithm‟s
usage of computational resources. Running time of an algorithm is described as a
function of input size n for large n.
Big oh(O): Definition: f(n) = O(g(n)) (read as f of n is big oh of g of n) if there
exist a positive integer n0 and a positive number c such that |f(n)| ≤ c|g(n)| for all n
≥ n0 . Here g(n) is the upper bound of the function f(n).
Linear Recursion:
It is the most common type of Recursion in which function calls itself repeatedly
until base condition [termination case] is reached. Once the base case is reached
the results are return to the caller function.
If a recursive function is called only once then it is called a linear recursion.
Binary Recursion:
Functions with two recursive calls are referred to as binary recursive
functions.
Example1: The Fibonacci function fib provides a classic example of binary
recursion. The Fibonacci numbers can be defined by the rule:
fib(n) = 0
if n is 0,
return 1
if n is 1,
= fib(n-1) + fib(n-2) otherwise
For example, the first seven Fibonacci numbers are
Fib(0) = 0
Fib(1) = 1
Fib(2) = Fib(1) + Fib(0) = 1
Fib(3) = Fib(2) + Fib(1) = 2
Fib(4) = Fib(3) + Fib(2) = 3
Fib(5) = Fib(4) + Fib(3) = 5
Fib(6) = Fib(5) + Fib(4) = 8
# Program to display the Fibonacci sequence up to n-th term where n is provided
by the user
# change this value for a different result
nterms = 10
# uncomment to take input from the user
#nterms = int(input("How many terms?
")) # first two terms
n1 = 0
n2 = 1
count = 0
# check if the number of terms is valid
if nterms <= 0:
print("Please enter a positive integer")
elif nterms == 1:
print("Fibonacci sequence upto",nterms,":")
print(n1)
else:
print("Fibonacci sequence upto",nterms,":")
while count < nterms:
print(n1,end=' , ')
nth = n1 + n2
# update values
n1 = n2
n2 = nth
count += 1
Tail Recursion:
Tail recursion is a form of linear recursion. In tail recursion, the recursive call is
the last thing the function does. Often, the value of the recursive call is returned.
As such, tail recursive functions can often be easily implemented in an iterative
manner; by taking out the recursive call and replacing it with a loop, the same
effect can generally be achieved. In fact, a good compiler can recognize tail
recursion and convert it to iteration in order to optimize the performance of the
code.
A good example of a tail recursive function is a function to compute the GCD, or
Greatest Common Denominator, of two numbers:
def factorial(n):
if n == 0: return 1
else: return factorial(n-1) * n
def tail_factorial(n, accumulator=1):
if n == 0: return 1
else: return tail_factorial(n-1, accumulator * n)
Recursive algorithms for Factorial, GCD, Fibonacci Series and Towers of
Hanoi:
Factorial(n)
Input: integer n ≥
0 Output: n!
1. If n = 0 then return (1)
2. else return prod(n, factorial(n − 1))
GCD(m, n)
Input: integers m > 0, n ≥ 0
Output: gcd (m, n)
12
1. If n = 0 then return (m)
2. else return gcd(n,m mod
n) Time-Complexity: O(ln n)
Fibonacci(n)
Input: integer n ≥ 0
Output: Fibonacci Series: 1 1 2 3 5 8 13………………………………..
1. if n=1 or n=2
2. then Fibonacci(n)=1
3. else Fibonacci(n) = Fibonacci(n-1) + Fibonacci(n-2)
Towers of Hanoi
Input: The aim of the tower of Hanoi problem is to move the initial n different
sized disks from needle
A to needle C using a temporary needle B. The rule is that no larger disk is to be
placed above the
smaller disk in any of the needle while moving or at any time, and only the top of
the disk is to be
moved at a time from any needle to any needle.
Output:
1. If n=1, move the single disk from A to C and return,
2. If n>1, move the top n-1 disks from A to B using C as temporary.
3. Move the remaining disk from A to C.
4. Move the n-1 disk disks from B to C, using A as
temporary. def TowerOfHanoi(n , from_rod, to_rod,
aux_rod):
if n == 1:
print "Move disk 1 from rod",from_rod,"to rod",to_rod
return
TowerOfHanoi(n-1, from_rod, aux_rod, to_rod)
print "Move disk",n,"from rod",from_rod,"to
rod",to_rod TowerOfHanoi(n-1, aux_rod, to_rod,
from_rod)
n=4
TowerOfHanoi(n, 'A', 'C', 'B')