Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
0% found this document useful (0 votes)
8 views

Advanced Python Material

Object-oriented programming (OOP) in Python is a paradigm that utilizes 'objects' containing data (attributes) and code (methods) to design programs. Key concepts include classes, which serve as blueprints for creating objects, and the use of inheritance for code reusability. Additionally, OOP emphasizes data hiding, instance methods, and the distinction between instance and class variables to enhance encapsulation and maintainability.

Uploaded by

jrn.begum
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
8 views

Advanced Python Material

Object-oriented programming (OOP) in Python is a paradigm that utilizes 'objects' containing data (attributes) and code (methods) to design programs. Key concepts include classes, which serve as blueprints for creating objects, and the use of inheritance for code reusability. Additionally, OOP emphasizes data hiding, instance methods, and the distinction between instance and class variables to enhance encapsulation and maintainability.

Uploaded by

jrn.begum
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
You are on page 1/ 72

What is Object Oriented

Programming in Python
Object-oriented programming (OOP) is a programming paradigm
based on the concept of "objects". The object contains both data and
code: Data in the form of properties (often known as attributes), and
code, in the form of methods (actions object can perform).

An object-oriented paradigm is to design the program using classes


and objects. Python programming language supports different
programming approaches like functional programming, modular
programming. One of the popular approaches is object-oriented
programming (OOP) to solve a programming problem is by creating
objects.

Python OOP concepts

An object has the following two characteristics:

 Attribute
 Behavior

For example, A Car is an object, as it has the following properties:

 name, price, color as attributes


 breaking, acceleration as behavior

One important aspect of OOP in Python is to create reusable


