OOPS Concepts in Python
This style of programming
where we create a template and create copies from that template is called
object oriented programming. This style allows us to code for scenarios closely
linked with real life.
The template we create is
called a Class and the copies we create out of it is called an object.
Objects are real world entities. Anything you
can describe in this world is an object. Classes on the other hand are not
real. They are just a concept. Class is a short form of Classification. A class
is a classification of certain objects and it is just a description of the
properties and behavior all objects of that classification should possess.
Built in class:
Like any programming
language, python also has built-in classes. Some of the built-in classes which
you have already used are:
To
create an object, we need a class. The syntax for creating an object is
"()", where is the name of the
class.
Eg:
Mobile()
Mobile()
Mobile()
Object Literals:
Object literal is a special syntax through with objects are
created and initialized.
For example,
- list1 = [1,2,3]
- dict1 =
{"name": "Gopal", "age": 32}
- tup1 = (1,2,3)
All objects have an internal unique id ( just
like aadhar or green card number ). We can check this using the inbuilt id().
The below code will display the unique number associated with the object.
class Mobile:
pass
mob1=Mobile()
mob2=Mobile()
print("Id for mobile 1
is :", id(mob1))
print("Id for mobile 2
is :", id(mob2))
To create
attributes and values for those attributes? This can be done by using the .
(dot) operator.
reference_variable.attribute_name=value.
class Mobile:
pass
mob1=Mobile()
mob2=Mobile()
mob1.price=20000
mob1.brand="Apple"
mob2.price=3000
mob2.brand="Samsung"
Variable
|
Attribute
|
Variable_name = value creates the
variable and assigns the value if the variable does not exist already.
For example:x = 5
|
reference_variable.attribute_name
= value creates the attribute and assigns the value if the attribute does not
exist already.
For example: m1.color =
"Green"
|
Variable_name
= value updates the the value if the variable exists already.
For example: x = 5,x = 6
|
reference_variable.attribute_name
= value updates the attribute if the attribute exists already.
For example: m1.color =
"Green", m1.color = "Red"
|
Accessing
a non-existent variable throws an error
|
Accessing
a non-existent attribute throws an error
|
A
variable can be assigned to another variable.
For example:
y
= x
|
The
value of an attribute can be assigned to another variable.
For example:
c1
= m1.color
|
Attributes can be added to a class through a
special function called __init__().
We can create behavior in a class by adding
functions in a class. However, such functions should have a special parameter
called self as the first parameter.
class
Mobile:
def
__init__(self
, brand
, price
):
print("Inside constructor")
self
.brand
= brand
self
.price
= price
def
purchase(self
):
print("Purchasing a mobile")
print("This mobile has brand", self
.brand
,
"and price", self
.price
)
print("Mobile-1")
mob1
=Mobile
("Apple",
20000)
mob1
.purchase
()
print("Mobile-2")
mob2
=Mobile
("Samsung",3000)
mob2
.purchase
()
In python, everything is an object. Thus
everything would have either attributes or behavior or both. That means even
numbers, strings, list, set, dictionary, etc are all treated as objects in
python.
Example
|
Description
|
(12.5).is_integer()
|
Here we are invoking a method
is_integer() on a numerical value. That means numerical values are all
objects
|
"hello".upper()
|
Here we are invoking upper() method
on a string object.
|
[1,2,3].reverse()
|
Here we are invoking the reverse
method on a list object
|
Function
|
Method
|
Is a block of code with a name
|
Is part of an object and represents
the behavior of the object
|
It can be invoked using the name of
the function and passing parameters
Example: len([1,2,3])
|
Can be invoked only on an object,
using dot operator. Without an object we cannot invoke a method
Example: [1,2,3].reverse()
|
Parameters are optional in a function
|
A method must have at least one
parameter : self
|
Abstraction
When we invoke the
purchase() on a mobile object, we don’t have to know the details of the method
to invoke it. We don’t have to know how the reverse() method is working in
order to use it in our list.
This ability to use
something without having to know the details of how it is working is called
as abstraction.
SUMMARY:
- OOP is a style
of programming which allows us to club data and behavior together.
- This is more
suited for coding real life scenarios.
- Objects are real
world entities
- Class is just a
classification. It is just a concept.
- Class is a
description of attributes and behavior that objects of that classification
should possess.
- Attributes are
created in a special function called __init__ and behaviors are created
using functions called methods.
- Objects can be
created using ClassName() or using object literals for some of the built
in classes
- Attributes are
created using reference_variable.attribute_name = value syntax.
- Behavior is
created by defining a function inside the class having a special parameter
called self.
In build functions:
we
can use the inbuilt special __str__ method. This method MUST return a string
and this string will be used when the object is printed. This is useful in
debugging as we can print the values of the attributes
class
Shoe:
def
__init__(self
, price
, material
):
self
.price
= price
self
.material
= material
def
__str__(self
):
return
"Shoe with price: "
+ str
(self
.price
)
+
" and material: "
+ self
.material
s1
=Shoe
(1000,
"Canvas")
print(s1
)
Output: Shoe with price: 1000 and material: Canvas
Class diagram
A
class diagram four parts: the name of the class, the list of attributes, the list
of methods and access specifiers.
In a class diagram, a –
sign indicates private access and + indicates public access
Note: We can create
private methods by adding a double underscore in front of it, just like private
variables. Also, if a method has both leading and trailing double underscores (
like __init__, __str__, etc) it indicates that it is a special built-in method.
As per coding convention, we are not supposed to create our own methods with
both leading and trailing underscores.
Coding Standards
All variable names and method names are in
snake_case and all class names should be in PascalCase ( It is similar to
camelCase but the first character is also Capitalized ). For example:
Classes:
Mobile
RegularCustomer
Methods:
change_password()
display_details()
Variables:
price = 1000
brand = "Samsung"
Summary:
- Reference
variables hold the objects
- An object can
have multiple reference variables
- Assigning a new
reference variable to an existing object does not create a new object
- We can create
objects without reference variable as well
- Class diagrams
represent the class with its attributes and behavior
class
Mobile:
def
__init__(self
, brand
, price
):
print("Inside the Mobile constructor")
self
.brand
= brand
self
.price
= price
self
.total_price
= None
def
purchase(self
):
if self
.brand
==
"Apple":
discount
=
10
else:
discount
=
5
self
.total_price
= self
.price
- self
.price
* discount
/
100
print("Total price of", self
.brand
,
"mobile is", self
.total_price
)
def
return_product(self
):
print("Refund Amount for", self
.brand
,
"mobile is", self
.total_price
)
class
Shoe:
def
__init__(self
, material
, price
):
print("Inside the Shoe constructor")
self
.material
= material
self
.price
= price
self
.total_price
= None
def
purchase(self
):
if self
.material
==
"leather":
tax
=
5
else:
tax
=
2
self
.total_price
= self
.price
+ self
.price
* tax
/
100
print("Total price of", self
.material
,
"shoe is", self
.total_price
)
def
return_product(self
):
print("Refund Amount for", self
.material
,
"shoe is", self
.total_price
)
mob1
=Mobile
("Apple",
20000)
mob2
=Mobile
("Samsung",
10000)
shoe1
=Shoe
("leather",3000)
shoe2
=Shoe
("canvas",200)
mob1
.purchase
()
mob2
.purchase
()
shoe1
.purchase
()
shoe2
.purchase
()
mob2
.return_product
()
shoe1
.return_product
()
We can put a lock on that data by adding a
double underscore in front of it,
class
Customer:
def
__init__(self
, cust_id
, name
, age
, wallet_balance
):
self
.cust_id
= cust_id
self
.name
= name
self
.age
= age
self
.__wallet_balance
= wallet_balance
def
update_balance(self
, amount
):
if amount
<
1000
and amount
>
0:
self
.__wallet_balance
+= amount
def
show_balance(self
):
print
("The balance is ",self
.__wallet_balance
)
c1
=Customer
(100,
"Gopal",
24,
1000)
print(c1
.__wallet_balance
)
Adding a double underscore makes the attribute
a private attribute. Private attributes are those which are accessible only
inside the class. This method of restricting access to our data is called
encapsulation
When we
put a double underscore in front of the attribute name, python will internally change
its name to _Classname__attribute
This is
why we get an error when we try to access a private attribute.
All
setter methods must accept the value to be updated as a parameter and all
getter methods must not have any parameter and they must return the value.
Setter
methods are called as mutator methods ( as they change or mutate the value )
and the getter methods are called accessor methods ( as they access the values
)
class
Customer:
def
__init__(self
, id
, name
, age
, wallet_balance
):
self
.id
= id
self
.name
= name
self
.age
= age
self
.__wallet_balance
= wallet_balance
def
set_wallet_balance(self
, amount
):
if amount
<
1000
and amount
>
0:
self
.__wallet_balance
= amount
def
get_wallet_balance(self
):
return self
.__wallet_balance
c1
=Customer
(100,
"Gopal",
24,
1000)
c1
.set_wallet_balance
(120)
print(c1
.get_wallet_balance
())
mob2.return_product()
can also be invoked as Mobile.return_product(mob2)
Thus
self now refers to mob2, as this is actually pass by reference. For simplicity
sake and for better readability we use mob2.return_product() instead of
Mobile.return_product(mob2)
class
Mobile:
def
__init__(self
,price
,brand
):
print
(id
(self
))
self
.price
= price
self
.brand
= brand
def
return_product(self
):
print
(id
(self
))
print
("Brand being returned is ",self
.brand
," and price is ",self
.price
)
mob1
= Mobile
(1000,
"Apple")
print
("Mobile 1 has id", id
(mob1
))
mob2
=Mobile
(2000,
"Samsung")
print
("Mobile 2 has id", id
(mob2
))
mob2
.return_product
()
Mobile
.return_product
(mob2
)
Method invocation
|
Method definition
|
Explanation
|
mob1.display()
|
def display(self): print(self.discount)
|
Here, 'self' is the first parameter. Hence it refers to mob1.
|
mob1.display()
|
def display(mob_obj): print(mob_obj.discount)
|
Here, ‘mob_obj' is the first parameter. Hence it refers to ‘mob1'.
|
mob1.purchase(2)
|
def purchase(self,qty): print("Total is ", self.price*qty)
|
Here, 'self' is the first parameter. Hence it refers to ‘mob1'.The second parameter is ‘qty' which stores 2 passed during invocation.
|
mob1.purchase(2)
|
def purchase(qty,self): print("Total is ", qty.price*self)
|
Here, ‘qty' is the first parameter. Hence it refers to ‘mob1'. The second parameter is 'self' which stores 2 passed during invocation.
|
mob1.display()
|
def display(): print(self.discount)
|
This is an error, since the first parameter of a method is always a reference to the object used. Hence it should have AT LEAST one parameter.
|
Collection of Objects:
We can also store a number of objects inside a list or a
dictionary. The below example, we have a list of mobile objects and we are
iterating over the list and printing the values
class
Mobile:
def
__init__(self
, brand
, price
):
self
.brand
= brand
self
.price
= price
mob1
=Mobile
("Apple",
1000)
mob2
=Mobile
("Samsung",
2000)
mob3
=Mobile
("Apple",
3000)
mob4
=Mobile
("Samsung",
4000)
mob5
=Mobile
("Apple",
5000)
list_of_mobiles
=[mob1
, mob2
, mob3
, mob4
, mob5
]
for mobile
in list_of_mobiles
:
print
(mobile
.brand
,mobile
.price
)
Dictionary
of Object
class
Mobile:
def
__init__(self
,brand
,price
):
self
.brand
= brand
self
.price
= price
mob1
=Mobile
("Apple",
1000)
mob2
=Mobile
("Samsung",
5000)
mob3
=Mobile
("Apple",
3000)
mob_dict
={
"m1":mob1
,
"m2":mob2
,
"m3":mob3
}
for key
,value
in mob_dict
.items
():
if value
.p
rice
>
3000:
print
(value
.brand
,value
.price
)
Summary:
- Everything is
treated as an object in python
- We can use
objects inside collections
- Encapsulation is
preventing access to a data outside the class
- Adding a __ in
front of a attribute makes it private
- In python,
adding a __ changes the name of the attribute to _Classname__attribute
We can create shared attributes by placing them directly
inside the class and not inside the constructor. And since this attribute is
not owned by any one object, we don’t need the self to create this attribute.
Such variables which are created at a class level are called static
variables.
Since
static variable is object independent, we need a way to access the getter
setter methods without an object. This is possible by creating static methods.
Static methods are those methods which can be accessed without an object. They
are accessed using the class name.
There
are two rules in creating such static methods:
- The methods
should not have self
- @staticmethod
must be written on top of it
@staticmethod
def
get_discount():
return Mobile
.__discount
@staticmethod
def
set_discount(discount
):
Mobile
.__discount
=discount
Complete
Solution:
·
class Mobile:
__discount = 50
def __init__(self, price, brand):
self.price = price
self.brand = brand
def purchase(self):
total = self.price - self.price * Mobile.__discount / 100
print (self.brand, "mobile with
price", self.price, "is available
after discount at", total)
@staticmethod
def enable_discount():
Mobile.set_discount(50)
@staticmethod
def disable_discount():
Mobile.set_discount(0)
@staticmethod
def get_discount():
return Mobile.__discount
@staticmethod
def set_discount(discount):
Mobile.__discount = discount
mob1=Mobile(20000, "Apple")
mob2=Mobile(30000, "Apple")
mob3=Mobile(5000, "Samsung")
Mobile.disable_discount()
mob1.purchase()
Mobile.enable_discount()
mob2.purchase()
Mobile.disable_discount()
mob3.purchase()
Summary:
- Static
attributes are created at class level.
- Static
attributes are accessed using ClassName.
- Static
attributes are object independent. We can access them without creating
instance (object) of the class in which they are defined.
- The value stored
in static attribute is shared between all instances(objects) of the class
in which the static attribute is defined.
Relationship
Relationship
|
Description
|
Example
|
Inheritance
|
When one object is a type of another
object
|
Mobile is a Product
|
Aggregation
|
When one object owns another object,
but they both have independent life cycle
|
Customer has an Address. Even if the
Customer is no more, there may be other customers in that address. So Address
continues to exist even after a customer is no more
|
Composition
|
When one object owns another object,
but they both have same life cycle
|
College has a department. If the
college closes, the department is also closed
|
AGGREGATION:
ust like Customer "has-a" name, Customer
"has-a" age, Customer "has-a" phone_no, now Customer also
"has-a" Address
add1=Address(123,"5th Lane",56001)
add2=Address(567,"6th Lane",82006)
cus1=Customer("Jack",24,1234,add1)
cus2=Customer("Jane",25,5678,add2)
Summary:
- Classes can have
relationships with other classes
- In aggregation
one class owns another though they have their own life cycle
- Aggregation is
represented using an diamond symbol in the class diagram
Inheritance:
FeaturePhone is inheriting the Phone and SmartPhone is
inheriting the Phone class (SmartPhone "is-A" phone, FeaturePhone
"is-A" phone). So Phone is the parent class and FeaturePhone and
SmartPhone are derived classes.
Advantages of inheritance:
We can keep common
properties in a single place. Thus any changes needs to be made need not be
repeated.
Inheritance encourages
code reuse thus saving us time.
If we want to add a new
type of phone later on, we can simply inherit the Phone class instead of
writing it from scratch.
When we have a inheritance
relationship, the attributes and behaviors get inherited, just like a child
inherits certain attributes and behaviours from its parent.
From a code perspective,
a child class inherits:
Constructor
Non Private Attributes
Non Private Methods
Method Overriding
When the child has a method with the same name
as that of the parent, it is said to override the parent’s method. This is
called as Method Overriding. Method overriding is also called as Polymorphism.
class
Phone:
def
__init__(self
, price
, brand
, camera
):
print
("Inside phone constructor")
self
.__price
= price
self
.brand
= brand
self
.camera
= camera
def
buy(self
):
print
("Buying a phone")
def
return_phone(self
):
print
("Returning a phone")
class
FeaturePhone(Phone
):
pass
class
SmartPhone(Phone
):
def
buy(self
):
print
("Buying a smartphone")
s
=SmartPhone
(20000,
"Apple",
13)
s
.buy
()
Even though the child class may override the
methods of the parent class, it might still decide to use the parent class
overridden method. To invoke anything belonging to the parent class, the child
class needs to use the super() function
class
Phone:
def
__init__(self
, price
, brand
, camera
):
print
("Inside phone constructor")
self
.__price
= price
self
.brand
= brand
self
.camera
= camera
def
buy(self
):
print
("Buying a phone")
def
return_phone(self
):
print
("Returning a phone")
class
FeaturePhone(Phone
):
pass
class
SmartPhone(Phone
):
def
buy(self
):
print
("Buying a smartphone")
super
().buy
()
s
=SmartPhone
(20000,
"Apple",
13)
s
.buy
()
To
access the parent class constructor we can use super(). Thus, the
data is passed to the child class constructor, from there the data is sent to
the parent class constructor and thus the attributes of the parent class get
inherited.
super()
function can be used to access the constructor or methods of the parent class,
but not the attributes. Also super() function can be used only inside a class
and not outside it
Summary:
A class can inherit from
another class.
Inheritance improves code
reuse
Constructor, attributes,
methods get inherited to the child class
The parent has no access
to the child class
Private properties of
parent are not accessible directly in child class
Child class can override
the attributes or methods. This is called method overriding
super() is an inbuilt
function which is used to invoke the parent class methods and constructor
class
Product:
def
review(self
):
print
("Product customer review")
class
Phone(Product
):
def
__init__(self
, price
, brand
, camera
):
print
("Inside phone constructor")
self
.__price
= price
self
.brand
= brand
self
.camera
= camera
def
buy(self
):
print
("Buying a phone")
def
return_phone(self
):
print
("Returning a phone")
class
SmartPhone(Phone
):
pass
s
=SmartPhone
(20000,
"Apple",
12)
s
.buy
()
s
.review
()
class
Phone:
def
__init__(self
, price
, brand
, camera
):
print
("Inside phone constructor")
self
.__price
= price
self
.brand
= brand
self
.camera
= camera
def
buy(self
):
print
("Buying a phone")
def
return_phone(self
):
print
("Returning a phone")
class
SmartPhone(Phone
):
pass
class
FeaturePhone(Phone
):
pass
SmartPhone
(1000,"Apple","13px").buy
()
class
Phone:
def
__init__(self
, price
, brand
, camera
):
print
("Inside phone constructor")
self
.__price
= price
self
.brand
= brand
self
.camera
= camera
def
buy(self
):
print
("Buying a phone")
def
return_phone(self
):
print
("Returning a phone")
class
Product:
def
review(self
):
print
("Customer review")
class
SmartPhone(Phone
, Product
):
pass
s
=SmartPhone
(20000,
"Apple",
12)
s
.buy
()
s
.review
()
If we programmatically declare our
return_policy() of Product class as an abstract method, then every sub-class of
Product class MUST override the abstract method.
from abc
import ABCMeta
, abstractmethod
class
Product(metaclass
=ABCMeta
):
@abstractmethod
def
return_policy(self
):
pass
Note:
Usually the parent class
is an abstract class.
An abstract class should
not be instantiated.
An abstract class may
contain 0 or many abstract methods
Abstract classes are
meant to be inherited.
The child class must
implement/override all the abstract methods of the parent class. Else the child
class cannot be instantiated. i.e., An abstract method must be overridden in
child class. If not, child class is considered as abstract.
Exception:
class CreditCard:
def __init__(self,card_no,balance):
self.card_no=card_no
self.balance=balance
class Customer:
def __init__(self,cards):
self.cards=cards
def purchase_item(self, price, card_no):
if price < 0:
raise Exception("Invalid Price")
if card_no not in self.cards:
raise Exception("Wrong card")
if price>self.cards[card_no].balance:
raise Exception("Wrong card")
card1=CreditCard(101,800)
card2=CreditCard(102,2000)
cards={card1.card_no:card1,card2.card_no:card2}
c=Customer(cards)
while(True):
card_no=int(input("Please enter a card number"))
try:
c.purchase_item(1200,card_no)
break
except Exception as e:
print("Something went
wrong. "+str(e))
Notes:
Custom exceptions are
created by inheriting the Exception class
Custom classes give
greater flexibility in handling exceptions
The parent class
exception must come after the child class exceptions in the except clause