code using the concept of inheritance. This concept is also known as
DRY (Don't Repeat Yourself).

Class and Objects


In Python, everything is an object. A class is a blueprint for the
object. To create an object we require a model or plan or blueprint
which is nothing but class.
For example, you are creating a vehicle according to the Vehicle
blueprint (template). The plan contains all dimensions and structure.
Based on these descriptions, we can construct a car, truck, bus, or any
vehicle. Here, a car, truck, bus are objects of Vehicle class

A class contains the properties (attribute) and action (behavior) of the


object. Properties represent variables, and the methods represent
actions. Hence class includes both variables and methods.

Object is an instance of a class. The physical existence of a class is


nothing but an object. In other words, the object is an entity that has a
state and behavior. It may be any real-world object like the mouse,
keyboard, laptop, etc.

An object consists of :
 State: It is represented by the attributes of an object. It also reflects the
properties of an object.
 Behavior: It is represented by the methods of an object. It also reflects the
response of an object to other objects.
 Identity: It gives a unique name to an object and enables one object to
interact with other objects.

Class Attributes and Methods


When we design a class, we use instance variables and class variables.

In Class, attributes can be defined into two parts:

 Instance variables: The instance variables are attributes attached


to an instance of a class. We define instance variables in the
constructor ( the __init__() method of a class).
 Class Variables: A class variable is a variable that is declared
inside of class, but outside of any instance method
or __init()__ method.

Inside a Class, we can define the following three types of methods.

 Instance method: Used to access or modify the object attributes.


If we use instance variables inside a method, such methods are
called instance methods.
 Class method: Used to access or modify the class state. In
method implementation, if we use only class variables, then such
type of methods we should declare as a class method.
 Static method: It is a general utility method that performs a task
in isolation. Inside this method, we don’t use instance or class
variable because this static method doesn’t have access to the
class attributes.

Class Definition Syntax:

class ClassName:
# Statement

Object Definition Syntax:

obj = ClassName()
print(obj.atrr)

Class creates a user-defined data structure, which holds its own data members
and member functions, which can be accessed and used by creating an
instance of that class. A class is like a blueprint for an object.
Some points on Python class:
 Classes are created by keyword class.
 Attributes are the variables that belong to a class.
 Attributes are always public and can be accessed using the dot (.) operator.
Eg.: Myclass.Myattribute

Example:

# Python3 program to

# demonstrate instantiating

# a class

class Dog:

# A simple class
# attribute

attr1 = "mammal"

attr2 = "dog"

# A sample method

def fun(self):

print("I'm a", self.attr1)

print("I'm a", self.attr2)

# Driver code

# Object instantiation

Rodger = Dog()

# Accessing class attributes

# and method through objects

print(Rodger.attr1)

Rodger.fun()

Output:
mammal
I'm a mammal
I'm a dog
.

__init__ method
The __init__ method is similar to constructors in C++ and Java. Constructors
are used to initializing the object’s state. Like methods, a constructor also
contains a collection of statements(i.e. instructions) that are executed at the
time of Object creation. It runs as soon as an object of a class is instantiated.
The method is useful to do any initialization you want to do with your object.

# A Sample class with init method

class Person:

# init method or constructor

def __init__(self, name):

self.name = name

# Sample Method

def say_hi(self):

print('Hello, my name is', self.name)

p = Person('Nikhil')

p.say_hi()

Output:
Hello, my name is Nikhil

Class and Instance Variables


Instance variables are for data, unique to each instance and class variables are
for attributes and methods shared by all instances of the class. Instance
variables are variables whose value is assigned inside a constructor or method
with self whereas class variables are variables whose value is assigned in the
class.
Defining instance variables using a constructor.
# Python3 program to show that the variables with a value

# assigned in the class declaration, are class variables and

# variables inside methods and constructors are instance

# variables.

# Class for Dog

class Dog:

# Class Variable

animal = 'dog'

# The init method or constructor

def __init__(self, breed, color):

# Instance Variable

self.breed = breed

self.color = color

# Objects of Dog class

Rodger = Dog("Pug", "brown")

Buzo = Dog("Bulldog", "black")

print('Rodger details:')

print('Rodger is a', Rodger.animal)

print('Breed: ', Rodger.breed)


print('Color: ', Rodger.color)

print('\nBuzo details:')

print('Buzo is a', Buzo.animal)

print('Breed: ', Buzo.breed)

print('Color: ', Buzo.color)

# Class variables can be accessed using class

# name also

print("\nAccessing class variable using class name")

print(Dog.animal)

Output:
Rodger details:
Rodger is a dog
Breed: Pug
Color: brown

Buzo details:
Buzo is a dog
Breed: Bulldog
Color: black

Accessing class variable using class name


dog
Defining instance variables using the normal method.
# Python3 program to show that we can create

# instance variables inside methods

# Class for Dog

class Dog:

# Class Variable

animal = 'dog'

# The init method or constructor

def __init__(self, breed):

# Instance Variable

self.breed = breed

# Adds an instance variable

def setColor(self, color):

self.color = color

# Retrieves instance variable

def getColor(self):

return self.color

# Driver Code

Rodger = Dog("pug")

Rodger.setColor("brown")

print(Rodger.getColor())
Output:
brown

constructors
Constructors are generally used for instantiating an object. The task of
constructors is to initialize(assign values) to the data members of the class
when an object of the class is created. In Python the __init__() method is called
the constructor and is always called when an object is created.
Syntax of constructor declaration :
def __init__(self):
# body of the constructor
Types of constructors :
 default constructor: The default constructor is a simple constructor which
doesn’t accept any arguments. Its definition has only one argument which is
a reference to the instance being constructed.
 parameterized constructor: constructor with parameters is known as
parameterized constructor. The parameterized constructor takes its first
argument as a reference to the instance being constructed known as self
and the rest of the arguments are provided by the programmer.

Example of default constructor :


class GeekforGeeks:

# default constructor

def __init__(self):

self.geek = "GeekforGeeks"

# a method for printing data members

def print_Geek(self):

print(self.geek)

# creating object of the class


obj = GeekforGeeks()

# calling the instance method using the object obj

obj.print_Geek()

Output :
GeekforGeeks
Example of the parameterized constructor :

class Addition:

first = 0

second = 0

answer = 0

# parameterized constructor

def __init__(self, f, s):

self.first = f

self.second = s

def display(self):

print("First number = " + str(self.first))

print("Second number = " + str(self.second))

print("Addition of two numbers = " + str(self.answer))

def calculate(self):
self.answer = self.first + self.second

# creating object of the class

# this will invoke parameterized constructor

obj = Addition(1000, 2000)

# perform Addition

obj.calculate()

# display result

obj.display()

Output :
First number = 1000
Second number = 2000
Addition of two numbers = 3000

What is Data Hiding?

Data hiding is a concept which underlines the hiding of data or information from
the user. It is one of the key aspects of Object-Oriented programming
strategies. It includes object details such as data members, internal work. Data
hiding excludes full data entry to class members and defends object integrity by
preventing unintended changes. Data hiding also minimizes system complexity
for increase robustness by limiting interdependencies between software
requirements. Data hiding is also known as information hiding. In class, if we
declare the data members as private so that no other class can access the data
members, then it is a process of hiding data.
Data Hiding in Python:
The Python document introduces Data Hiding as isolating the user from a part
of program implementation. Some objects in the module are kept internal,
unseen, and unreachable to the user. Modules in the program are easy enough
to understand how to use the application, but the client cannot know how the
application functions. Thus, data hiding imparts security, along with discarding
dependency. Data hiding in Python is the technique to defend access to specific
users in the application. Python is applied in every technical area and has a
user-friendly syntax and vast libraries. Data hiding in Python is performed using
the __ double underscore before done prefix. This makes the class members
non-public and isolated from the other classes.

Example

class Solution:

__privateCounter = 0

def sum(self):

self.__privateCounter += 1

print(self.__privateCounter)

count = Solution()

count.sum()

count.sum()

# Here it will show error because it unable

# to access private member

print(count.__privateCount)

Output:
Traceback (most recent call last):
File "/home/db01b918da68a3747044d44675be8872.py", line 11, in
<module>
print(count.__privateCount)
AttributeError: 'Solution' object has no attribute
'__privateCount'
To rectify the error, we can access the private member through the class
name :

class Solution:

__privateCounter = 0

def sum(self):

self.__privateCounter += 1

print(self.__privateCounter)

count = Solution()

count.sum()

count.sum()

# Here we have accessed the private data

# member through class name.

print(count._Solution__privateCounter)

Output:
1
2
2
Advantages of Data Hiding:
1. It helps to prevent damage or misuse of volatile data by hiding it from the
public.
2. The class objects are disconnected from the irrelevant data.
3. It isolates objects as the basic concept of OOP.
4. It increases the security against hackers that are unable to access important
data.
Disadvantages of Data Hiding:
1. It enables programmers to write lengthy code to hide important data from
common clients.
2. The linkage between the visible and invisible data makes the objects work
faster, but data hiding prevents this linkage.

What is Instance Methods in Python


If we use instance variables inside a method, such methods are called
instance methods. The instance method performs a set of actions
on the data/value provided by the instance variables.

 A instance method is bound to the object of the class.


 It can access or modify the object state by changing the value of a
instance variables
When we create a class in Python, instance methods are used
regularly. To work with an instance method, we use the self keyword.
We use the self keyword as the first parameter to a method.
The self refers to the current object.

Any method we create in a class will automatically be created as an


instance method unless we explicitly tell Python that it is a class or
static method.

Instance method in Python

Define Instance Method


Instance variables are not shared between objects. Instead, every
object has its copy of the instance attribute. Using the instance
method, we can access or modify the calling object’s attributes.

Instance methods are defined inside a class, and it is pretty similar to


defining a regular function.

 Use the def keyword to define an instance method in Python.


 Use self as the first parameter in the instance method
when defining it. The self parameter refers to the current object.
 Using the self parameter to access or modify the current object
attributes.
You may use a variable named differently for self, but it is discouraged
since self is the recommended convention in Python.

Let’s see the example to create an instance method show() in the


Student class to display the student details.

Example:

class Student:
# constructor
def __init__(self, name, age):
# Instance variable
self.name = name
self.age = age

# instance method to access instance variable


def show(self):
print('Name:', self.name, 'Age:', self.age)

Calling An Instance Method


We use an object and dot (.) operator to execute the block of code or
action defined in the instance method.

 First, create instance variables name and age in the Student class.
 Next, create an instance method display() to print student name
and age.
 Next, create object of a Student class to call the instance method.
et’s see how to call an instance method show() to access the student
object details such as name and age.

class Student:
# constructor
def __init__(self, name, age):
# Instance variable
self.name = name
self.age = age
# instance method access instance variable
def show(self):
print('Name:', self.name, 'Age:', self.age)

# create first object


print('First Student')
emma = Student("Jessa", 14)
# call instance method
emma.show()

# create second object


print('Second Student')
kelly = Student("Kelly", 16)
# call instance method
kelly.show()

Output:

First Student

Name: Jessa Age: 14

Second Student

Name: Kelly Age: 16

Note:

Inside any instance method, we can use self to access any data or
method that reside in our class. We are unable to access it without
a self parameter.

An instance method can freely access attributes and even modify the
value of attributes of an object by using the self parameter.

By Using self.__class__ attribute we can access the class attributes and


change the class state. Therefore instance method gives us control of
changing the object as well as the class state.
Modify Instance Variables inside
Instance Method
Let’s create the instance method update() method to modify the
student age and roll number when student data details change.

class Student:
def __init__(self, roll_no, name, age):
# Instance variable
self.roll_no = roll_no
self.name = name
self.age = age

# instance method access instance variable


def show(self):
print('Roll Number:', self.roll_no, 'Name:', self.name, 'Age:',
self.age)

# instance method to modify instance variable


def update(self, roll_number, age):
self.roll_no = roll_number
self.age = age

# create object
print('class VIII')
stud = Student(20, "Emma", 14)
# call instance method
stud.show()

# Modify instance variables


print('class IX')
stud.update(35, 15)
stud.show()

Output:

class VIII

Roll Number: 20 Name: Emma Age: 14

class IX

Roll Number: 35 Name: Emma Age: 15


Create Instance Variables in
Instance Method
Till the time we used constructor to create instance attributes. But,
instance attributes are not specific only to the __init__() method; they
can be defined elsewhere in the class. So, let’s see how to create an
instance variable inside the method.

Example:

class Student:
def __init__(self, roll_no, name, age):
# Instance variable
self.roll_no = roll_no
self.name = name
self.age = age

# instance method to add instance variable


def add_marks(self, marks):
# add new attribute to current object
self.marks = marks

# create object
stud = Student(20, "Emma", 14)
# call instance method
stud.add_marks(75)

# display object
print('Roll Number:', stud.roll_no, 'Name:', stud.name, 'Age:', stud.age,
'Marks:', stud.marks)

Output:

Roll Number: 20 Name: Emma Age: 14 Marks: 75

Dynamically Add Instance Method to


a Object
Usually, we add methods to a class body when defining a class.
However, Python is a dynamic language that allows us to add or delete
instance methods at runtime. Therefore, it is helpful in the following
scenarios.

 When class is in a different file, and you don’t have access to


modify the class structure
 You wanted to extend the class functionality without changing its
basic structure because many systems use the same structure.
Let’s see how to add an instance method in the Student class at
runtime.

Example:

We should add a method to the object, so other instances don’t have


access to that method. We use the types module’s MethodType() to add a
method to an object. Below is the simplest way to method to an object.

import types

class Student:
# constructor
def __init__(self, name, age):
self.name = name
self.age = age

# instance method
def show(self):
print('Name:', self.name, 'Age:', self.age)

# create new method


def welcome(self):
print("Hello", self.name, "Welcome to Class IX")

# create object
s1 = Student("Jessa", 15)

# Add instance method to object


s1.welcome = types.MethodType(welcome, s1)
s1.show()

# call newly added method


s1.welcome()
Output:

Name: Jessa Age: 15


Hello Jessa Welcome to Class IX

Dynamically Delete Instance


Methods
We can dynamically delete the instance method from the class. In
Python, there are two ways to delete method:

 By using the del operator


 By using delattr() method
By using the del operator

The del operator removes the instance method added by class.

Example:

In this example, we will delete an instance method


named percentage() from a Student class. If you try to access it after
removing it, you’ll get an Attribute Error.

class Student:
# constructor
def __init__(self, name, age):
self.name = name
self.age = age

# instance method
def show(self):
print('Name:', self.name, 'Age:', self.age)

# instance method
def percentage(self, sub1, sub2):
print('Percentage:', (sub1 + sub2) / 2)

emma = Student('Emma', 14)


emma.show()
emma.percentage(67, 62)
# Delete the method from class using del operator
del emma.percentage

# Again calling percentage() method


# It will raise error
emma.percentage(67, 62)

Run
Output:

Name: Emma Age: 14

Percentage: 64.5

File "/demos/oop/delete_method.py", line 21, in <module>

del emma.percentage

AttributeError: percentage

By using the delattr() method

The delattr() is used to delete the named attribute from the object with
the prior permission of the object. Use the following syntax to delete
the instance method.

delattr(object, name)

 object: the object whose attribute we want to delete.


 name:the name of the instance method you want to delete from the
object.
Example:

In this example, we will delete an instance method


named percentage() from a Student class.

emma = Student('Emma', 14)


emma.show()
emma.percentage(67, 62)

# delete instance method percentage() using delattr()


delattr(emma, 'percentage')
emma.show()

# Again calling percentage() method


# It will raise error
emma.percentage(67, 62)

What is an Class Variable in Python?


If the value of a variable is not varied from object to object,
such types of variables are called class variables or static variables.

Class variables are shared by all instances of a class. Unlike


instance variable, the value of a class variable is not varied from object
to object,

In Python, Class variables are declared when a class is being


constructed. They are not defined inside any methods of a class
because of this only one copy of the static variable will be created and
shared between all objects of the class.

For example, in Student class, we can have different instance variables


such as name and roll number because each student’s name and roll
number are different.

But, if we want to include the school name in the student class, we


must use the class variable instead of an instance variable as the
school name is the same for all students. So instead of maintaining the
separate copy in each object, we can create a class variable that will
hold the school name so all students (objects) can share it.

We can add any number of class variables in a class.

Understand Class Variables

Create Class Variables


A class variable is declared inside of class, but outside of any instance
method or __init__() method.

By convention, typically it is placed right below the class header and


before the constructor method and other methods.

Example:

class Student:
# Class variable
school_name = 'ABC School '

def __init__(self, name, roll_no):


self.name = name
self.roll_no = roll_no

# create first object


s1 = Student('Emma', 10)
print(s1.name, s1.roll_no, Student.school_name)
# access class variable

# create second object


s2 = Student('Jessa', 20)
# access class variable
print(s2.name, s2.roll_no, Student.school_name)

Output

Emma 10 ABC School

Jessa 20 ABC School

In the above example, we created the class variable school_name and


accessed it using the object and class name.

Accessing Class Variables


We can access static variables either by class name or by object
reference, but it is recommended to use the class name.
In Python, we can access the class variable in the following places

 Access inside the constructor by using either self parameter or


class name.
 Access class variable inside instance method by using either self
of class name
 Access from outside of class by using either object reference or
class name.
Example 1: Access Class Variable in the constructor

class Student:
# Class variable
school_name = 'ABC School '

# constructor
def __init__(self, name):
self.name = name
# access class variable inside constructor using self
print(self.school_name)
# access using class name
print(Student.school_name)

# create Object
s1 = Student('Emma')

Output

ABC School

ABC School

Example 2: Access Class Variable in Instance method and outside


class

class Student:
# Class variable
school_name = 'ABC School '

# constructor
def __init__(self, name, roll_no):
self.name = name
self.roll_no = roll_no
# Instance method
def show(self):
print('Inside instance method')
# access using self
print(self.name, self.roll_no, self.school_name)
# access using class name
print(Student.school_name)

# create Object
s1 = Student('Emma', 10)
s1.show()

print('Outside class')
# access class variable outside class
# access using object reference
print(s1.school_name)

# access using class name


print(Student.school_name)

Output

Inside instance method

Emma 10 ABC School

ABC School

Outside class

ABC School

ABC School

In this example, we accessed the class variable school_name using class


name and a self keyword inside a method.

Modify Class Variables


Generally, we assign value to a class variable inside the class
declaration. However, we can change the value of the class variable
either in the class or outside of class.
Note: We should change the class variable’s value using the class
name only.

Example

class Student:
# Class variable
school_name = 'ABC School '

# constructor
def __init__(self, name, roll_no):
self.name = name
self.roll_no = roll_no

# Instance method
def show(self):
print(self.name, self.roll_no, Student.school_name)

# create Object
s1 = Student('Emma', 10)
print('Before')
s1.show()

# Modify class variable


Student.school_name = 'XYZ School'
print('After')
s1.show()

Output:

Before

Emma 10 ABC School

After

Emma 10 XYZ School

Note:

It is best practice to use a class name to change the value of a class


variable. Because if we try to change the class variable’s value by
using an object, a new instance variable is created for that particular
object, which shadows the class variables.
Example:

class Student:
# Class variable
school_name = 'ABC School '

# constructor
def __init__(self, name, roll_no):
self.name = name
self.roll_no = roll_no

# create Objects
s1 = Student('Emma', 10)
s2 = Student('Jessa', 20)

print('Before')
print(s1.name, s1.roll_no, s1.school_name)
print(s2.name, s2.roll_no, s2.school_name)

# Modify class variable using object reference


s1.school_name = 'PQR School'
print('After')
print(s1.name, s1.roll_no, s1.school_name)
print(s2.name, s2.roll_no, s2.school_name)

Output:

Before

Emma 10 ABC School

Jessa 20 ABC School

After

Emma 10 PQR School

Jessa 20 ABC School

A new instance variable is created for the s1 object, and this variable
shadows the class variables. So always use the class name to modify
the class variable.
Class Variable vs Instance variables
The following table shows the difference between the instance variable
and the class variable.

In Python, properties can be defined into two parts:

 Instance variables: Instance variable’s value varies from object


to object. Instance variables are not shared by objects. Every
object has its own copy of the instance attribute
Instance Variable Class Variable

Instance variables are not shared by objects. Every object


Class variables are shared by all instances.
has its own copy of the instance attribute

Instance variables are declared inside the constructor i.e., Class variables are declared inside the class definition but

the __init__() method. outside any of the instance methods and constructors.

It is gets created when an instance of the class is created. It is created when the program begins to execute.

Changes made to these variables through one object will Changes made in the class variable will reflect in all

not reflect in another object. objects.

 Class Variables: A class variable is a variable that is declared


inside of class, but outside of any instance method
or __init__() method. Class variables are shared by all instances of
a class.
Example:

Let’s see the example to create a class variable and instance variable.

class Car:
# Class variable
manufacturer = 'BMW'

def __init__(self, model, price):


# instance variable
self.model = model
self.price = price

# create Object
car = Car('x1', 2500)
print(car.model, car.price, Car.manufacturer)

Output:

x1 2500 BMW

Class Variables In Inheritance


As you know, only one copy of the class variable will be created and
shared between all objects of that class.

When we use inheritance, all variables and methods of the base class
are available to the child class. In such cases, We can also change the
value of the parent class’s class variable in the child class.

We can use the parent class or child class name to change the value of
a parent class’s class variable in the child class.

Example

class Course:
# class variable
course = "Python"

class Student(Course):

def __init__(self, name):


self.name = name

def show_student(self):
# Accessing class variable of parent class
print('Before')
print("Student name:", self.name, "Course Name:", Student.course)
# changing class variable value of base class
print('Now')
Student.course = "Machine Learning"
print("Student name:", self.name, "Course Name:", Student.course)
# creating object of Student class
stud = Student("Emma")
stud.show_student()

Run
Output

Before

Student name: Emma Course Name: Python

Now

Student name: Emma Course Name: Machine Learning

What if both child class and parent class has the same class
variable name. In this case, the child class will not inherit the class
variable of a base class. So it is recommended to create a separate
class variable for child class instead of inheriting the base class
variable.

Example:

class Course:
# class variable
course = "Python"

class Student(Course):
# class variable
course = "SQL"

def __init__(self, name):


self.name = name

def show_student(self):
# Accessing class variable
print('Before')
print("Student name:", self.name, "Course Name:", Student.course)
# changing class variable's value
print('Now')
Student.course = "Machine Learning"
print("Student name:", self.name, "Course Name:", Student.course)
# creating object of Student class
stud = Student("Emma")
stud.show_student()

# parent class course name


print('Parent Class Course Name:', Course.course)

Run
Output:

Before

Student name: Emma Course Name: SQL

Now

Student name: Emma Course Name: Machine Learning

Parent Class Course Name: Python

Wrong Use of Class Variables


In Python, we should properly use the class variable because all
objects share the same copy. Thus, if one of the objects modifies the
value of a class variable, then all objects start referring to the fresh
copy.

For example,

Example

class Player:
# class variables
club = 'Chelsea'
sport = 'Football'

def __init__(self, name):


# Instance variable
self.name = name

def show(self):
print("Player :", 'Name:', self.name, 'Club:', self.club, 'Sports:',
self.sport)

p1 = Player('John')

# wrong use of class variable


p1.club = 'FC'
p1.show()

p2 = Player('Emma')
p2.sport = 'Tennis'
p2.show()

# actual class variable value


print('Club:', Player.club, 'Sport:', Player.sport)

Run
Output

Player : Name: John Club: FC Sports: Football

Player : Name: Emma Club: Chelsea Sports: Tennis

Club: Chelsea Sport: Football

In the above example, the instance variable name is unique for each
player. The class variable team and sport can be accessed and modified
by any object.

Because both objects modified the class variable, a new instance


variable is created for that particular object with the same name as the
class variable, which shadows the class variables.

In our case, for object p1 new instance variable club gets created, and
for object p2 new instance variable sport gets created.

So when you try to access the class variable using the p1 or p2 object, it
will not return the actual class variable value.
inheritance in Python
The process of inheriting the properties of the parent class into a child class is called
inheritance. The existing class is called a base class or parent class and the new class is called a
subclass or child class or derived class.

In this Python lesson, you will learn inheritance, method overloading, method overriding, types
of inheritance, and MRO (Method Resolution Order).

In Object-oriented programming, inheritance is an important aspect. The main purpose of


inheritance is the reusability of code because we can use the existing class to create a new class
instead of creating it from scratch.

In inheritance, the child class acquires all the data members, properties, and functions from the
parent class. Also, a child class can also provide its specific implementation to the methods of
the parent class.

For example, In the real world, Car is a sub-class of a Vehicle class. We can create a Car by
inheriting the properties of a Vehicle such as Wheels, Colors, Fuel tank, engine, and add extra
properties in Car as required.

Syntax

class BaseClass:
Body of base class
class DerivedClass(BaseClass):
Body of derived class

Types Of Inheritance
In Python, based upon the number of child and parent classes involved, there are five types of
inheritance. The type of inheritance are listed below:

1. Single inheritance
2. Multiple Inheritance
3. Multilevel inheritance
4. Hierarchical Inheritance
5. Hybrid Inheritance

Now let’s see each in detail with an example.


Single Inheritance
In single inheritance, a child class inherits from a single-parent class. Here is one child class and
one parent class.

Python Single Inheritance

Example

Let’s create one parent class called ClassOne and one child class called ClassTwo to implement
single inheritance.

# Base class
class Vehicle:
def Vehicle_info(self):
print('Inside Vehicle class')

# Child class
class Car(Vehicle):
def car_info(self):
print('Inside Car class')

# Create object of Car


car = Car()

# access Vehicle's info using car object


car.Vehicle_info()
car.car_info()

Output

Inside Vehicle class

Inside Car class

Multiple Inheritance
In multiple inheritance, one child class can inherit from multiple parent classes. So here is one
child class and multiple parent classes.

Python Multiple Inheritance

Example
# Parent class 1
class Person:
def person_info(self, name, age):
print('Inside Person class')
print('Name:', name, 'Age:', age)

# Parent class 2
class Company:
def company_info(self, company_name, location):
print('Inside Company class')
print('Name:', company_name, 'location:', location)

# Child class
class Employee(Person, Company):
def Employee_info(self, salary, skill):
print('Inside Employee class')
print('Salary:', salary, 'Skill:', skill)

# Create object of Employee


emp = Employee()

# access data
emp.person_info('Jessa', 28)
emp.company_info('Google', 'Atlanta')
emp.Employee_info(12000, 'Machine Learning')

Run

Output

Inside Person class

Name: Jessa Age: 28

Inside Company class

Name: Google location: Atlanta

Inside Employee class

Salary: 12000 Skill: Machine Learning

In the above example, we created two parent classes Person and Company respectively. Then we
create one child called Employee which inherit from Person and Company classes.
Multilevel inheritance
In multilevel inheritance, a class inherits from a child class or derived class. Suppose three
classes A, B, C. A is the superclass, B is the child class of A, C is the child class of B. In other
words, we can say a chain of classes is called multilevel inheritance.Python Multilevel
Inheritance

Example

# Base class
class Vehicle:
def Vehicle_info(self):
print('Inside Vehicle class')

# Child class
class Car(Vehicle):
def car_info(self):
print('Inside Car class')

# Child class
class SportsCar(Car):
def sports_car_info(self):
print('Inside SportsCar class')

# Create object of SportsCar


s_car = SportsCar()

# access Vehicle's and Car info using SportsCar object


s_car.Vehicle_info()
s_car.car_info()
s_car.sports_car_info()

Output

Inside Vehicle class

Inside Car class

Inside SportsCar class

In the above example, we can see there are three classes


named Vehicle, Car, SportsCar. Vehicle is the superclass, Car is a child of
Vehicle, SportsCar is a child of Car. So we can see the chaining of classes.
Hierarchical Inheritance
In Hierarchical inheritance, more than one child class is derived from a single parent class. In
other words, we can say one parent class and multiple child classes.Python hierarchical
inheritance

Example

Let’s create ‘Vehicle’ as a parent class and two child class ‘Car’ and ‘Truck’ as a parent class.

class Vehicle:
def info(self):
print("This is Vehicle")

class Car(Vehicle):
def car_info(self, name):
print("Car name is:", name)

class Truck(Vehicle):
def truck_info(self, name):
print("Truck name is:", name)

obj1 = Car()
obj1.info()
obj1.car_info('BMW')

obj2 = Truck()
obj2.info()
obj2.truck_info('Ford')

Output

This is Vehicle

Car name is: BMW

This is Vehicle

Truck name is: Ford

Hybrid Inheritance
When inheritance is consists of multiple types or a combination of different inheritance is called
hybrid inheritance.Python hybrid inheritance

Example

class Vehicle:
def vehicle_info(self):
print("Inside Vehicle class")

class Car(Vehicle):
def car_info(self):
print("Inside Car class")

class Truck(Vehicle):
def truck_info(self):
print("Inside Truck class")

# Sports Car can inherits properties of Vehicle and Car


class SportsCar(Car, Vehicle):
def sports_car_info(self):
print("Inside SportsCar class")

# create object
s_car = SportsCar()

s_car.vehicle_info()
s_car.car_info()
s_car.sports_car_info()

Run

Note: In the above example, hierarchical and multiple inheritance exists. Here we created,
parent class Vehicle and two child classes named Car and Truck this is hierarchical inheritance.

Another is SportsCar inherit from two parent classes named Car and Vehicle. This is multiple
inheritance.

Python super() function


When a class inherits all properties and behavior from the parent class is called inheritance. In
such a case, the inherited class is a subclass and the latter class is the parent class.

In child class, we can refer to parent class by using the super() function. The super function
returns a temporary object of the parent class that allows us to call a parent class method inside a
child class method.

Benefits of using the super() function.


1. We are not required to remember or specify the parent class name to access its methods.
2. We can use the super() function in both single and multiple inheritances.
3. The super() function support code reusability as there is no need to write the entire function

Example

class Company:
def company_name(self):
return 'Google'

class Employee(Company):
def info(self):
# Calling the superclass method using super()function
c_name = super().company_name()
print("Jessa works at", c_name)

# Creating object of child class


emp = Employee()
emp.info()

Run

Output:

Jessa works at Google

In the above example, we create a parent class Company and child class Employee.
In Employee class, we call the parent class method by using a super() function.

issubclass()
In Python, we can verify whether a particular class is a subclass of another class. For this
purpose, we can use Python built-in function issubclass(). This function returns True if the
given class is the subclass of the specified class. Otherwise, it returns False.

Syntax

issubclass(class, classinfo)

Where,

 class: class to be checked.


 classinfo: a class, type, or a tuple of classes or data types.
Example

class Company:
def fun1(self):
print("Inside parent class")

class Employee(Company):
def fun2(self):
print("Inside child class.")

class Player:
def fun3(self):
print("Inside Player class.")

# Result True
print(issubclass(Employee, Company))

# Result False
print(issubclass(Employee, list))

# Result False
print(issubclass(Player, Company))

# Result True
print(issubclass(Employee, (list, Company)))

# Result True
print(issubclass(Company, (list, Company)))

Run

Also, see Python isinstance().

Method Overriding
In inheritance, all members available in the parent class are by default available in the child
class. If the child class does not satisfy with parent class implementation, then the child class is
allowed to redefine that method by extending additional functions in the child class. This concept
is called method overriding.

When a child class method has the same name, same parameters, and same return type as a
method in its superclass, then the method in the child is said to override the method in the parent
class.

Python method overriding

Example
class Vehicle:
def max_speed(self):
print("max speed is 100 Km/Hour")

class Car(Vehicle):
# overridden the implementation of Vehicle class
def max_speed(self):
print("max speed is 200 Km/Hour")

# Creating object of Car class


car = Car()
car.max_speed()

Run

Output:

max speed is 200 Km/Hour

In the above example, we create two classes named Vehicle (Parent class) and Car (Child
class). The class Car extends from the class Vehicle so, all properties of the parent class are
available in the child class. In addition to that, the child class redefined the
method max_speed().

Method Resolution Order in Python


In Python, Method Resolution Order(MRO) is the order by which Python looks for a method
or attribute. First, the method or attribute is searched within a class, and then it follows the
order we specified while inheriting.

This order is also called the Linearization of a class, and a set of rules is called MRO (Method
Resolution Order). The MRO plays an essential role in multiple inheritances as a single
method may found in multiple parent classes.

In multiple inheritance, the following search order is followed.

1. First, it searches in the current parent class if not available, then searches in the parents class
specified while inheriting (that is left to right.)
2. We can get the MRO of a class. For this purpose, we can use either the mro attribute or
the mro() method.

Example
class A:

def process(self):

print(" In class A")

class B(A):

def process(self):

print(" In class B")

class C(B, A):

def process(self):

print(" In class C")

# Creating object of C class

C1 = C()

C1.process()

print(C.mro())

# In class C

# [<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>]

In the above example, we create three classes named A, B and C. Class B is inherited from A,
class C inherits from B and A. When we create an object of the C class and calling
the process() method, Python looks for the process() method in the current class in
the C class itself.

Then search for parent classes, namely B and A, because C class inherit from B and A. that is, C(B,
A) and always search in left to right manner.
class A:

def process(self):

print(" In class A")

class B(A):

def process(self):

print(" In class B")

class C(A,B):

def process(self):

print(" In class C")

# Creating object of C class

C1 = C()

C1.process()

class A:

def process(self):

print(" In class A")

class B():

def process(self):

print(" In class B")


class C(A,B):

def process1(self):

print(" In class C")

# Creating object of C class

C1 = C()

C1.process()

use this code, the author did a small mistake by inheriting class A in B, just
remove that A. It should look like classB()

What is Polymorphism in Python?


Polymorphism in Python is the ability of an object to take many forms.
In simple words, polymorphism allows us to perform the same action in
many different ways.

For example, Jessa acts as an employee when she is at the office.


However, when she is at home, she acts like a wife. Also, she
represents herself differently in different places. Therefore, the same
person takes different forms as per the situation.A person takes
different forms

In polymorphism, a method can process objects differently


depending on the class type or data type. Let’s see simple
examples to understand it better.

Polymorphism in Built-in function len()


The built-in function len() calculates the length of an object depending
upon its type. If an object is a string, it returns the count of characters,
and If an object is a list, it returns the count of items in a list.
The len() method treats an object as per its class type.

Example:

students = ['Emma', 'Jessa', 'Kelly']


school = 'ABC School'

# calculate count
print(len(students))
print(len(school))

Output

10

Polymorphic len() function

Polymorphism With Inheritance


Polymorphism is mainly used with inheritance. In inheritance, child
class inherits the attributes and methods of a parent class. The
existing class is called a base class or parent class, and the new class
is called a subclass or child class or derived class.

Using method overriding polymorphism allows us to defines methods


in the child class that have the same name as the methods in the
parent class. This process of re-implementing the inherited
method in the child class is known as Method Overriding.

Advantage of method overriding

 It is effective when we want to extend the functionality by altering


the inherited method. Or the method inherited from the parent
class doesn’t fulfill the need of a child class, so we need to re-
implement the same method in the child class in a different way.
 Method overriding is useful when a parent class has multiple child
classes, and one of that child class wants to redefine the method.
The other child classes can use the parent class method. Due to
this, we don’t need to modification the parent class code
In polymorphism, Python first checks the object’s class type and
executes the appropriate method when we call the method. For
example, If you create the Car object, then Python calls
the speed() method from a Car class.

Let’s see how it works with the help of an example.

Example: Method Overriding


In this example, we have a vehicle class as a parent and a ‘Car’ and
‘Truck’ as its sub-class. But each vehicle can have a different seating
capacity, speed, etc., so we can have the same instance method name
in each class but with a different implementation. Using this code can
be extended and easily maintained over time.olymorphism with
Inheritance

class Vehicle:

def __init__(self, name, color, price):


self.name = name
self.color = color
self.price = price

def show(self):
print('Details:', self.name, self.color, self.price)

def max_speed(self):
print('Vehicle max speed is 150')

def change_gear(self):
print('Vehicle change 6 gear')

# inherit from vehicle class


class Car(Vehicle):
def max_speed(self):
print('Car max speed is 240')

def change_gear(self):
print('Car change 7 gear')

# Car Object
car = Car('Car x1', 'Red', 20000)
car.show()
# calls methods from Car class
car.max_speed()
car.change_gear()

# Vehicle Object
vehicle = Vehicle('Truck x1', 'white', 75000)
vehicle.show()
# calls method from a Vehicle class
vehicle.max_speed()
vehicle.change_gear()

Output:

Details: Car x1 Red 20000

Car max speed is 240

Car change 7 gear

Details: Truck x1 white 75000

Vehicle max speed is 150

Vehicle change 6 gear

As you can see, due to polymorphism, the Python interpreter


recognizes that the max_speed() and change_gear() methods are
overridden for the car object. So, it uses the one defined in the child
class (Car)

On the other hand, the show() method isn’t overridden in the Car class,
so it is used from the Vehicle class.

Overrride Built-in Functions


In Python, we can change the default behavior of the built-in functions.
For example, we can change or extend the built-in functions such
as len(), abs(), or divmod() by redefining them in our class. Let’s see the
example.

Example

In this example, we will redefine the function len()

class Shopping:
def __init__(self, basket, buyer):
self.basket = list(basket)
self.buyer = buyer

def __len__(self):
print('Redefine length')
count = len(self.basket)
# count total items in a different way
# pair of shoes and shir+pant
return count * 2

shopping = Shopping(['Shoes', 'dress'], 'Jessa')


print(len(shopping))

Output

Redefine length

Polymorphism In Class methods


Polymorphism with class methods is useful when we group different
objects having the same method. we can add them to a list or a tuple,
and we don’t need to check the object type before calling their
methods. Instead, Python will check object type at runtime and call the
correct method. Thus, we can call the methods without being
concerned about which class type each object is. We assume that
these methods exist in each class.

Python allows different classes to have methods with the same name.

 Let’s design a different class in the same way by adding the same
methods in two or more classes.
 Next, create an object of each class
 Next, add all objects in a tuple.
 In the end, iterate the tuple using a for loop and call methods of a
object without checking its class.
Example

In the below example, fuel_type() and max_speed() are the instance


methods created in both classes.

class Ferrari:
def fuel_type(self):
print("Petrol")

def max_speed(self):
print("Max speed 350")

class BMW:
def fuel_type(self):
print("Diesel")

def max_speed(self):
print("Max speed is 240")

ferrari = Ferrari()
bmw = BMW()

# iterate objects of same type


for car in (ferrari, bmw):
# call methods without checking class of object
car.fuel_type()
car.max_speed()

Output

Petrol

Max speed 350

Diesel

Max speed is 240


As you can see, we have created two classes Ferrari and BMW. They
have the same instance method names fuel_type() and max_speed().
However, we have not linked both the classes nor have we used
inheritance.

We packed two different objects into a tuple and iterate through it


using a car variable. It is possible due to polymorphism because we
have added the same method in both classes Python first checks the
object’s class type and executes the method present in its class.

Polymorphism with Function and Objects


We can create polymorphism with a function that can take
any object as a parameter and execute its method without checking its
class type. Using this, we can call object actions using the same
function instead of repeating method calls.

Example

class Ferrari:
def fuel_type(self):
print("Petrol")

def max_speed(self):
print("Max speed 350")

class BMW:
def fuel_type(self):
print("Diesel")

def max_speed(self):
print("Max speed is 240")

# normal function
def car_details(obj):
obj.fuel_type()
obj.max_speed()

ferrari = Ferrari()
bmw = BMW()

car_details(ferrari)
car_details(bmw)

Run
Output

Petrol

Max speed 350

Diesel

Max speed is 240

Polymorphism In Built-in Methods


The word polymorphism is taken from the Greek words poly (many)
and morphism (forms). It means a method can process objects
differently depending on the class type or data type.

The built-in function reversed(obj) returns the iterable by reversing the


given object. For example, if you pass a string to it, it will reverse it.
But if you pass a list of strings to it, it will return the iterable by
reversing the order of elements (it will not reverse the individual
string).

Let us see how a built-in method process objects having different data
types.

Example:

students = ['Emma', 'Jessa', 'Kelly']


school = 'ABC School'

print('Reverse string')
for i in reversed('PYnative'):
print(i, end=' ')

print('\nReverse list')
for i in reversed(['Emma', 'Jessa', 'Kelly']):
print(i, end=' ')

Run
Output:
Reverse string

e v i t a n Y P

Reverse list

Kelly Jessa Emma

Method Overloading
The process of calling the same method with different parameters is
known as method overloading. Python does not support method
overloading. Python considers only the latest defined method even if
you overload the method. Python will raise a TypeError if you overload
the method.

Example

def addition(a, b):


c = a + b
print(c)

def addition(a, b, c):


d = a + b + c
print(d)

# the below line shows an error


# addition(4, 5)

# This line will call the second product method


addition(3, 7, 5)

Run
To overcome the above problem, we can use different ways to achieve
the method overloading. In Python, to overload the class method, we
need to write the method’s logic so that different code executes inside
the function depending on the parameter passes.
For example, the built-in function range() takes three parameters and
produce different result depending upon the number of parameters
passed to it.

Example:

for i in range(5): print(i, end=', ')


print()
for i in range(5, 10): print(i, end=', ')
print()
for i in range(2, 12, 2): print(i, end=', ')

Run
Output:

0, 1, 2, 3, 4,

5, 6, 7, 8, 9,

2, 4, 6, 8, 10,

Let’s assume we have an area() method to calculate the area of a


square and rectangle. The method will calculate the area depending
upon the number of parameters passed to it.

 If one parameter is passed, then the area of a square is calculated


 If two parameters are passed, then the area of a rectangle is
calculated.
Example: User-defined polymorphic method

class Shape:
# function with two default parameters
def area(self, a, b=0):
if b > 0:
print('Area of Rectangle is:', a * b)
else:
print('Area of Square is:', a ** 2)

square = Shape()
square.area(5)
rectangle = Shape()
rectangle.area(5, 3)

Output:

Area of Square is: 25

Area of Rectangle is: 15

Operator Overloading in Python


Operator overloading means changing the default behavior of
an operator depending on the operands (values) that we use. In other
words, we can use the same operator for multiple purposes.

For example, the + operator will perform an arithmetic addition


operation when used with numbers. Likewise, it will perform
concatenation when used with strings.

The operator + is used to carry out different operations for distinct


data types. This is one of the most simple occurrences of
polymorphism in Python.

Example:

# add 2 numbers
print(100 + 200)

# concatenate two strings


print('Jess' + 'Roy')

# merger two list


print([10, 20, 30] + ['jessa', 'emma', 'kelly'])

Output:

300

JessRoy
[10, 20, 30, 'jessa', 'emma', 'kelly']

Overloading + operator for custom objects


Suppose we have two objects, and we want to add these two objects
with a binary + operator. However, it will throw an error if we perform
addition because the compiler doesn’t add two objects. See the
following example for more details.

Example:

class Book:
def __init__(self, pages):
self.pages = pages

# creating two objects


b1 = Book(400)
b2 = Book(300)

# add two objects


print(b1 + b2)

Output

TypeError: unsupported operand type(s) for +: 'Book' and 'Book'

We can overload + operator to work with custom objects also. Python


provides some special or magic function that is automatically invoked
when associated with that particular operator.

For example, when we use the + operator, the magic


method __add__() is automatically invoked. Internally + operator is
implemented by using __add__() method. We have to override this
method in our class if you want to add two custom objects.

Example:

class Book:
def __init__(self, pages):
self.pages = pages

# Overloading + operator with magic method


def __add__(self, other):
return self.pages + other.pages

b1 = Book(400)
b2 = Book(300)
print("Total number of pages: ", b1 + b2)

Output

Total number of pages: 700

Overloading the * Operator


The * operator is used to perform the multiplication. Let’s see how to
overload it to calculate the salary of an employee for a specific period.
Internally * operator is implemented by using the __mul__() method.

Example:

class Employee:
def __init__(self, name, salary):
self.name = name
self.salary = salary

def __mul__(self, timesheet):


print('Worked for', timesheet.days, 'days')
# calculate salary
return self.salary * timesheet.days

class TimeSheet:
def __init__(self, name, days):
self.name = name
self.days = days

emp = Employee("Jessa", 800)


timesheet = TimeSheet("Jessa", 50)
print("salary is: ", emp * timesheet)

Output
Wroked for 50 days

salary is: 40000

Magic Methods
In Python, there are different magic methods available to perform
overloading operations. The below table shows the magic methods
names to overload the mathematical operator, assignment operator,
and relational operators in Python.

Operator Name Symbol Magic method

Addition + __add__(self, other)

Subtraction - __sub__(self, other)

Multiplication * __mul__(self, other)

Division / __div__(self, other)

Floor Division // __floordiv__(self,other)

Modulus % __mod__(self, other)

Power ** __pow__(self, other)

Increment += __iadd__(self, other)

Decrement -= __isub__(self, other)

Product *= __imul__(self, other)

Division /+ __idiv__(self, other)

Modulus %= __imod__(self, other)

Power **= __ipow__(self, other)

Less than < __lt__(self, other)


Operator Name Symbol Magic method

Greater than > __gt__(self, other)

Less than or equal to <= __le__(self, other)

Greater than or equal to >= __ge__(self, other)

Equal to == __eq__(self, other)

Not equal != __ne__(self, other)

Python Custom Exceptions


In this tutorial, you will learn how to define custom exceptions depending upon
your requirements with the help of examples.

Python has numerous built-in exceptions that force your program to output an
error when something in the program goes wrong.
However, sometimes you may need to create your own custom exceptions
that serve your purpose.

Creating Custom Exceptions


In Python, users can define custom exceptions by creating a new class. This
exception class has to be derived, either directly or indirectly, from the built-
in Exception class. Most of the built-in exceptions are also derived from this
class.
>>> class CustomError(Exception):
... pass
...

>>> raise CustomError


Traceback (most recent call last):
...
__main__.CustomError

>>> raise CustomError("An error occurred")


Traceback (most recent call last):
...
__main__.CustomError: An error occurred

Here, we have created a user-defined exception called CustomError which


inherits from the Exception class. This new exception, like other exceptions,
can be raised using the raise statement with an optional error message.
When we are developing a large Python program, it is a good practice to
place all the user-defined exceptions that our program raises in a separate
file. Many standard modules do this. They define their exceptions separately
as exceptions.py or errors.py (generally but not always).
User-defined exception class can implement everything a normal class can
do, but we generally make them simple and concise. Most implementations
declare a custom base class and derive others exception classes from this
base class. This concept is made clearer in the following example.

Example: User-Defined Exception in Python


In this example, we will illustrate how user-defined exceptions can be used in
a program to raise and catch errors.
This program will ask the user to enter a number until they guess a stored
number correctly. To help them figure it out, a hint is provided whether their
guess is greater than or less than the stored number.

# define Python user-defined exceptions


class Error(Exception):
"""Base class for other exceptions"""
pass

class ValueTooSmallError(Error):
"""Raised when the input value is too small"""
pass

class ValueTooLargeError(Error):
"""Raised when the input value is too large"""
pass

# you need to guess this number


number = 10

# user guesses a number until he/she gets it right


while True:
try:
i_num = int(input("Enter a number: "))
if i_num < number:
raise ValueTooSmallError
elif i_num > number:
raise ValueTooLargeError
break
except ValueTooSmallError:
print("This value is too small, try again!")
print()
except ValueTooLargeError:
print("This value is too large, try again!")
print()

print("Congratulations! You guessed it correctly.")


Run Code
Here is a sample run of this program.

Enter a number: 12
This value is too large, try again!

Enter a number: 0
This value is too small, try again!

Enter a number: 8
This value is too small, try again!

Enter a number: 10
Congratulations! You guessed it correctly.

We have defined a base class called Error .

The other two exceptions ( ValueTooSmallError and ValueTooLargeError ) that


are actually raised by our program are derived from this class. This is the
standard way to define user-defined exceptions in Python programming, but
you are not limited to this way only.

Customizing Exception Classes


We can further customize this class to accept other arguments as per our
needs.

To learn about customizing the Exception classes, you need to have the basic
knowledge of Object-Oriented programming.

Visit Python Object Oriented Programming to start learning about Object-


Oriented programming in Python.
Let's look at one example:
class SalaryNotInRangeError(Exception):
"""Exception raised for errors in the input salary.

Attributes:
salary -- input salary which caused the error
message -- explanation of the error
"""

def __init__(self, salary, message="Salary is not in (5000, 15000)


range"):
self.salary = salary
self.message = message
super().__init__(self.message)

salary = int(input("Enter salary amount: "))


if not 5000 < salary < 15000:
raise SalaryNotInRangeError(salary)
Run Code

Output

Enter salary amount: 2000


Traceback (most recent call last):
File "<string>", line 17, in <module>
raise SalaryNotInRangeError(salary)
__main__.SalaryNotInRangeError: Salary is not in (5000, 15000) range

Here, we have overridden the constructor of the Exception class to accept our
own custom arguments salary and message . Then, the constructor of the
parent Exception class is called manually with the self.message argument
using super() .

The custom self.salary attribute is defined to be used later.


The inherited __str__ method of the Exception class is then used to display
the corresponding message when SalaryNotInRangeError is raised.
We can also customize the __str__ method itself by overriding it.
class SalaryNotInRangeError(Exception):
"""Exception raised for errors in the input salary.
Attributes:
salary -- input salary which caused the error
message -- explanation of the error
"""

def __init__(self, salary, message="Salary is not in (5000, 15000)


range"):
self.salary = salary
self.message = message
super().__init__(self.message)

def __str__(self):
return f'{self.salary} -> {self.message}'

salary = int(input("Enter salary amount: "))


if not 5000 < salary < 15000:
raise SalaryNotInRangeError(salary)
Run Code

Output

Enter salary amount: 2000


Traceback (most recent call last):
File "/home/bsoyuj/Desktop/Untitled-1.py", line 20, in <module>
raise SalaryNotInRangeError(salary)
__main__.SalaryNotInRangeError: 2000 -> Salary is not in (5000, 15000)
range

Iterators, generators and decorators

In this chapter we will learn about iterators, generators and decorators.

Iterators
Python iterator objects are required to support two methods while following the iterator
protocol.

__iter__ returns the iterator object itself. This is used in for and in statements.
__next__ method returns the next value from the iterator. If there is no more items to
return then it should raise StopIteration exception.

class Counter(object):
def __init__(self, low, high):
self.current = low
self.high = high

def __iter__(self):
'Returns itself as an iterator object'
return self

def __next__(self):
'Returns the next value till current is lower than high'
if self.current > self.high:
raise StopIteration
else:
self.current += 1
return self.current - 1

Now we can use this iterator in our code.

>>> c = Counter(5,10)
>>> for i in c:
... print(i, end=' ')
...
5 6 7 8 9 10

Remember that an iterator object can be used only once. It means after it
raises StopIteration once, it will keep raising the same exception.

>>> c = Counter(5,6)
>>> next(c)
5
>>> next(c)
6
>>> next(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in next
StopIteration
>>> next(c)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in next
StopIteration

Using the iterator in for loop example we saw, the following example tries to show the
code behind the scenes.

>>> iterator = iter(c)


>>> while True:
... try:
... x = iterator.__next__()
... print(x, end=' ')
... except StopIteration as e:
... break
...
5 6 7 8 9 10

Generators
In this section we learn about Python generators. They were introduced in Python 2.3. It
is an easier way to create iterators using a keyword yield from a function.

>>> def my_generator():


... print("Inside my generator")
... yield 'a'
... yield 'b'
... yield 'c'
...
>>> my_generator()
<generator object my_generator at 0x7fbcfa0a6aa0>

In the above example we create a simple generator using the yield statements. We can
use it in a for loop just like we use any other iterators.

>>> for char in my_generator():


... print(char)
...
Inside my generator
a
b
c

In the next example we will create the same Counter class using a generator function
and use it in a for loop.

def counter_generator(low, high):


while low <= high:
yield low
low += 1

>>> for i in counter_generator(5,10):


... print(i, end=' ')
...
5 6 7 8 9 10

Inside the while loop when it reaches to the yield statement, the value of low is returned
and the generator state is suspended. During the second next call the generator
resumed where it freeze-ed before and then the value of low is increased by one. It
continues with the while loop and comes to the yield statement again.

When you call an generator function it returns a *generator* object. If you call *dir* on
this object you will find that it contains __iter__ and *__next__* methods among the
other methods.

>>> c = counter_generator(5,10)
>>> dir(c)
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__iter__',
'__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__',
'__subclasshook__',
'close', 'gi_code', 'gi_frame', 'gi_running', 'send', 'throw']

We mostly use generators for laze evaluations. This way generators become a good
approach to work with lots of data. If you don’t want to load all the data in the memory,
you can use a generator which will pass you each piece of data at a time.

One of the biggest example of such example is os.path.walk() function which uses a
callback function and current os.walk generator. Using the generator implementation
saves memory.

We can have generators which produces infinite values. The following is a one such
example.

>>> def infinite_generator(start=0):


... while True:
... yield start
... start += 1
...
>>> for num in infinite_generator(4):
... print(num, end=' ')
... if num > 20:
... break
...
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

If we go back to the example of my_generator we will find one feature of generators.


They are not re-usable.

>>> g = my_generator()
>>> for c in g:
... print(c)
...
Inside my generator
a
b
c
>>> for c in g:
... print(c)
...

One way to create a reusable generator is Object based generators which do not hold
any state. Any class with a __iter__ method which yields data can be used as an object
generator. In the following example we will recreate our counter generator.

>>> class Counter(object):


... def __init__(self, low, high):
... self.low = low
... self.high = high
... def __iter__(self):
... counter = self.low
... while self.high >= counter:
... yield counter
... counter += 1
...
>>> gobj = Counter(5, 10)
>>> for num in gobj:
... print(num, end=' ')
...
5 6 7 8 9 10
>>> for num in gobj:
... print(num, end=' ')
...
5 6 7 8 9 10

Generator expressions
In this section we will learn about generator expressions which is a high performance,
memory efficient generalization of list comprehensions and generators.

For example we will try to sum the squares of all numbers from 1 to 9.

>>> sum([x*x for x in range(1,10)])

The example actually first creates a list of the square values in memory and then it
iterates over it and finally after sum it frees the memory. You can understand the
memory usage in case of a big list.

We can save memory usage by using a generator expression.

sum(x*x for x in range(1,10))

The syntax of generator expression says that always needs to be directly inside a set of
parentheses and cannot have a comma on either side. Which basically means both the
examples below are valid generator expression usage example.

>>> sum(x*x for x in range(1,10))


285
>>> g = (x*x for x in range(1,10))
>>> g
<generator object <genexpr> at 0x7fc559516b90>
We can have chaining of generators or generator expressions. In the following example
we will read the file */var/log/cron* and will find if any particular job (in the example we
are searching for anacron) is running successfully or not.

We can do the same using a shell command tail -f /var/log/cron |grep anacron

>>> jobtext = 'anacron'


>>> all_lines = (line for line in open('/var/log/cron', 'r') )
>>> job = ( line for line in all_lines if line.find(jobtext) != -1)
>>> text = next(job)
>>> text
"May 6 12:17:15 dhcp193-104 anacron[23052]: Job `cron.daily' terminated\n"
>>> text = next(job)
>>> text
'May 6 12:17:15 dhcp193-104 anacron[23052]: Normal exit (1 job run)\n'
>>> text = next(job)
>>> text
'May 6 13:01:01 dhcp193-104 run-parts(/etc/cron.hourly)[25907]: starting
0anacron\n'

You can write a for loop to the lines.

Closures
Closures are nothing but functions that are returned by another function. We use
closures to remove code duplication. In the following example we create a simple
closure for adding numbers.

>>> def add_number(num):


... def adder(number):
... 'adder is a closure'
... return num + number
... return adder
...
>>> a_10 = add_number(10)
>>> a_10(21)
31
>>> a_10(34)
44
>>> a_5 = add_number(5)
>>> a_5(3)
8

adder is a closure which adds a given number to a pre-defined one.

Decorators
Decorator is way to dynamically add some new behavior to some objects. We achieve
the same in Python by using closures.

In the example we will create a simple example which will print some statement before
and after the execution of a function.

>>> def my_decorator(func):


... def wrapper(*args, **kwargs):
... print("Before call")
... result = func(*args, **kwargs)
... print("After call")
... return result
... return wrapper
...
>>> @my_decorator
... def add(a, b):
... "Our add function"
... return a + b
...
>>> add(1, 3)
Before call
After call
4

You might also like