0% found this document useful (0 votes)
11 views125 pages

Understanding Object-Oriented Programming

Uploaded by

mecha.ai1915
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
11 views125 pages

Understanding Object-Oriented Programming

Uploaded by

mecha.ai1915
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Object Oriented Programming

14.1 Programming Paradigms


Programming paradigm is an approach to organize and structure your code; you can think
of it as a way or style of programming. Each paradigm prescribes some design principles
and features that define how a program is structured. The three common programming
paradigms are procedural programming paradigm, object-oriented programming paradigm
(OOP) and functional programming paradigm.
Programming languages are designed such that they provide features to support one or
more programming paradigms. Python is a multi-paradigm programming language which
means that it supports multiple programming paradigms. A Python programmer has the
flexibility to write the program in procedural, functional or object-oriented style. It is up to
the programmers to choose the suitable style according to their problem. They can also mix
all the approaches to accomplish a specific task, if the need arises.
In the procedural paradigm, there is a step-by-step procedure that is sequentially followed
for solving a specific problem. It is implemented through code blocks called functions. The
program is organised in a way such that the functions process the data of the program. In
object-oriented programming, real world entities or concepts are modelled using objects.
An object has both state and behaviour which means it contains both data and code to
manipulate that data. In procedural programming, you model your program in terms of
functions, while in object-oriented programming you model your program in terms of
objects. Functional programming paradigm is a style of programming that uses built-in
higher-order functions. A higher-order function is function that takes another function as
an argument or returns it as a result. Functional programming focuses more on ‘what to
solve’ rather than ‘how to solve’.
So far, we have been mostly using procedural approach in our programs. Now we will see
the object-oriented approach. In this chapter and the next two chapters, we will explore the
object-oriented features of Python. Python also supports functional programming with the
help of tools like comprehensions, function objects, lambdas, generators, decorators, map,
filter etc. Some of these functional features, have already been covered and we will explore
the rest of them later in the book.

14.2 Introduction to object-oriented programming


Before learning how to implement object-oriented programming in Python, let us see the
common terms used in object-oriented programming. Classes and objects are the two main
components of object-oriented programming.
We have been talking about objects right from the introductory chapter. We have seen and
used different types of objects like integer object, list object, string object, file object and
function object. Each object has a specific type and objects of each type have certain
characterises and behaviours which are all predefined. We do not have any control over the
structure or behaviour of these objects, these are objects of either built in types or come
from libraries. We just use these objects according to our requirement, so we are clients of
these built-in types. While writing large and complex programs we will realise that the
predefined types do not serve our purpose. For example, if you are creating a graphical
game, you would want to have objects representing circles, triangles, players etc; for a
mathematical project you might want to have objects representing vectors, matrices etc;
for a grocery store application you might want to have objects representing different
products, shopping carts and customers.
Object oriented programming allows us to create our own type of objects that would
behave the way we want them to behave. We can create our domain specific objects while
solving a problem. When we have to create custom objects that our program requires, we
have to define our own types. These new types are called user defined types and are
created by defining classes. A class is a blueprint or template for creating objects. A class
definition introduces a new type, and it describes the state and behaviour that the objects
of this new type will have. Each object that is created from a class will have the data and
behaviour specified in the class.
In object-oriented programming, we model our program in terms of objects. So, first we
identify the kind of objects that our system will have and then we write class definitions
that represent these types of objects. For example, if we are writing a program for a library
management system, we might want to have objects that represent different users and
books. Each user object will have a name, an ID, and each user can borrow a book or return
a book. Each book object contains title of the book, an ISBN number, author name, and a
book can be issued or deposited.
Figure 14.1: Objects

We generally need to model real-world things that have similar behaviour but differ in
their internal state, which means that their data is different. For example, all the user
objects can borrow a book or return a book, but each one has its own data. Similarly, all the
Book objects have similar behaviour but different data.
To create the objects that represent users, we can define a class called User and to create
objects that represent books, we can define a class called Book.

Figure 14.2: User class and Book class

Each class definition introduces a new type and it specifies the data and behaviour that
objects created from it will have. Classes encapsulate state and behaviour together - state
refers to the internal data stored in the object and behaviour refers to the actions that can
be performed by the object. These actions generally act on the state of the object in some
way. A class, on its own does not do anything, it is just a template for creating objects, the
real work is done by the concrete objects created from the class. You can think of a class as
a cookie cutter while the objects created from it are the cookies.
The objects created from a class are called instances or instance objects. Creating a new
instance of the class is called instantiation. We can use User and Book classes to
instantiate different user objects and book objects that we saw earlier.
This binding of data and code that acts on that data is called encapsulation. State is
maintained through variables which are also called data members and behaviour is
implemented through methods. Methods are like functions but they are defined within a
class. This concept of encapsulation helps isolate the members of a class. The members of a
class are separate from the members of another class and so we can have members with
same name in different classes. For example, we can have a data member named id in both
the Book class and the User class.
So, a class defines what data and methods should the object have, and the objects contain
the actual data. Instantiation means creating an object using a class as the blueprint. The
behaviour defined inside the class is shared by all the objects but data is not. Each object of
a specific type behaves in the same way but has its own data. This means that the methods
defined inside the class are shared by all the objects, so there is only one copy of each
method which is used by all the objects. Each instance object maintains its own copy of
data. So, you can think of class as a template that is used to create objects that behave in the
same way but have their own data.
Now let us look at some of the benefits of the object-oriented programming approach.
One of the advantages of object-oriented programming is code reusability. Classes that you
define can be used multiple times by different applications. You can inherit from these
classes to make new classes. This reduces development time and effort and hence lowers
the development cost. There are many libraries available that provide classes that can be
used by different client programs.
With object-oriented programming, it is easier to represent the real world in code. This
modelling of real-world entities and concepts as objects helps in overall understanding of
the program code. As your programs get longer, it becomes important to write code that is
easier to understand. Better understanding of the code helps in easier debugging,
modification and maintenance. In object-oriented programming, we identify the objects
that will help in solving our problem and each object is given some responsibility. This
structuring of program is more natural to work with and helps to break our program into
smaller manageable pieces. So, whenever there is need to fix a bug or add a new feature,
the programmer knows exactly where to go, he does not need to go through the entire
program. Different parts of the system can be developed and updated independently
without affecting the other parts. This also facilitates collaborative development where
different teams work on a single project. This is why object-oriented program is well suited
for programs that are large and complex and have to be regularly updated.
There is a sort of data security as the data is encapsulated inside the object and hence there
are less chances of it being misused by other parts of the program. In procedural
programming, your data passes through functions, but in object-oriented approach the data
is safely placed inside the object.
Encapsulation also leads to abstraction. While studying functions, we saw that they
provided abstraction which means hiding the internal details from the user. Object
oriented programming offers a higher level of abstraction. You can hide all the inner
working of the class from the user of the class. The user of the class needs to know about
only the interface (functionality) of the class, which specifies what the class does, not how it
works. For example, we have been using built in classes like int, list, dict and str
without knowing their internal implementation details. As users (clients) we just need to
be aware of the interface of the class. This information hiding also allows the creator of the
class to change the implementation without breaking the client code that uses the class.
Polymorphism, which means one thing many forms, can also be implemented in object-
oriented programming. Do not worry if some of the terms do not make sense now, things
will become clearer once we start coding.
In the next section we will see how to create classes and objects in Python. Before that, let
us clarify the terminology used in Python so that you do not get confused between objects
and classes.
We know that everything in Python is an object. Integers, strings, functions, and modules
are all objects in Python. When you define a function, a function object is created, similarly
when you define a class, a class object is created. The objects that are created by
instantiating the class are called instance objects or instances or sometimes simply objects.

14.3 Defining Classes and Creating Instance Objects


In this section, we will see the syntax for defining classes and creating instance objects. A
new class is created by writing the class statement:
class Person:

pass

The keyword class is written, followed by the class name and a colon. Conventionally, the
class names begin with a capital letter and are generally singular nouns. If there are
multiple words in the class name, then they are joined using the CapWords convention,
where the first letter of each word is capitalized.
The header line is followed by an indented block of statements that form the class body.
Right now, we do not want to add any data or code to our class, so we have written a pass
statement. This makes an empty class. When we execute this class definition, Python
creates a class object and assigns it to the name Person. This is somewhat similar to what
happens when a def statement is executed.
We can see the id and type of the class object that is created:
>>> id(Person)

2769602751456

>>> type(Person)

<class 'type'>

Like everything else, classes are also objects in Python; they are called class objects and
their type is type. Now, let us see how to create instance objects from this class. A class
object is callable, we can instantiate a class object by calling it like a function, i.e., by putting
a pair of parentheses around it. The call to class object returns an object which is called the
instance of the class.
>>> p1 = Person()

When this line is executed, an instance object is created whose type is Person. That
object will be assigned to name p1. So, the name p1 refers to an instance object whose type
is Person. Let us create one more instance object:
>>> p2 = Person()

When we execute this statement, another object of type Person will be created which will
be assigned to name p2. We can see types of p1 and p2 by using the built-in type function.
>>> type(p1)

<class '__main__.Person'>

>>> type(p2)

<class '__main__.Person'>

They are objects of type Person, let us see their ids:


>>> id(p1)

2769564298320

>>> id(p2)
2769601607888

We can see that these are 2 different objects in memory. Let us print these objects:
>>> p1

<__main__.Person object at 0x00000284D6E56C50>

>>> p2

<__main__.Person object at 0x00000284D91EB8D0>

The values shown here are in hexadecimal; in the


id
function, the same numbers were printed in decimal.

14.4 Adding methods to the class


We have seen how to define a class and how to instantiate it, but the class that we have
created is useless as it does not have any data or methods. Let us first add behaviour to our
class with the help of methods. For that, we will write two def statements inside the class:
class Person:

def display(self):

print('I am a person')

def greet(self):

print('Hello, how are you doing?')

These definitions of methods look like ordinary function definitions except that that there
is parameter named self. We will talk about this parameter in a short while. You can think
of methods as functions inside a class.
To call a method, we will write the instance name, followed by a dot and the method name.
p1 = Person()

p2 = Person()
[Link]()

[Link]()

[Link]()

[Link]()

Output-
I am a person

Hello, how are you doing?

I am a person

Hello, how are you doing

This is how we can execute the methods using instance objects. You must be wondering
how this code executed without any error because both the methods that we defined inside
the class have one parameter each, but while calling the methods, we did not send any
argument corresponding to the parameter named self. This worked because when a class
method qualified with an instance is called, Python automatically sends the argument for
the parameter self.
To see what value Python sends for this parameter, let us print the self parameter inside
these methods.
class Person:

def display(self):

print('I am a person', self)

def greet(self):

print('Hi, how are you doing ? ', self)

p1 = Person()

p2 = Person()
[Link]()

[Link]()

[Link]()

[Link]()

Output-
I am a person <__main__.Person object at 0x00000242BD0A7190>

Hi, how are you doing ? <__main__.Person object at


0x00000242BD0A7190>

I am a person <__main__.Person object at 0x00000242BD0A71D0>

Hi, how are you doing ? <__main__.Person object at


0x00000242BD0A71D0>

On executing this code, we get objects p1 and p2 printed in place of self. This means that
the instance object that called the method, is printed in the place of self. In the first two
calls, self refers to object p1, and in the last two calls, self refers to object p2. So, now
we know that Python provides the instance that calls the method as the argument for the
parameter self. Although you specify this parameter self in the method definition, you
do not have to provide a value for it while calling the method.
Generally, all methods inside a class should have this first parameter named self. There
are some exceptions that we will see later on. Python uses this parameter to identify the
instance object that calls the method. You can use any other name instead of self, but
self is a convention widely adopted within the programming community. It is a very
strong convention, so it is generally better to adhere to it.
So, in this section, we saw how to add methods to our class. The difference between
methods and functions is that methods are always defined inside a class, they are invoked
using the dot syntax and in a method definition the first parameter is generally always
self. Apart from this, whatever features we have seen in Chapter 10, like default values,
returning values, variable arguments, etc., hold true for methods, also.
In OOP terminology, sometimes methods are referred to as messages that can be sent to
objects. By calling a method, the user (client code) of the object sends a message to the
object for performing a task. For example, when we write [Link](), we were
sending a message to the list object to sort its data. Similarly, the code that uses an object of
our Person class can send messages to the object by calling the methods display or
greet.

14.5 Adding instance variables


We have added behaviour to our Person class in the form of the two methods
display() and greet(). These methods are shared by all the instance objects. Now, we
will add data to our instance objects in the form of instance variables. Each instance object
will maintain its own data which means that instance variables are not shared, each
instance object will have its own copy of instance variables.
An instance variable is created like you create any other variable in Python, by assigning a
value to it. But since an instance variable is associated with an instance object, you have to
use the dot syntax.
>>> [Link] = 'Tom'

This statement attaches the instance variable


name
to the instance object
p1
.

>>> [Link]

'Tom'

This is called an instance variable because it is attached to an instance. The instance


variable
name
has been attached only to
p1
, not
p2
.

>>> [Link]

AttributeError: 'Person' object has no attribute 'name'

We would generally want all the instance objects of a class to have the same variables. So,
we will not attach the instance variables dynamically like this outside the class; we will
attach them inside the methods. That way, all instance objects created from the same class
will have the same set of instance variables.
We know that inside any method, we can access the instance object by writing self. So,
inside a method if a variable name is prefixed with self, then that variable will be an
instance variable. We will create a new method set_details(), and inside this method,
we will create two instance variables name and age.
class Person:

def set_details(self):

[Link] = 'John'

[Link] = 20

def display(self):

print('I am a person', self)

def greet(self):

print('Hi, how are you doing ? ', self)

p1 = Person()

p1.set_details()

p2 = Person()

p2.set_details()

After creating the instance objects, we called the set_details method for each one. After
executing this program, the objects referred to by p1 and p2 will have two instance
variables each. Let us see their values:
>>> [Link]

'John'

>>> [Link]
20

>>> [Link]

'John'

>>> [Link]

20

So, whenever an instance object of Person class will call the method set_details, it
will get these two instance variables attached to it.
Instance variables are specific to an instance of the class, every instance has its own of copy
of instance variables.
Changing the value for one instance does not affect the value in another instance. Let us
change [Link] to 'Jack' and [Link] to 30.
>>> [Link] = 'Jack'

>>> [Link] = 30

>>> [Link]

'Jack'

>>> [Link]

30

Let us check the instance variables of object p1.


>>> [Link]

'John'

>>> [Link]

20

The instance variables of p1 were not changed. So, each instance object has its own copy of
instance variables and these variables define the state of that instance object.
The method set_details always sets the name to 'John' and age to 20. We would
generally want to assign different values to different instance objects. So, to make this
method more flexible, we will add two parameters in the definition, name and age.
def set_details(self, name, age):

[Link] = name

[Link] = age

We have assigned name to [Link] and age to [Link]. Do not get confused in the
two sets of names. [Link] and [Link] are instance variables while name and age
are parameters of this method, so they are just local variables inside the method. You can
use name and age only inside this method, but you can use the instance variables in any
method inside the class. This is because instance variables are attached to the instance
object, and they will live as long as the object lives. They will not be destroyed when the
method terminates, as is the case with local variables.
The dot notation makes sure that there is no conflict between the two sets of names. You
can use any other name for the parameters, but it is a convention to use the same names as
instance variables. Now, when we call set_details, we will send two arguments.
p1 = Person()

p1.set_details('Bob', 20)

p2 = Person()

p2.set_details('Ted', 90)

Now, we are able to give different values for name and age of different instance objects.
After executing our modified program, we will see that [Link] and [Link] are
different, and similarly, the age instance variable also has different values for objects p1
and p2.
>>> [Link]

'Bob'

>>> [Link]

'Ted'
>>> [Link]

20

>>> [Link]

90

So, now each instance object can start with a different state.
After these instance variables have been created, they are available inside the methods of
the class (because of self) and so any method of the class can use them. Let us use the two
instance variables in the methods display and greet.
class Person:

def set_details(self, name, age):

[Link] = name

[Link] = age

def display(self):

print('I am', [Link])

def greet(self):

if [Link] < 80:

print('Hi, how are you doing?')

else:

print('Hello, how do you do?')

p1 = Person()

p1.set_details('Bob', 20)
[Link]()

[Link]()

p2 = Person()

p2.set_details('Ted', 90)

[Link]()

[Link]()

Output-
I am Bob

Hi, how are you doing?

I am Ted

Hello, how do you do?

In the method display, we have used the instance variable name, and in the method
greet, we have used the instance variable age. The instance variables name and age are
created in set_details() method and referenced in the methods display and greet.
When you reference an instance variable outside a class, it has to be prefixed with the
instance name and a dot (for example, [Link] or [Link]). Inside the methods, self
refers to the current instance object (the object that called the method), so the instance
variable name is prefixed with self and a dot. The self parameter helps you access or
change the instance variables from within the methods, and this is why self is the first
parameter in all the methods.
If you have worked in Java or C++, you must have noticed the difference in how the
instance variables are defined. In these languages, these instance variables, which are also
called data members, are statically declared; they are a formal part of the class definition.
They are defined inside the class, outside of any method. In Python, instance variables are
defined inside methods and it is possible to even dynamically attach instance variables. The
variables that we create outside the methods at the class level are class variables that we
will see shortly.

14.6 Calling a method inside another method


Suppose we want to call the method display inside the method greet. We have seen
that, when an instance object calls the display method outside the class, it is called like
[Link]() or [Link](). Inside a method, the current instance is accessed by
using self, so here we will call it as [Link]().

def greet(self):

if [Link] < 80:

print('Hi, how are you doing?')

else:

print('Hello, how do you do?')

[Link]()

We have called the method display with self. From outside the class, we will call the
method greet like [Link]() or [Link]() and inside the method greet, the
method display will be called.
p1 = Person()

p1.set_details('Bob', 20)

[Link]()

p2 = Person()

p2.set_details('Ted', 90)

[Link]()

Output-
Hi, how are you doing?

I am Bob

Hello, how do you do?


I am Ted

So, outside the class, the instance variables and methods will be accessed by preceding
them with instance object name. Inside the class methods, they will be accessed by
preceding them with the name self.
We have seen how to define classes and how to create instance objects. There was a lot of
new syntax involved, so let us summarise in a few points, whatever we have studied till
now:
• We define a new class by using the class statement. When a class statement
executes, it creates a new class object and binds it to the class name.
• Instantiation of the class creates a new instance object. To instantiate the class,
we have to call the class object with a pair of parentheses.
• The instance object is like any other object of Python. It can be used as an element
of a list, tuple, dictionary, or set. It can be passed to a function as an argument or
can be returned from a function. It is a first-class object in Python.
• Even class objects are first-class objects in Python. They can also be passed as
arguments or returned from a function, bound to variables, used as an element in
a container, or even an attribute of an object.
• Methods are defined inside the class using the def statement, and they follow all
the rules that we have studied in functions.
• Inside the method definition, the first parameter should be self. You do not have
to provide any argument for self while calling the method. Python will
automatically assign the instance object that calls the method to this parameter
self. This parameter is always required so that we can access the instance
variables and methods of an instance object from within the class.
• Instance variables can be created inside any method by assigning to a variable
name prefixed with self.
[Link] = value

• To reference an instance variable inside any method, you must prefix the variable
name with self.
print([Link])

• To call a method inside another method, you must prefix the method name with
self.
[Link]()
• Outside the class, we must use an instance object name before methods and
instance variables. Inside the class, we must use self in front of the methods
and instance variables.

14.7 Common pitfalls


Many programmers who are used to other languages like Java or C++, generally forget to
include self as the first parameter in the methods. If you forget to do this, the interpreter
will complain. Let us remove the self parameter from the set_details method of our
class Person.
class Person:

def set_details(name, age):

[Link] = name

[Link] = age

def display(self):

print('I am', [Link])

def greet(self):

if [Link] < 80:

print('Hi, how are you doing?')

else:

print('Hello, how do you do?')

[Link]()

p1 = Person()

p1.set_details('Bob', 20)
[Link]()

Output-
File "E:\Programs\14_ObjectOriented\P14_8.py", line 18, in
<module>

p1.set_details('Bob', 20)

TypeError: Person.set_details() takes 2 positional arguments but


3 were given

When we execute the program with


self
parameter removed from the definition
set_details
method, Python shows an error.

We have sent two arguments in the call p1.set_details('Bob',20) but the error
message is saying that 3 were given. This shows that Python automatically sends an
argument, and so we always need to specify the first parameter as self, and after that we
can have our regular parameter list.
Another mistake that beginners in Python make, is forgetting to add self as a prefix for
the instance variables and methods. Let us remove the self from the call to display that
we made in the greet method.
def greet(self):

if [Link] < 80:

print(
'
Hi, how are you doing?')

else:

print('Hello, how do you do?')

display()

Now, on executing the program we will get the following output:


Hi, how are you doing?

Traceback (most recent call last):

File "E:\Programs\14_ObjectOriented\P14_9.py", line 19, in


<module>

[Link]()

File "E:\Programs\14_ObjectOriented\P14_9.py", line 14, in


greet

display()

NameError: name 'display' is not defined

display
is a method of the class so it should be called with an instance of the class. We know that
self
refers to the current instance inside the class, so you need to call it as
[Link]()
inside the class.

So, if we forget to use self before the method name, we get NameError. Similarly, if you
forget to use self before an instance variable inside a method, then also you will get an
error. For example, suppose we forget to write self in front of the age instance variable:
def greet(self):

if age < 80:

print('Hi, how are you doing?')

else:

print('Hello, how do you do?')

[Link]()

We will get the following error on executing the program:


Traceback (most recent call last):

File "E:\Programs\14_ObjectOriented\P14_10.py", line 19, in


<module>

[Link]()

File "E:\Programs\14_ObjectOriented\P14_10.py", line 10, in


greet

if age < 80:

NameError: name 'age' is not defined

Now, suppose we define another method get_old inside our Person class.
def get_old(self):

age = 75

Inside this method, we want to change instance variable age to 75, but we forget to put
self before the instance variable age. Let us see what happens when we call this method
for object p1:
>>> p1.get_old()

When we execute this, we expect a NameError but the statement will execute without any
error. After execution of the program if we check age instance variable of p1, it is still 20. It
was not changed to 75.
>>> [Link]

20

Let us see what happened here. An assignment was made to name age and we know that
in Python, a variable is created when it is first assigned, so here the interpreter created a
local variable named age with value 75. This is why we did not get any error.
If we add self before age, then the instance variable age will be changed.
def get_old(self):

[Link] = 75
After executing the modified program, if we call get_old() for p1 and then check the
instance variable age of p1, then we can see the changes.
>>> p1.get_old()

>>> [Link]

75

So, you need to remember to write self whenever you have to use an instance variable or
a method inside the class. Qualifying every instance variable and method with self
involves more typing as compared to some other languages but it makes things clear. It
helps you distinguish between a local variable and an instance variable and between a
method call and a function call. There is no ambiguity; by looking at the code you can tell
whether you are referring to an instance variable or a local variable and whether you are
calling a function or a method.

14.8 Initializer
We have seen the following class in the previous sections:
class Person:

def set_details(self, name, age):

[Link] = name

[Link] = age

def display(self):

print('I am', [Link])

def greet(self):

if [Link] < 80:

print('Hi, how are you doing?')

else:
print('Hello, how do you do?')

p1 = Person()

p1.set_details('Bob', 20)

[Link]()

[Link]()

p2 = Person()

p2.set_details('Ted', 90)

[Link]()

[Link]()

Whenever we create a new instance object for this class, immediately we have to call the
method set_details, because when this method will be called, then only the instance
object will have its instance variables created. Now suppose that we forget to call
set_details for the instance object p2, and we call the methods display and greet
for it.
p1 = Person()

p1.set_details('Bob', 20)

[Link]()

[Link]()

p2 = Person()

[Link]()

[Link]()

Output-
I am Bob

Hi, how are you doing?

Traceback (most recent call last):

File "E:\Programs\14_ObjectOriented\P14_13.py", line 23, in


<module>

[Link]()

File "E:\Programs\14_ObjectOriented\P14_13.py", line 7, in


display

print('I am', [Link])

AttributeError: 'Person' object has no attribute 'name'

For object p1, we have called the method set_details so its instance variables will be
created, but for object p2 we forgot to call this method so its instance variables will not be
created. We have called the methods display() and greet() on object p2. The method
display() wants to access instance variable name of p2, and the method greet()
wants to access the instance variable age but these instance variables were not created for
object p2, so we get AttributeError.
Therefore, if you forget to call the set_details method and call any of the two methods,
greet or display, you will get an error. You must always remember to call the
set_details method immediately after creating any Person object so that the instance
variables are created for that particular object and can be used in other methods. Calling
this method every time we create an object is cumbersome.
Python has a solution for this. It lets you automate this object initialization task. You can
define a method named __init__ in your class. This method will automatically be called
right after the instance has been created. So, if you have any code you think should be
executed just after the object creation, put that code inside this method.
For our Person class, we want the code inside the set_details method to be executed
after we have created the instance object so, let us change the name of the set_details
method to __init__.
def __init__(self, name, age):

[Link] = name
[Link] = age

This name __init__ is not a convention like naming of self, this is a special name and
you cannot choose any other name for this method. The two leading and trailing
underscores are also important. Because of these underscores, this method is generally
called dunder init, where dunder is shortform of double underscore.
Now we can delete the calls to set_details() from the program. We do not have to call
this method __init__ explicitly, the interpreter will call it implicitly. Now you must be
thinking, when we do no have to call this method, how will we send the arguments for the
parameters name and age. These arguments will be sent when the object is instantiated.

p1 = Person('Bob', 20)

[Link]()

[Link]()

p2 = Person('Ted', 90)

[Link]()

[Link]()

Output-
I am Bob

Hi, how are you doing?

I am Ted

Hello, how do you do?

When you create instance objects, any arguments that you pass to the class are passed to
the __init__ method.
So, we have seen that the initialization work is automatically done by the interpreter if you
define the __init__ method. You can create and initialize all your instance variables in
this method. Although instance variables can be created in any other method also, it is
more readable and clearer if you create all instance variables in the __init__ method.
Also, there is no risk of the instance variables being accessed before they are defined.
You can also perform any other startup task that you want, in this initializer method. For
example, opening a file or setting up a network connection, or connecting to a database.
Like other methods, the first parameter to __init__ is always self. After self, other
parameters that are coded in __init__ are generally used to give initial values to
instance variables. These parameters can be given default values if required. The instance
variables can also be initialized with values that are independent of parameters.
These methods, which have special names and double underscores before and after their
names, are called magic methods in Python. The magic is that they are not called directly;
they are called automatically in certain contexts. We will learn more about these dunder
methods in a separate chapter.
If you have worked in other object-oriented languages, you must be thinking about
constructors right from the start of the section. The __init__ method definitely looks like
a Java or C++ constructor, but it would be technically incorrect to call it a constructor of the
class because by the time this method is called, the object is already constructed. To
construct an instance, the magic method __new__ is invoked. This method is responsible
for creating the object by allocating memory for it. So technically, this is the actual
constructor. As a beginner, you won’t need to use this method much; it is used while coding
metaclasses, which is an advanced topic. The default __new__ is automatically invoked if
we do not provide our version, and in most cases, the default version serves the purpose.
The __init__ (dunder init) is the initializer method. It is called immediately after the
instance is created. It is the first method that is called on the newly created instance object.
The self parameter passed to this method refers to the newly created object. So, the
method __init__ does not construct the instance object. It initializes an already
constructed instance object. In languages like C++ and Java, construction and initialization
are a one-step process, but in Python, these two steps are separated.
You can have only one initializer method in a class, as there is no concept of function
overloading in Python. However, it is possible to create instance objects with different
types of data using class methods, which we will see later in this chapter. Also, you can give
default values to parameters to create the illusion of having multiple initializers.

14.9 Data Hiding


Some languages implement data hiding in a class by declaring the data and methods as
private or public. Private data and methods can be used only by the methods inside the
class while public data and methods can be accessed from outside the class as well. The
part of the class that is designated as private can be used inside the class only. In Python,
there is no such concept of private or public; it does not enforce any sort of privacy. The
access specifiers, public and private, are not available in Python.
First, let us see why there is a need for this distinction between private and public when
working with a class. The users of a class are generally called clients and the client code
uses the class by instantiating it, i.e., by creating its objects. Suppose we have three clients
Client1, Client2, Client3 that use a class named Product in their applications.

Figure 14.3: Data hiding

These clients can access the variable data3 and can call the two methods methodX and
methodY, through the instances of the class, but they have no access to variables data1,
data2 and methods methodA and methodB. The creator of the class has chosen to hide
them from the user because these things are used in internal working of the class, they are
not required by the user. For example, in a car you have access to steering, accelerator and
brakes, you do not need access to all the internal parts of the car that make your car move
or stop. Those internal details are best left to the creator of the car. If you get access, you
might inadvertently damage something and the car will stop working. Moreover, nobody
would want to drive a car with all the inner circuitry exposed. It would be very difficult to
use such a car.
Similarly, in a class, only those parts are exposed to the user which are required, other
internal details are hidden in the form of private variables and private methods. This
avoids any confusion and also protects sensitive data that can be inadvertently or
maliciously modified by the user. Your objects can be modified in a way that they do not
work properly or go into an invalid state. This is why the internal details are not revealed
to the user. The part that is visible to the user is the interface of the class and the part that
is hidden is the implementation. Interface of a class allows the programmer to use the class
without understanding its internal details.
The interface of a class is well-defined and generally comes with a guarantee that it will not
change with time, but the implementation may be changed without any notice. So, a private
method or variable may be deleted, or its behavior can be changed without notice. These
changes may be done to fix some bugs, change some functionality, or maybe to improve
efficiency. Any change in implementation does not affect the client code because the client
is not using the implementation part; it is not concerned about how the class is doing its
work; it just gets its work done by calling the public methods.
If we again take the car analogy, the internal parts of the car may be changed or can be
made to work in some other way but your steering and brakes will work in the same way.
So, the interface is generally not changed.
Now, let us come to Python. In Python, everything inside the class is public, clients can
access any data or method written inside the class. So, if the clients use the Product class
written in Python, they are free to call any of the 4 methods and they can access any of the
3 data variables. Python does not enforce any access restrictions on data and methods like
Java or C++ do. However, there is a naming convention that is used to indicate that a certain
attribute is meant to be used inside the class only and it should not be used directly by the
client. The word attribute in Python is used for any name following a dot. So, instance
variables and methods are collectively called attributes.
The convention is that you can use a leading underscore on a variable or a method name to
suggest that it is private and should not be used outside the class. For example, the names
_phone, _age, _change(), _increase() indicate that these instance variables and
methods are non-public. Variables or methods with a leading underscore should be
accessed and modified only inside the methods of the class. They are not meant to be
accessed from outside the class. This protects the internal data of the class from intentional
or accidental modification.

Figure 14.4:Leading underscore indicates privacy


These variables and methods with a leading underscore mean nothing special to the
interpreter, they are technically just like any other variable or method, it is possible to
access them outside the class also. The leading underscore is there to indicate privacy. This
way you can discourage clients from using the private things of a class. However, you
cannot stop them from doing so. If you remember, we had seen a similar data hiding
convention in modules chapter.
Python works on the policy that we are all consenting and responsible adults and know
how to use the code. Its philosophy is based on the trust, that users of the class will respect
the convention and documentation and use the methods and variables appropriately.
One of the reasons for making everything accessible outside the class is debugging; when
you need to fix a bug, you have to sometimes access the private attributes of the class.
So, if you prefix a variable or a method with a single underscore, then it indicates that this
name is non-public, it is only for the internal use of the class and should not be accessed
outside it.
If you prefix a name with double underscores, Python will do some name mangling and that
attribute will not be directly visible from outside the class. For example, __value is
internally replaced with _MyClass__value, where MyClass is the name of the class in
which this attribute __value is defined. These names are mangled by prefixing with a
single underscore and the class name. If you want to use the name __value, you will have
to write _MyClass__value. These names are not directly accessible from outside the
class, but they can be indirectly accessed by using the mangled name.
So, if you use a name that starts with at least two leading underscores and has at most one
trailing underscore, that name is mangled by Python, and it cannot be directly used by the
user.
This naming can be used for your non-public members of the class to make it difficult for
the user to access those members. But this name mangling mechanism is not there in the
language for this purpose, its purpose is to make the name specific to the class so that there
is no name clash with subclasses (inherited classes). This type of naming should be only
used to avoid name clashes with attributes in subclasses. To indicate privacy, you should
use single leading underscore. Names with double leading underscores are used to reduce
the risk of duplicating the name in subclasses.
There are names that start and end with two underscores, we have seen one such name
__init__(dunder init), and we will see many more. These types of names are used by
Python for its internal use and we should not write our own names that have double
leading and double trailing underscores.
A single trailing underscore is used to avoid name clashes with Python keywords and built
in names. For example, if you want to use the name class or range in your program, you
can use it as class_ or range_. It is best not to use these names in your programs but if
you ever need to do so, the convention is to use a trailing underscore.
Now let us see an example program. We have a class named Product in which we have 2
instance variables and 2 methods out of which one variable and one method are prefixed
with an underscore which indicates that they are not supposed to be used outside the class.
class Product:

def __init__(self):

self.data1 = 10

self._data2 = 20

def method1(self):

print('Executing method1')

def _method2(self):

print('Executing method2')

p = Product()

print(p.data1, p._data2)

p.method1()

p._method2()

Output-
10 20

Executing method1

Executing method2

When we execute this program, we do not get any error which means that we can access
both variables and call both methods from outside the class. Although the names _data2
and _method2 are prefixed with an underscore, it is possible to access them outside the
class. For the interpreter, this leading underscore does not make any difference, it is just a
convention. Programmers should respect this convention and not access these attributes
like this outside the class, unless there is some need for debugging or something similar.
In the above program if we change the single underscores to double underscores, and then
execute the program we will get AttributeError.
class Product:

def __init__(self):

self.data1 = 10

self.__data2 = 20

def method1(self):

print('Executing method1')

def __method2(self):

print('Executing method2')

p = Product()

print(p.data1, p.__data2)

p.method1()

p.__method2()

Output-
AttributeError: 'Product' object has no attribute '__data2'

If you execute the dir function for the instance object p, you will be able to see the
mangled names. They have been prefixed with the class name and an underscore.
>>>dir(p)

['_Product__data2', '_Product__method2', '__class__', ……… ……… ,


'data1', 'method1']

If we want to access them, we can do so with these mangled names.


p = Product()
print(p.data1, p._Product__data2)

p.method1()

p._Product__method2()

Output-
10 20

Executing method1

Executing method2

We cannot directly access these attributes from outside the class, but we can access them
indirectly. Inside the class methods, these variables can be accessed directly. As mentioned
before, this naming should be used to avoid name clashes with attributes in subclasses. For
making attributes non-public, we should use a single underscore.
Here is an example program that illustrates data hiding:
----------- [Link] ----------
class Student:

def __init__(self, name, phone, marks):

[Link] = name

[Link] = phone

self._marks = marks

def _calculate_total(self):

return sum(self._marks)

def _calculate_percentage(self):

return self._calculate_total() / 4

def display(self):
print([Link], [Link])

def show_result(self):

[Link]()

percentage = self._calculate_percentage()

print(f'Percentage : {percentage : .1f}')

print('Pass' if percentage > 40 else 'Fail')

In this class Student, the instance variable _marks and the methods
_calculate_total and _calculate_percentage are not supposed to be used
outside the class. The implementation of these three can be changed or they can even be
deleted so the clients should not use them in their code. The instance variables name and
phone and the methods display and show_result can be used by the client.
Generally, classes are written in separate modules and that module is imported in the
application program. We have placed our Student class in the [Link] file and this
module will be imported by different applications or clients. We have two client programs
that import this Student class.
---------[Link]----------
from student import Student

s = Student('Dev', 986754361, [50, 85, 70, 90])

s.show_result()

---------[Link]----------
from student import Student

s = Student('Raj', 987654535, [73, 89, 78, 88])

[Link]()

if s._calculate_total() > 160:

print('Pass')
else:

print('Fail')

The code in [Link] instantiates the Student class and then calls the public method
show_result. The code in [Link] also instantiates the class and it calls the methods
display and _calculate_total. The method _calculate_total was supposed to
be private; it shouldn’t have been used by the client but the program will work because
Python does not enforce any data hiding.
Now, let us consider a scenario where changes are made in the implementation of the
Student class after some time. The results are now calculated based on the ‘best of 3’
approach. _calculate_total will now calculate the total of best 3 subjects and
_calculate_percentage will calculate the percentage of these three 3 subjects.
------- [Link] -------
class Student:

def __init__(self, name, phone, marks):

[Link] = name

[Link] = phone

self._marks = marks

def _calculate_total(self):

total_best3 = sum(sorted(self._marks)[1:])

return total_best3

def _calculate_percentage(self):

return self._calculate_total() / 3

def display(self):

print([Link], [Link])
def show_result(self):

[Link]()

percentage = self._calculate_percentage()

print(f'Percentage : {percentage : .1f}')

print('Pass' if percentage > 40 else 'Fail')

For client1 there will be no problem, he does not need to change his code because he never
used any of the private things of the class. The code of client2 will still work but now it has
a logical error in it. The _calculate_total is now returning the total in 3 subjects and
so if s._calculate_total() > 160: does not make sense now and has to be
changed. The number 160 should be changed to 120.
Now, suppose after some time, grading system is introduced and students are assigned
CGPA instead of percentage. So, in the Student class, the methods _calculate_total
and _calculate_percentage are deleted and a new method _calculate_cgpa is
introduced.
------- [Link] -------
class Student:

def __init__(self, name, phone, marks):

[Link] = name

[Link] = phone

self._marks = marks

def _calculate_cgpa(self):

credit_hours = [3, 3, 4, 2]

total_grade_points = 0

for i, score in enumerate(self._marks):

if score > 90:


grade_points = 10

elif score > 70:

grade_points = 8

elif score > 50:

grade_points = 6

elif score > 30:

grade_points = 4

else:

grade_points = 0

total_grade_points += grade_points * credit_hours[i]

cgpa = total_grade_points / sum(credit_hours)

return cgpa

def display(self):

print([Link], [Link])

def show_result(self):

[Link]()

cgpa = self._calculate_cgpa()

print(f'cgpa : {cgpa : .1f}')

print('Pass' if cgpa > 4 else 'Fail')


Again, client1 has no problem, but the code of client2 now will give an error as now there is
no method named _calculate_total.
Therefore, it is advisable to avoid using the private attributes of a class. If users choose to
use them, they do so at their own risk, the given example illustrates this concern. In real life
code, the code of the class and the client code will not be so small, and so the issues
resulting from using private attributes can be extensive and challenging to identify and
rectify.

14.10 Class Variables


While studying the object-oriented concepts we had seen that the behaviour of all instance
object is same while their data is generally different. This is why methods are stored in the
class object and shared by all instances while the instance variables are stored in different
instance objects.
While modelling our objects, we might find that there is some data that does not vary for
each instance, it is the same for every instance created from a particular class. Storing this
piece of data in every instance object would be an unnecessary waste of memory, it would
be good if we could have just one copy of that data and let each instance object access it. We
can do this by defining variables at the class level; these variables are called class variables
or class attributes. Let us define a class variable for the Person class that we had written
earlier.
class Person:

species = 'Homo sapien'

def __init__(self, name, age):

[Link] = name

[Link] = age

def display(self):

print(f'{[Link]} is {[Link]} years old')

p1 = Person('John', 20)

p2 = Person('Jack', 34)

[Link]()
[Link]()

The variable named species is defined inside the class but outside any method, so it is a
class variable. Class variables are generally placed at the top of the class definition, just
below the class header. There is only a single copy of a class variable and it is shared by all
the instances of the class. It belongs to the class, not to individual instance objects. The data
of a class variable is stored in the class object itself, while the data of instance variables is
stored in individual instance objects.
The value of species will be the same for all Person objects, there is no need to have a
unique copy for each instance, so we have defined it as a class variable. Class variables are
created in the class definition while the instance variables are created inside the methods,
usually inside __init__().
A class variable can be accessed using the class name or the instance name.
>>> [Link]

'Homo sapiens'

>>> [Link]

'Homo sapiens'

>>> [Link]

'Homo sapiens'

We can see that the class variable is the same whether you access it with a class name or an
instance name. Let us use the id function to verify that all these three references refer to
the same variable stored in the class object.
>>> id([Link])

2605159076592

>>> id([Link])

2605159076592

>>> id([Link])

2605159076592
We cannot access an instance variable like this with the class name, for example we cannot
write [Link].
>>> [Link]

AttributeError: type object 'Person' has no attribute 'name'

This is because each Person instance object has a different name, there is no name
attribute attached with the Person class itself, but you can write [Link] since
species is the same for all Person instance objects.
In fact, the class variables can be accessed even before any instance object is created. Inside
the class methods, you can access a class variable by preceding it with class name or self.
Let us use this inside the display method.
def display(self):

print(f'{[Link]} is {[Link]} years old


{[Link]}')

We could have written [Link] but using the class name clearly shows that it is a
class variable.
So, if there is any value that needs to be shared by all instances of a class, then there is no
need to waste memory by storing it in each instance object, we can make it a class variable
and only one copy will be stored in the class object and all the instance objects can use the
same copy. Class variables are created for storing data that does not vary for each instance
while instance variables are created for data that can be different for each instance.
Let us see one more example. We have the following class named BankAccount that has
instance variables for the representing the account number, owner name and balance.
class BankAccount:

rate = 5

min_balance = 1000

min_balance_fees = 10

def __init__(self, account_number, owner_name, balance):

self.account_number = account_number
self.owner_name = owner_name

[Link] = balance

def withdraw(self, amount):

[Link] -= amount

def deposit(self, amount):

[Link] += amount

account1 = BankAccount('7348', 'Tom', 50)

account2 = BankAccount('6378', 'Bob', 400)

The rate of interest would be the same for each instance of the account, so you can make it
a class variable.
A bank can charge some fees if the balance becomes less than a minimum amount. So, you
can make class variables for minimum balance and for minimum balance fees. The values of
these variables can change but they will not vary for different accounts, which means that
they will be the same for all the instances, so we have defined them at the class level. You
can use these class variables in different methods that you define for this class. For
example, you can check for minimum balance after a withdrawal in the withdraw method.
Class variables are often used to store class specific constants. For example, when you are
creating a bounded data structure, you need a to specify a maximum limit for the size of the
structure. It is better if we use a named constant instead of embedding a literal value in our
code.
class Stack:

MAX_LIMIT = 10

def __init__(self):

[Link] = []

def push(self, item):


if len([Link]) >= Stack.MAX_LIMIT:

raise Exception('Stack is full')

[Link](item)

def pop(self):

if [Link] == []:

raise RuntimeError('Stack is empty')

return [Link]()

def display(self):

print([Link])

In this class, MAX_LIMIT is a class level constant, it is in all upper case as that is the
convention for naming constants in Python.
We can use a class variable to count the number of instance objects created from a
particular class. Let us add one more class variable in our Person class, this variable will
store the number of Person instance objects created.
class Person:

species = 'Homo sapien'

count = 0

def __init__(self, name, age):

[Link] = name

[Link] = age

[Link] += 1

def display(self):
print(f'{[Link]} is {[Link]} years old
{[Link]}')

When the class definition executes, count variable is created and stored in the class object
and it is initialized to zero. In the initializer, we have incremented count by 1. So,
whenever a new instance object will be created the value of this variable count will be
incremented.
>>> p1 = Person('Devanshi', 18)

>>> p2 = Person('Devank', 10)

>>> [Link]

The value of class variable count is 2, since we have created two instance objects.
Class variables can be used to track data across all instances of a class. In the following
program, we have a list as the class attribute and every time we create a new instance
object, we add the account owner’s name to the list.
class BankAccount:

account_holders = []

def __init__(self, account_number, owner_name, balance):

self.account_number = account_number

self.owner_name = owner_name

[Link] = balance

BankAccount.account_holders.append(self.owner_name)

account1 = BankAccount('7348', 'Tom', 50)

account2 = BankAccount('6378', 'Bob', 400)

account3 = BankAccount('8348', 'Ron', 500)


print(BankAccount.account_holders)

Output-
['Tom', 'Bob', 'Ron']

14.11 Class and object namespaces


We have learnt about namespaces; they are mapping from names to objects. In Python,
classes and instance objects have their own distinct namespaces, generally implemented
through dictionaries.
There is a namespace created for each class that is defined. When a class definition is
executed, a new namespace is created for it. Anything defined at the top level of the class
lives in this namespace, so all class variables and methods are part of this namespace.
Basically, this namespace manages all names that are to be shared by all the instances of
the class. When an instance object is created it gets its own namespace. Instance variables
are part of this namespace. An instance gets access to all the names defined in the class
namespace and the names defined in its own instance namespace.
These namespaces are represented by the __dict__ attribute of the class or the instance.
After executing the previous program, we can see the __dict__ attribute of the class and
the instance objects.
>>> account1.__dict__

{'account_number': '7348', 'owner_name': 'Tom', 'balance': 50}

>>> account2.__dict__

{'account_number': '6378', 'owner_name': 'Bob', 'balance': 400}

>>> BankAccount.__dict__

mappingproxy({'__module__': '__main__', 'account_holders':


['Tom', 'Bob', 'Ron'], '__init__': <function
BankAccount.__init__ at 0x000001A1B8A32340>, '__dict__':
<attribute '__dict__' of 'BankAccount' objects>, '__weakref__':
<attribute '__weakref__' of 'BankAccount' objects>, '__doc__':
None})

When an attribute is accessed using an instance name, first the instance namespace is
searched. If the attribute is found there then the value is returned, otherwise the attribute
is searched in the class namespace. If found there, then the value is returned, otherwise
AttributeError is raised. If there is an attribute with same name in both instance
namespace and class namespace, then the attribute in the instance namespace will be
returned, because it is looked up before the class namespace.
In other words, if there is an instance variable that has the same name as the class variable,
then the instance variable hides the class variable if you access the name through an
instance.
In the following example, we have a class variable named rate and we have an instance
variable which is also named rate.
class Account():

rate = 5

def __init__(self):

[Link] = 10

def display(self):

print([Link], [Link])

a = Account()

[Link]()

Output-
5 10

[Link]
gives us the value of class variable while
[Link]
gives us the value of instance variable. When we access a variable through an instance,
Python first checks whether the instance contains that variable, if the instance does not
contain that variable, then it checks the class to see if there is any class variable.

14.12 Changing a class variable through an instance


We have seen that we can access a class variable using either the class name or the instance
name, however things are different when we change the value of a class variable. If you
change the value of a class variable using the class name, it gets changed but if you try to
change the value of a class variable by using an instance, then something unexpected
occurs. Let us understand this with the help of a simple example.
class Account():

rate = 5

a1 = Account()

a2 = Account()

In this class, we have a class variable named rate and we have created two instances of
this class named a1 and a2. As we know, we can access this variable rate using the class
name or any of the two instances.
>>> [Link]

>>> [Link]

>>> [Link]

Let us change the value of this variable rate using the class name.
>>> [Link] = 6

>>> [Link]

>>> [Link]

>>> [Link]
6

The value of class variable was changed successfully. Now let us change the value of this
class variable using the instance variable a1.
>>> [Link] = 7

>>> [Link]

>>> [Link]

>>> [Link]

We observe that only the expression [Link] is showing the new value 7, while
[Link] and [Link] are showing 6. The assignment [Link] = 7 did not
change the class variable, it actually created a new instance variable named rate for the
instance a1 and the expression [Link] accessed this instance variable. To verify this, let
us check the ids and the __dict__ attribute.
>>> id([Link])

140713805079496

>>> id([Link])

140713805079528

>>> id([Link])

140713805079496

>>> a1.__dict__

{'rate': 7}
>>> a2.__dict__

{}

>>> Account.__dict__

mappingproxy({'__module__': '__main__', 'rate': 6, '__dict__':


<attribute '__dict__' of 'Account' objects>, '__weakref__':
<attribute '__weakref__' of 'Account' objects>, '__doc__':
None})

This confirms that the instance variable a1 got a new instance variable named rate.
In the previous section, we saw that if there is an instance variable and a class variable with
same name, then the class variable gets hidden if we access that name using the instance. In
our example, when we write [Link], first the instance namespace is searched and the
variable is found there and so its value is returned. When we write [Link], the instance
namespace is searched, this name rate is not found so the class namespace is searched,
the name is found there and its value is returned. Only instance object a1 gets an instance
variable named rate, other instances will continue to use the class variable whenever
attribute rate is accessed through them.
So, if you want to change a class variable, you should do it through the class name,
otherwise a new instance variable with the same name will be created for that particular
instance object, and this instance variable will shadow the class variable.
Similar thing will happen if you change the value of a class variable inside a method using
self. Let us see this with the help of the example that we have seen earlier. In the Person
class, we had added a class variable to count the number of instance objects. In the
__init__ method, we incremented this variable by writing the statement
[Link] += 1, if we change this to [Link] += 1 then the program will
not work correctly.
class Person:

species = 'Homo sapien'

count = 0

def __init__(self, name, age):

[Link] = name
[Link] = age

[Link] += 1

def display(self):

print(f'{[Link]} is {[Link]} years old


{[Link]}')

p1 = Person('Devanshi', 18)

p2 = Person('Devank', 10)

print([Link], [Link], [Link])

Output-
0 1 1

The statement [Link] += 1 does not change the class variable. We know that this
statement is equivalent to [Link] = [Link] + 1. When this statement
executes, the interpreter accesses the value of class variable count, adds 1 to this value
and then creates a new instance variable named count with the new value. This is why the
value of class variable count always remains 0, and whenever a new instance object is
created it gets a new instance variable named count with value 1.
When you assign to a class variable via the class, the attribute in the class namespace is
changed. When you assign to a class variable via an instance, a new instance variable with
the same name is created in the instance namespace.
If your class attribute is a mutable object, then it is possible to mutate it through the
instance objects. Since all objects access the same class attribute, anyone of them can make
in-place changes in the class attribute. This can give unexpected results if users are not
aware of this.

14.13 Class Methods


In the BankAccount class that we have seen earlier, suppose we need to create a method
that shows the class related details. We have three class variables in our class, so in our
method we will print the values of these variables.
class BankAccount:
rate = 5

min_balance = 100

min_balance_fees = 10

def __init__(self, account_number, owner_name, balance):

self.account_number = account_number

self.owner_name = owner_name

[Link] = balance

def withdraw(self, amount):

[Link] -= amount

def deposit(self, amount):

[Link] += amount

def details(self):

print(f'Rate : {[Link]}')

print(f'Minimum Balance : {BankAccount.min_balance}')

print(f'Minimum Balance fees :


{BankAccount.min_balance_fees}')

account1 = BankAccount('7348', 'Tom', 50)

[Link]()

Output-
Rate : 5

Minimum Balance : 100


Minimum Balance fees : 10

The details method displays all the class variables and as usual we have called it with an
instance. The self parameter was not used inside the method since it needed to access
only the class variables. This method does not need to access any instance specific
information. It would be better if we could call this method using the class name instead of
any instance name. We can do so if we make this method a class method. A class method is
a method that is associated with the class itself not with any particular instance of the class.
To make this method a class method we need to precede the method definition with the
line @classmethod.
@classmethod

def details(cls):

print(f'Rate : {[Link]}')

print(f'Minimum Balance : {cls.min_balance}')

print(f'Minimum Balance fees : {cls.min_balance_fees}')

The line @classmethod is a function decorator about which we will study later on, for
now you can understand that adding this line turns a normal method into a class method.
The other change that we can see in this method is that the parameter is now named cls.
This is because when a class method is called, the interpreter automatically sends the class
object and not any instance object. The parameter is conventionally named cls because it
is referring to the class object. You could write any other name here instead of cls, but just
like self, this name cls is also a strong convention. This word is a short form of class;
since class is a reserved word, this word cls is used.
Now, we can call this method with the class name. While calling, there is no need to provide
any argument for the cls parameter, interpreter will automatically send the class as the
argument for this parameter.
>>> [Link]()

Rate : 5

Minimum Balance : 100

Minimum Balance fees : 10


We do not need any instance to call this method. We know that all the class variables are
created even before any instance object is created, so we can call this method even if we do
not have any instance of this class. When the call [Link]() will
execute, interpreter will automatically send the class as the first argument. So, the
parameter cls refers to the class object inside the method definition, and that is why
inside the method we have accessed the class variables through cls instead of hardcoding
the class name.
A class method can also be invoked using an instance, but it makes more sense to invoke it
using the class name. Class methods can work only with the class variables, they cannot
access instance variables as they do not have a self parameter, and thus they have no
access to the state of the instance.
So, if we have to implement a method that needs to use only the class variables, we can
make that method a class method.
The normal methods that we have been defining till now, have self as the first parameter
and when they are called, they automatically receive the current instance as the first
argument. These methods are more precisely called instance methods, to distinguish them
from the class methods and static methods. So, when we simply say methods of a class, we
generally mean instance methods, because the other two are not as frequently used.
Now let us add a class method to the Person class that we have written earlier:
class Person:

species = 'Homo sapien'

count = 0

def __init__(self, name, age):

[Link] = name

[Link] = age

[Link] += 1

def display(self):

print(f'{[Link]} is {[Link]} years old


{[Link]}')

@classmethod
def show_count(cls):

print(f'There are {[Link]} {[Link]}s')

Person.show_count()

p1 = Person('Devanshi', 18)

p2 = Person('Devank', 10)

Person.show_count()

Output-
There are 0 Homo sapiens

There are 2 Homo sapiens

Inside the class method show_count we have used two class variables species and
count. We could call this method using any of the two instances.
>>> [Link]

>>> [Link]

We will get the same output, but calling with class name is more natural. So, when you have
to process some information that is associated with the class itself not with any instance
object, you can turn your method into a class method by writing the decorator
@classmethod and specifying cls as the first parameter.
Class methods can be used to create alternative initializers in a class and to break static
methods, we will see both of these approaches in detail.

14.14 Creating alternative initializers using class Methods


Class methods allow us to define alternative initializers (also known as factory methods) in
a class. These methods help us create instance objects from different types of input data.
Let us understand this with the help of an example. Again, we take the same Person class.
We have deleted the class variables to keep it short and simple.
class Person:

def __init__(self, name, age):

[Link] = name

[Link] = age

def display(self):

print(f'{[Link]} is {[Link]} years old')

p1 = Person('Devanshi', 18)

p2 = Person('Devank', 10)

We can initialize a new instance object of this Person class in only one way, by providing
values of name and age. There may be situations when we want to create instance objects
of type Person from different types of data. For example, we may have a string that
contains name and age separated by a comma, or we may have a dictionary that contains
name and age.
s = 'Jack, 23'

d = {'name': 'Jane', 'age': 34}

You might read this type of data from a file or from any other place. Now you want to be
able to create an instance of type Person from these types of strings and dictionaries.
Python does not support function overloading, so there can be only one type of initializer.
We cannot have more than one definition for __init__ method inside a class. To initialize
our objects in different ways, we can use class methods. In our Person class we will add
two class methods named from_str and from_dict.
class Person:

def __init__(self, name, age):

[Link] = name
[Link] = age

@classmethod

def from_str(cls, s):

name, age = [Link](',')

return cls(name, int(age))

@classmethod

def from_dict(cls, d):

return cls(d['name'], d['age'])

def display(self):

print(f'{[Link]} is {[Link]} years old')

s = 'Jack, 23'

d = {'name': 'Jane', 'age': 34}

p3 = Person.from_str(s)

[Link]()

p4 = Person.from_dict(d)

[Link]()

Output-
Jack is 23 years old

Jane is 34 years old

The method from_str takes a string as argument and creates and returns a Person
object, and the method from_dict creates a Person object from a dictionary.
In the from_str method, cls as usual is the first parameter and the next parameter s is
for accepting a string. We split the string s to get the values of name and age. After that we
create a new instance object of type Person by using these values of name and age. We
know that inside the class methods, the cls parameter refers to the class object. So, cls
here refers to the Person class and writing cls(name, int(age)) is equivalent to
writing Person(name, int(age)) and it will create a new Person instance object. It
will call __init__ to initialize the newly created object.
Similarly, our class method from_dict creates and returns a Person object from a
dictionary with 'name' and 'age' as keys. We have sent the values of the dictionary as
argument to the Person class initializer.
The methods from_str and from_dict are called with the class name. The instance
objects that are returned by these methods are assigned to names p3 and p4.
We can see that both the factory methods internally use the __init__ method to create
and return the instance objects. Instead of hardcoding the class name in these methods we
have used the cls parameter to create the objects. This is good, if in future we have to
rename the class or inherit a new class from this class. These factory methods would work
for any class inherited from the Person class.
So, if we want to create factory methods that support inheritance, we should use class
methods.
Instead of using these class methods, you might be tempted to change your __init__ so
that it works with different types of input data. You might think of using default arguments
or variable number of arguments and then use checks inside the method to process data
differently in each case. This approach can sometimes work, but it makes the code difficult
to understand and maintain. The if..elif..else construct could be confusing if there
are many cases to consider. The class methods approach is simpler to understand and also
increases the readability of the calling code. The __init__ method is generally a simple
method that initializes the instance variables from the arguments. The alternative
initializers can do additional pre-processing of data to create the instance. Rather than
cluttering our __init__ method with all the code, we create separate initializers. Here is
another example of a class method used to create an alternative initializer:
from datetime import datetime

class Employee:

def __init__(self, first_name, last_name, birth_year,


salary):

self.first_name = first_name
self.last_name = last_name

self.birth_year = birth_year

[Link] = salary

def show(self):

print(f'I am {self.first_name} {self.last_name} born in


{self.birth_year}')

class Person:

def __init__(self, name, age):

[Link] = name

[Link] = age

@classmethod

def from_employee(cls, emp):

name = emp.first_name + ' ' + emp.last_name

age = [Link]().year - emp.birth_year

return cls(name, age)

def display(self):

print('I am', [Link], [Link], 'years old')

e1 = Employee('James', 'Smith', 1990, 5000)

p1 = Person.from_employee(e1)

[Link]()
We want to create a Person object from an Employee object. For this, we will create a
class method named from_employee in the Person class.
An Employee object has first_name, last_name, birth_year and salary as
instance variables and in Person class we only need name and age. To get the name we
will add the first name and last name. To get age we will subtract birth year from the
current year. To get the current year we have to import datetime class from the
datetime module. After getting the name and age of Employee, we create a new
Person object and return it.

14.15 Static Methods


Sometimes we have to write methods that are related to the class but do not need any
access to instance or class data for performing their work. These methods could be some
helper or utility methods that are used inside the class but they can perform their task
independently. There is no need to define these methods as instance methods or class
methods as they do not need access to the instance object or the class object. We can define
these methods as static methods by preceding them with the @staticmethod decorator.
Unlike instance methods and class methods, static methods do not have any special first
parameter. They can have regular parameters, but the first parameter has no special
significance. So, when a static method is called, Python does not send the class object or the
instance object as the first argument. This is why these methods cannot access or modify
the instance state or the class state.
In the BankAccount class we saw earlier, we can add a static method named about that
can be used to display general information about the class.
class BankAccount:

……………………

……………………

@staticmethod

def about():

print('Information about BankAccount class ……')

print('…………')
print('…………')

[Link]()

A static method can be invoked using either the class name or an instance name.
In the following Date class, you can write a static method is_leap that can be used as
helper method in other methods of the class.
class Date:

def __init__(self, d, m, y):

self.d = d

self.m = m

self.y = y

def method1(self, year):

………

if [Link](year):

………

………

def method2(self, days):

………

if [Link](self.y):

………

………

@staticmethod
def is_leap(year):

if year % 4 == 0 and year % 100 != 0 or year % 400 ==


0:

return True

else:

return False

So, when you have to create a helper or utility method, that contains some logic related to
the class, turn it into a static method. For example, if you are creating a Fraction class,
you can create a static method for finding hcf of two numbers. This method can be used to
reduce the fraction to lowest terms.
We have learnt about instance methods, class methods and static methods. If you have to
make a method that needs to access instance variables, make it an instance method. An
instance method has special first parameter named self that refers to the current
instance object. If you have to make a method that needs to use only class variables and not
instance variables, make it a class method. A class method has a special first parameter
named cls that refers to the class object. When you need to create a general utility
method, that needs to use neither instance variables nor class variables, make it a static
method. Such a method depends only on its own argument values. It does not have any
special first parameter.
A static method is just like a regular function, but it belongs to the class namespace. We
know that the definition of a class defines a separate namespace and when you want to
group functionalities under the class namespace, you can create static methods.
Static methods are like normal functions so instead of defining a static method, you could
define a module level function that is defined near the class. If you have a single class per
module or only closely related classes in a module, then you can make a module level
function instead of writing a static method.
In the previous section, we saw that class methods could be used to create alternative
initializers. Class methods can also be useful while splitting static methods. Suppose we
have to write a static method that is very long and we decide to split it into several static
methods. So, now, our static method will call other static methods. For this, we have to
hardcode the class name, which can be a problem if we have inherited classes. We can
avoid the hardcoding of the class name if we use a class method instead of a static method,
because class method can use the parameter cls instead of the class name. Let us
understand this with the help of an example:
class MyClass:
@staticmethod

def method1():

print('method1 doing work')

MyClass.method2()

MyClass.method3()

@staticmethod

def method2():

print('method2 doing work')

@staticmethod

def method3():

print('method3 doing work')

Inside method1, we must hardcode the class name to call the other two static methods. We
can avoid this if we make method1 a class method.
class MyClass:

@classmethod

def method1(cls):

print('method1 doing work')

cls.method2()

cls.method3()

@staticmethod
def method2():

print('method2 doing work')

@staticmethod

def method3():

print('method3 doing work')

So, when you have a static method calling other static methods, convert it to a class method
to avoid hardcoding the class name.

14.16 Creating Managed Attributes using properties


Properties can be used to create data attributes with special functionality. If you want some
extra functionality (like type checking, data validation or transformation) while getting or
setting a data attribute, you can define a property which creates a managed attribute. The
user can access and modify this managed attribute with regular syntax (e.g.
print(MyClass.x) or MyClass.x = 3), but behind the scene some method will be
automatically executed while setting or getting the attribute. Property allows us to access
data like a variable, but the accessing is handled internally by methods. This way, we can
control attribute access by attaching custom behavior. Before seeing the syntax of creating
a property, first, we will see with the help of a simple example why we need properties.
Suppose we have developed this class Person, with two instance variables name and age,
and the method display.
class Person:

def __init__(self, name, age):

[Link] = name

[Link] = age

def display(self):

print([Link], [Link])

if __name__ == '__main__':
p = Person('Raj', 30)

[Link]()

Let us assume that this is a big class that is being used by many clients. After some time, we
as the implementors of the class want to restrain the value of age. We want to ensure that
whenever age is assigned a value, that value should be within the range 20 - 80.
A solution to this could be to make age a private variable and use getter and setter
methods to access and update this private variable. Setters (also know as mutators) and
getters (also know as accessors) are generally used in object-oriented languages to restrict
access to private variables and they allow you to control how these variables are accessed
and updated.
We modify the class and make age a private variable by prefixing it with an underscore, so
now client is not supposed to access it directly. We define a method set_age that will be
used to assign a value to the private variable _age, and we define another method
get_age that will be used to access the value of variable _age. In the set_age method
we can put the validation code.
class Person:

def __init__(self, name, age):

[Link] = name

self._age = age

def display(self):

print([Link], self._age)

def set_age(self, new_age):

if 20 <= new_age <= 80:

self._age = new_age

else:

raise ValueError('Age must be between 20 and 80')


def get_age(self):

return self._age

if __name__ == '__main__':

p = Person('Raj', 30)

[Link]()

Now, whenever the user wants to change the age, he will do it through the set_age
method, and the data validation will be done.
>>> p.set_age(100)

ValueError: Age must be between 20 and 80

>>> p.set_age(12)

ValueError: Age must be between 20 and 80

>>> p.set_age(25)

>>> [Link]()

Raj 25

So, by defining the setter and getter methods, we could successfully implement the new
restriction on age.
Earlier when there was no restriction, and age was a public variable, if the user had to
increase the current age by 1, he would simply write:
[Link] +=1

Now in the modified class, we have setter and getter methods so to increase the value of
age, user has to write this:
p.set_age(p.get_age() + 1)

These types of expressions are confusing and decrease readability. There is still a problem
in our modified class. When the user creates a new object, he can send any value for the age
because there is no data validation done in the initializer.
p1 = Person('Dev', 2000)

So, we need to perform the data validation in the initializer also by calling the set_age
method.
def __init__(self, name, age):

[Link] = name

self.set_age(age)

Now the data validation will be done at the time of creation of a new object also. It seems
that we have solved the problem of restricting the value of age. Now users of our class will
not be able to enter any value of age outside the range 20-80. But remember our Person
class is being used by several clients, and there is lot of existing code that accesses age
directly, for example [Link] = 30 or print([Link]). The new changes in your class will
break this client code and it will have to be rewritten with statements like
p.set_age(30) and print(p.get_age(). You have changed the user interface and so
your new update is not backward compatible. This refactoring can cause problems in your
client code.
To avoid this problem, in other object-oriented languages, programmers would start their
class design with private attributes along with getters and setters that do nothing except
getting and setting the value of the private variable. These setters and getters do not
perform any extra processing and they are not needed at the outset but they have to be
added because they might be needed later, when you need some processing to be done
while setting and getting an attribute. This design makes sure that if in future you have to
add any data validation, then the existing client code will not break. The clients will already
be accessing data through setters and getters, so you can change the implementation
without changing the interface and breaking your client’s code.
The getter and setter methods can also be used to make an attribute read only or write
only. If you define only the getter method for a private variable and don’t define the setter
method for it then the variable becomes read only, users will be able to read that variable
but cannot update it. As we have seen, setters and getters also allow data validation, i.e., the
setter method can control what value can be assigned to the variable and getter method
can change the way the variable is represented when it is accessed. In most other
languages, getter are setter methods are common and they are used to protect and validate
your private data.
This setter and getter methods approach is not preferred in Python, the Pythonic way of
going about this whole thing would be to create a property. Properties allow us to write our
class in a way that does not require the user of the class to call setter and getter methods.
The syntax of calling a property is same as the syntax for accessing a data attribute,
although it is actually a method. The client code that uses a property does not look like a
method call, instead it looks like a direct data attribute access. Let us see how we would use
create a property for age in our Person class:
class Person:

def __init__(self, name, age):

[Link] = name

[Link] = age

def display(self):

print([Link], [Link])

This was our initial Person class in which we had to make changes to include data
validation for age. Here is the modified class:
class Person:

def __init__(self, name, age):

[Link] = name

[Link] = age

@property

def age(self):

return self._age

@[Link]

def age(self, new_age):

if 20 <= new_age <= 80:

self._age = new_age
else:

raise ValueError('Age must be between 20 and 80')

def display(self):

print([Link], self._age)

We have added two special methods, and both are named age. Before the header line of
these methods, we have added a line starting with ‘@’ symbol. The line @property makes
the first method a getter method, and the line @[Link] makes the second method a
setter method.
Now after this modification, the name age has become a property, we can access it like we
access an instance variable. There is no need to call it like a method by using parentheses.
The actual value of age is stored in the private variable named _age. The age attribute is
a property which provides an interface to this private variable. The name of the property
should be different from the attribute where we store our data.
Whenever we reference the attribute named age, the method with the line @property
will be executed and whenever we assign something to it, the method with the line
@[Link] will be executed. The method with @property is the getter method and
the method with @[Link] is the setter method for the property. The setter method
accepts an argument which is used for setting the property. Note that the name of both
methods is the same; they are different because they are prefixed with different @ lines.
These lines are decorators, they decorate these methods. We have seen similar decorator
syntax when we learnt about class methods and static methods. We will learn about the
details of decorators later in a separate chapter. The getter method is always preceded with
@property decorator and the setter method is preceded with the decorator that contains
the property name followed by a dot and the word setter. If the name of your property is
salary then the decorator for its setter would be @[Link].
The user of the class can now access age as if it were an instance variable.
>>> p = Person('Raj', 30)

>>> [Link] + 1

31

>>> [Link] = 40

>>> [Link] = 200


ValueError: Age must be between 20 and 80

So, now we can easily access age as an instance variable and the data validation is also
done. This is much more concise and cleaner than it was using the set_age and get_age
methods approach. There is no need of calling the methods explicitly; whenever we access
or update the attribute age, these methods will be automatically called behind the scenes.
So, you can reference or assign to the property using the syntax of an instance variable, but
under the covers, the method code is getting executed. By defining this property, we have
added a new attribute that can be accessed like an instance variable.
In fact, if you put the parentheses, it will show error.
>>> [Link]()

TypeError: 'int' object is not callable

Note that we have not changed the initializer, we have not written self._age = age.
The statement in the initializer is [Link] = age. Since age is a property now, we are
setting the property age here and so the setter method will be automatically called and the
data validation will be done here also.
>>> p = Person('Raj', 300)

ValueError: Age must be between 20 and 80

The private variable _age is created in the setter method of the property. The initializer is
indirectly calling this setter method to make sure that the data validation is done. If in the
initializer, we write self._age = age then the data validation will not be done when a
new object is initialized.
So, when you need to perform some data validation on an existing instance attribute, you
can turn it into a property. The client can execute the property without using the
parentheses after the property name, so the client gets a cleaner syntax, which is more like
accessing a data attribute rather than a method call. The syntax is much better than the
set_age and get_age approach, and the existing client code will continue to work
smoothly even after these changes. No changes need to be done in the existing client code,
so the changes made to your class are backward compatible.
All this makes sense only when you respect the convention of using an underscore for
private attributes. The client code could use _age for referencing and assigning directly.
Python does not enforce any strict restriction, so programmers are supposed to follow the
conventions.

14.16.1 Creating read only attributes using properties


Another use of property is that you can make an attribute read-only or write-only. If you
provide only the getter method, not the setter method, the property becomes a read only
property. This way we can protect our private attribute from any sort of modification by
the client, while still giving the access to read the value of the attribute.
class Employee:

def __init__(self, name, password, salary):

self._name = name

[Link] = password

[Link] = salary

@property

def name(self):

return self._name

@property

def password(self):

raise AttributeError('password not readable')

@[Link]

def password(self, new_password):

self._password = new_password

@property

def salary(self):

return self._salary

@[Link]
def salary(self, new_salary):

self._password = new_salary

In this class Employee, we have defined three properties, name, password and salary.
For the name property, we have defined only the getter method, so this property becomes a
read only property. The attribute name is read only. This attribute can only be set when the
instance is created and it can only be changed within the class methods. It cannot be
modified from outside the class.
The attribute password is write-only because in its getter method we have raised
AttributeError. Note that it is necessary to provide the getter method, you cannot
make an attribute write only by providing just the setter method.
If you provide both the setter and getter methods, the property becomes read/write
property. For example, the attribute salary can both be referenced and assigned to, it is a
read/write property.
>>> e = Employee('Jill', 'feb31', 5000)

>>> [Link]

'Jill'

The name attribute is read-only, if we try to assign something to it, we cannot.


>>> [Link] = 'Jack'

AttributeError: property 'name' of 'Employee' object has no


setter

The password attribute is not readable.


>>> [Link]

AttributeError: password not readable. Did you mean:


'_password'?

>>> [Link] = 'feb29'

The salary attribute is both readable and writable.


>>> [Link] = 6000
>>> [Link]

6000

14.16.2 Creating Computed attributes using properties


A common use of property is to create dynamically computed attributes, the values of these
attributes are not actually stored, they are computed on request. Let us see an example of
this:
class Rectangle():

def __init__(self, length, breadth):

[Link] = length

[Link] = breadth

[Link] = ([Link] * [Link] + [Link] *


[Link]) ** 0.5

def area(self):

return [Link] * [Link]

def perimeter(self):

return 2 * ([Link] + [Link])

In this Rectangle class we have three instance variables, length, breadth and
diagonal, and two methods area and perimeter. The value of instance variable
diagonal is computed from the values of instance variables length and breadth.
>>> r = Rectangle(2, 5)

>>> [Link]

5.385164807134504
>>> [Link]()

10

>>> [Link]()

14

Now let us change length:


>>> [Link] = 10

>>> [Link]

5.385164807134504

We changed length, but value of diagonal has not changed.


>>> [Link]()

50

>>> [Link]()

30

Area and perimeter have changed because they are implemented as methods.
So, if you change the value of an instance variable, any other instance variable that is
computed from it will not automatically update. Here in this class if we change length or
breadth, then diagonal will not change accordingly. One solution could be to
implement diagonal as a method. But then we will not be able to access it as an instance
variable; whenever we want to access it, we have to put parentheses. This will also break
any client code that has used diagonal as an instance variable. The solution is to turn it
into a property.
@property

def diagonal(self):

return ([Link] * [Link] + [Link] *


[Link]) ** 0.5
Now we can continue to access diagonal as an instance variable; whenever we will
access diagonal, its value will be calculated and we will get the updated value. So,
changes in length and breadth will be reflected in the diagonal.
>>> r = Rectangle(2, 5)

>>> [Link]

5.385164807134504

>>> [Link] = 10

>>> [Link]

11.180339887498949

There is no need to define the setter method, because we do not expect the user to change
the diagonal.

14.16.3 Deleter method of property


We can also define a deleter method for the property, this deleter method defines what
happens when a property is deleted. To create the deleter method, you have to define a
method with the same name as the property and add the decorator with the word
deleter in it.
class Person:

def __init__(self, name, age):

[Link] = name

[Link] = age

@property

def age(self):

return self._age

@[Link]
def age(self, new_age):

if 20 <= new_age <= 80:

self._age = new_age

else:

raise ValueError('Age must be between 20 and 80')

@[Link]

def age(self):

del self._age

print('age deleted')

def display(self):

print([Link], self._age)

The deleter method will be executed, when the attribute is deleted.


>>> p = Person('Jill', 25)

>>> print([Link])

25

>>> del [Link]

age deleted

Let us summarise what we have learnt about properties.


A property allows access to an instance variable through methods, even though the method
syntax is not used. By using the property syntax, we can define methods that are
automatically called when an instance variable is referenced, assigned or deleted. We can
define three methods for a property:
Getter - executed when the attribute is accessed, preceded with decorator @property
Setter - executed when the value of attribute is set, preceded with decorator
@[Link]
Deleter - Executed when the attribute is deleted, preceded with decorator
@[Link]
All three methods have same name which is the name of the property, they are
distinguished because of the decorators. All of them take self as the first argument and
the setter method takes an additional argument for setting the value of the property. If you
want to provide a docstring for the property, specify it in the getter method.
Properties can be used for attribute type checking and validation, for creating read-only or
write-only attributes and for creating computed attributes. You can incorporate new
behaviour in your instance variables, without any need to rewrite the existing client code.
Thus, you can use a property to give new functionality to existing instance variables.
In the property getter and setter methods, do not perform actions that are surprising or
take much time. Referencing or assigning to an attribute is something that the client will
expect to run instantly so it is not advisable to run other helper methods in the property
methods. If a task is very complex and time consuming and may have side effects, consider
putting it in a separate normal method.

14.17 Designing a class


After learning about data hiding and properties, let us see how to decide which attributes
should be private, public or turned into a property. If there is an attribute that should never
be accessed by the user, it should be made private by prefixing it with an underscore. There
is no need of defining any property for it, as it should not be accessed from outside the
class. These internal attributes are part of the implementation and should not be exposed
in the public interface in any way.
Then there are attributes that have to be accessed or modified by the user. You can start
implementing your class by coding such instance variables as public and in future if you
need more control over any instance variable, you can change it to a private attribute and
write a property to access it. You should define a property only if it provides some extra
functionality to the attribute. There is no point in defining a property that just gets and sets
data without any extra logic. In other languages that do not have property mechanism, we
need such setter and getters, but in Python we can always start with plain public attributes
and promote them to properties whenever required without changing the interface. Public
attributes that need no extra functionality while being accessed or modified should remain
plain public attributes in the class.
So, in Python, you can start with a very simple design and later introduce properties
without changing the interface. There is no need to pollute your space with multiple setters
and getters just to ensure that future changes are backward compatible.
Inheritance and Polymorphism
The example classes that we have seen are quite short, but the classes written for real-
world applications would be complex, lengthy, and will contain a lot of code. It would take a
considerable amount of time to develop and test a fully functional class. Sometimes, we
may need to write a class that has most of the features of an existing class, along with some
additional features. Writing such a class from scratch and testing it would be time-
consuming, and it would be good if we could somehow use our existing class to create our
new class. Python provides the feature of inheritance for this purpose. By using
inheritance, we can create a new class based on an existing class. In our new class, we get
all the features of the existing class, and can also add new features and also override
(replace) them as needed. Thus, we can easily create new classes by using the tried and
tested functionality of existing classes. This reduces time and effort and simplifies the task
of writing a new class.
Inheritance is an important feature of object-oriented programming; it is basically a
mechanism of creating a new class from an existing class. The new class is the extended
and modified version of the existing class. The main advantage of inheritance is that it
facilitates code reuse and reduces code duplication. Inheritance also simplifies the design
of the program as it lets you represent the real-world problems in a natural and better way.
This makes the program more readable and easier to manage.
Let us understand inheritance with the help of an example. Suppose we want to create
Employee objects which should have the following data members and methods:

Figure 16.1: Employee class

We already have a Person class that has most of the functionality that we need for an
Employee class.
Figure 16.2: Person class

Instead of creating a brand-new Employee class from scratch, we can create our
Employee class by inheriting from the Person class.

Figure 16.3: Employee class inheriting from Person class

The existing class is called the base class and the new class is called the derived class. When
you inherit from a class, everything from that class becomes automatically available in the
derived class. The derived class inherits members from the base class and also contains its
own members. There is no need to copy everything from Person class to the Employee
class. Due to inheritance, Employee class has access to everything from the Person class
and the Employee class can have variables and methods of its own also.
So, when you derive a class, that class gets access to everything from the base class, it can
add new variables and methods of its own and it can even change the way some methods of
the base class work. For example, the contact_details method that is there in the
Person class, will be inherited by the Employee class. If you want this method to work
differently for Employee class, you can provide a separate code for it in the Employee
class. Thus, a derived class can add its own version of a method which is called overriding.
Derived classes generally have some added functionality and provide more specific
behaviour than the base class. Base class is also called the parent class or super class and
the derived class is also called child class or subclass.
In object-oriented terms, the relationship between the base class and derived class is called
is-a relationship. Derived class is a type of base class, for example an Employee is a Person.
So, by using inheritance, you can implement an is-a type of relationship between classes.

16.1 Inheriting a class


We have the following class Person with four instance variables and three methods and
we have made a new class named Employee by inheriting from this class.
class Person:

def __init__(self, name, age, address, phone):

[Link] = name

[Link] = age

[Link] = address

[Link] = phone

def greet(self):

print('Hello I am', [Link])


def is_adult(self):

if [Link] > 18:

return True

else:

return False

def contact_details(self):

print([Link], [Link])

class Employee(Person):

pass

When we write a new class from scratch, after the class name, we have the colon but when
we write a class by inheriting from an existing class, after the class name we have the name
of the existing class inside the parentheses. Since we are creating our Employee class by
inheriting from the Person class, the name Person is inside the parentheses. The line
class Employee(Person): means create a new class Employee that inherits from
the Person class.
In the class definition we just have a pass statement. We have not written anything inside
this class but since it is inherited from the Person class, it gets access to everything from
the Person class. Let us create an instance of this class:
>>> emp = Employee('Raghu', 30, 'D4, XYZ Street, Delhi', '994477291')

The Employee class has access to the __init__ method of the Person class, so all these
arguments will be passed to that __init__ method. This instance object will have all the
attributes name, age, address and phone.
>>> [Link]
'Raghu'

>>> [Link]

30

>>> [Link]

'D4, XYZ Street, Delhi'

>>> [Link]

'994477291'

We can call all the methods of the Person class through emp.
>>> [Link]()

Hello I am Raghu

>>> emp.is_adult()

True

>>> emp.contact_details()

D4, XYZ Street, Delhi 994477291

So, we can see that the instance object of Employee class has access to everything from
the Person class. Let us use the isinstance function on emp.
>>> isinstance(emp, Employee)
True

>>> isinstance(emp, Person)

True

The isinstance function returned True for the Person class also, which proves the is-a
relationship between Employee and Person.
There is another built-in function named issubclass that can be used to check whether
a class is subclass of another class.
>>> issubclass(Employee, Person)

True

This returns True because Employee is a subclass of Person class.


In the process of inheritance, the base class is not changed in any way. The derived class
can differentiate itself from the base class in two ways: by adding new data members and
methods or by overriding the methods of the base class. We will see how to do this in the
coming sections.

16.2 Adding new methods and data members to the derived


class
While defining our derived class, we can add new data members and methods that are
specific to our derived class. We will again create our Employee class by inheriting from
the Person class but this time, we will add some new methods and data members to our
Employee class.
class Employee(Person):

def __init__(self, name, age, address, phone, salary, office_address, office_phone):

[Link] = name

[Link] = age
[Link] = address

[Link] = phone

[Link] = salary

self.office_address = office_address

self.office_phone = office_phone

def calculate_tax(self):

if [Link] < 5000:

return 0

else:

return [Link] * 0.05

We have defined two new methods in this class __init__ and calculate_tax. The
__init__ method of the base class is inherited but our derived class needs some
additional variables that need to be initialized so the inherited base method will not be
sufficient. An Employee object needs to have 3 more instance variables which are
salary, office_address and office_phone. In the __init__ method, the first 4
parameters are the same as in Person class and after that we have added 3 new
parameters. The first four lines can be copied from the __init__ of Person class, and
then we have written three more lines to create the three attributes that are specific to the
Employee class.
Now when we create an Employee instance, we need to send seven arguments.
>>> emp = Employee('Raghu', 30, 'D4, XYZ Street, Delhi', '994477291', 8000, 'ABC Street,
Delhi', '897657888')

Since Employee class has its own __init__ now, the __init__ of Person will not be
called when we create an Employee instance. The __init__ of Employee is called and
all the seven instance attributes are created. The Employee class inherits the methods
greet, is_adult, contact_details from the Person class and has its own
method named calculate_tax.
>>> [Link]

'Raghu'

>>> [Link]

8000

>>> emp.calculate_tax()

400.0

16.3 Overriding a base Method


Sometimes you may want a method from the base class but you would like it to behave
differently in the derived class. For example, we want a contact_details method for
Employee class but we want that to have a different definition from what is there in the
Person class. In such a case, you can override the method. To override a method, just
define a method in the derived class with same name as in the base class. So, let us override
the method contact_details. In Employee class, we want to display the
office_address and office_phone also.
class Employee(Person):

def __init__(self, name, age, address, phone, salary, office_address, office_phone):


[Link] = name

[Link] = age

[Link] = address

[Link] = phone

[Link] = salary

self.office_address = office_address

self.office_phone = office_phone

def calculate_tax(self):

if [Link] < 5000:

return 0

else:

return [Link] * 0.05

def contact_details(self):

print([Link], [Link])

print(self.office_address, self.office_phone)
Now Employee has its own version of contact_details, and when an Employee
instance will call this method, its own version will be executed instead of the base class
version.
>>> emp = Employee('Jack', 30, 'D4, XYZ Street, Delhi', '994477291', 8000, 'ABC Street,
Delhi', '897657888')

>>> emp.contact_details()

D4, XYZ Street, Delhi 994477291

ABC Street, Delhi 897657888

So, if a derived class defines a method with same name as a method in base class, then the
derived class method overrides the method of base class, it effectively hides the base class
method. We have seen that this happened with the __init__ method also. Since we have
written a definition for __init__ in the Employee class, the base class version of
__init__ is hidden.

16.4 Invoking the base class methods


While overriding a method, most of the times you want to extend the base class method
instead of replacing it fully. So, mostly you need to begin by calling the base class method
and then add special code that is specific to the derived class. The base class version can be
accessed in the derived class by calling it explicitly using the base class name. For example,
in contact_details method, instead of copying the code from the base class, we could
call the base class version. We have overridden the __init__ method also, so we could do
the same thing in __init__ also. Instead of copying the code from __init__of Person
class, we could call the Person class __init__.
class Employee(Person):

def __init__(self, name, age, address, phone, salary, office_address, office_phone):

Person.__init__(self, name, age, address, phone)

[Link] = salary
self.office_address = office_address

self.office_phone = office_phone

def calculate_tax(self):

if [Link] < 5000:

return 0

else:

return [Link] * 0.05

def contact_details(self):

Person.contact_details(self)

print(self.office_address, self.office_phone)

If the derived class is overriding a method and wants to use the functionality of the base
version, then it is better to call the base method instead of just copying the code. This
reduces code duplication and later if the base class method changes, the change will be
reflected in the derived class method. A better way of calling the base class method is by
using the super built in function.
class Employee(Person):

def __init__(self, name, age, address, phone, salary, office_address, office_phone):

super().__init__(name, age, address, phone)


[Link] = salary

self.office_address = office_address

self.office_phone = office_phone

def calculate_tax(self):

if [Link] < 5000:

return 0

else:

return [Link] * 0.05

def contact_details(self):

super().contact_details()

print(self.office_address, self.office_phone)

Now there is no need of sending self as the first argument. Use of super is preferred
because using base class name can create confusion in multiple inheritance, where a class
inherits from more than one class. Writing super makes sure that all the base class
versions are called, even from the classes that are inherited indirectly as we will see
shortly.

16.5 Multilevel Inheritance


We can have multilevel inheritance which means that from the derived class we can further
inherit another class. For example, from the Employee class we can inherit a class called
Teacher and a class called Accountant.
Figure 16.4: Multilevel inheritance

All attributes of Person are available in Employee and all attributes of Employee are
available in Teacher class and Accountant class. From Person we have inherited
another class named Student. We know that there is an is-a relationship between the
derived classes and base classes. Therefore, Teacher is-a Employee, Accountant is-a
Employee, Employee is-a Person, Student is-a Person, Teacher is-a Person,
Accountant is-a Person.
An advantage of inheritance is that you can design your system by using inheritance so that
it will reflect the natural relationship between different components of your system. This
simplifies the design and makes programs easier to understand. Thus, reusability of code
and being able to represent the system using hierarchy of classes are the two main benefits
of inheritance. However, you should use inheritance only when there is some natural
relationship between classes. Unnecessary use of inheritance can make the system
incomprehensible and can create unwanted dependencies between classes.

16.6 object class


object
class is a built-in class from which every class automatically inherits. All built-in classes
inherit from it and the custom classes that you define also inherit from it.

>>> class Person:

pass

>>> issubclass(Person, object)


True

We defined a Person class, and we can see that it automatically inherits from the object
class. So, when we don’t specify any base class, the class directly inherits from object
class. The above class definition is equivalent to explicitly inheriting from the object
class.
>>> class Person(object):

pass

In Python 3, this is redundant. It is not necessary to define a new class by deriving it


explicitly from the object class. Any class that is defined without an explicit base class
will be a derived class of object. So, every class inherits from the object class directly or
indirectly. This class is at the top of any inheritance hierarchy in Python.
Our Person class definition is empty, but if we call dir function on it, we don’t see an
empty list.
>>> class Person:

pass

>>> dir(Person)

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',


'__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__',
'__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

Most of the attributes in this list come from the object class since our Person class is
implicitly derived from the object class.
>>> dir(object)

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__',


'__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__str__', '__subclasshook__']

The dunder methods available in object class are inherited by the classes that you define
and so are available in all your classes. You can override these methods by providing your
own definition in the class. For example, when you use == with your instance objects,
Python will call the __eq__ method defined in the object class, which will compare
objects based on their identity. If you want the objects to be compared in some other way,
you can override __eq__ by defining it in your class. If you do so, the interpreter will call
your version when you use == with the instance objects.
All built-in types like str, int, dict are names of classes, and so they are also subclasses
of object class.
>>> issubclass(str, object)

True

>>> issubclass(int, object)

True

>>> issubclass(dict, object)

True

16.7 Multiple Inheritance


Multiple inheritance is not very commonly used as it can make the design quite complex
and confusing, but it is good to have an idea about it as you might encounter it in some
library or some other code.
Till now, the inheritance that we have seen is single inheritance, which means that a class
inherits from a single class. Python supports multiple inheritance, meaning a class can
inherit from multiple base classes.
Figure 16.5: Multiple inheritance

Here class X inherits from classes A, B and C. All the data members and methods of all the
three base classes will be available in the derived class X.
class X(A, B, C):

pass

This is the syntax of defining a new class that inherits from multiple classes. All the base
classes are placed inside the parentheses. This class definition creates a new class named X
that inherits from classes A, B and C.
Here is another example of multiple inheritance:

Figure 16.6: TeachingAssistant inheriting from Student and Teacher

A teaching assistant is a student who also teaches. Thus, the class TeachingAssistant
inherits from both the class Teacher and Student.
class Teacher:

def greet(self):

print('I am a Teacher')
class Student:

def greet(self):

print('I am a Student')

class TeachingAssistant(Student, Teacher):

def greet(self):

print('I am a Teaching Assistant')

The class TeachingAssistant is inherited from classes Teacher and Student. All
the 3 classes have defined a method named greet. Let us create an instance of the class
TeachingAssistant and call the method greet on it.
>>> x = TeachingAssistant()

>>> [Link]()

I am a Teaching Assistant

Since the class TeachingAssistant has its own greet method, it will be called. Now
suppose the class TeachingAssistant had not defined the greet method.
class TeachingAssistant(Student, Teacher):

pass

Now greet() is not present in this class, so the interpreter will look for it in the base
classes. Both the base classes have the method named greet, so now the question is which
one will be executed.
>>> x = TeachingAssistant()
>>> [Link]()

I am a Student

This output shows us that the greet method from Student class was executed. This is
because while searching the multiple base classes, the search is performed from left to
right. Let us change the order of base classes in the class definition and execute the greet
method again.
class TeachingAssistant(Teacher, Student):

pass

>>> x = TeachingAssistant()

>>> [Link]()

I am a Teacher

Now the greet method from Teacher is executed because the Teacher class is now on
the left in the class definition of TeachingAssistant.
All the base classes of a class can be seen using the __bases__ attribute.
>>> TeachingAssistant.__bases__

(<class '__main__.Teacher'>, <class '__main__.Student'>)

We get a tuple with both the base classes and the order of the base classes is the same as
specified in the class definition.
So, we saw an example of multiple inheritance, but it was just one level deep. There can be
multiple levels of inheritance, for example the two class Student and Teacher could be
derived from a common base class Person.
Figure 16.7: Diamond inheritance

This is known as diamond inheritance. We know that every class is derived from object,
so it is the base class for Person. If there are many classes in the system, and both
multilevel and multiple inheritance are involved, then the whole structure can be quite
complex. As the structure becomes complex, searching for attributes in base classes does
not remain straightforward. Conflicts can arise if the base classes contain attributes with
the same names. To resolve any sort of conflict while searching for attributes in base
classes, Python uses a well-defined algorithm which is the topic of the next section.

16.8 Method Resolution Order (MRO)


The order in which Python searches for attributes in base classes is called method
resolution order(MRO). It gives a linearized path for an inheritance structure.
Python computes an MRO for every class in the hierarchy; this MRO is computed using the
‘C3 linearization algorithm’. This algorithm is quite complicated, you can check the
documentation if you are interested in the details but roughly it works in a depth first, left
to right manner, and it searches each class only once. For example, in our previous
diamond inheritance example, the Person class can be reached in two ways but it will be
looked up only once in MRO.
We can see the MRO for any class using the __mro__ attribute or the mro method or by
using the help function. If we have an instance and want to see its MRO dynamically, we
can use the __class__ attribute.
Here is the code for the diamond example that we have seen. The classes Student and
Teacher inherit from class Person, and the class TeachingAssistant inherits from
classes Student and Teacher. All the classes have defined a method named greet.
class Person:
def greet(self):

print('I am a Person')

class Teacher(Person):

def greet(self):

print('I am a Teacher')

class Student(Person):

def greet(self):

print('I am a Student')

class TeachingAssistant(Student, Teacher):

def greet(self):

print('I am a Teaching Assistant')

If we use help on the class TeachingAssistant, we will see MRO for it at the top.
>>> help(TeachingAssistant)

Help on class TeachingAssistant in module __main__:

class TeachingAssistant(Student, Teacher)

| Method resolution order:


| TeachingAssistant

| Student

| Teacher

| Person

| [Link]

…………………………

If we use any attribute on an instance of the class TeachingAssistant, first it will be


searched in the class TeachingAssistant, then Student, then Teacher and then in
Person, and at last in the built-in object class. The search will stop as soon as the
attribute is found, and if the attribute is not found in any of these classes, then an error will
be raised.
We can get this MRO in a tuple by using the __mro__ attribute on the class name.
>>> TeachingAssistant.__mro__

(<class '__main__.TeachingAssistant'>, <class '__main__.Student'>, <class


'__main__.Teacher'>, <class '__main__.Person'>, <class 'object'>)

If we use the mro() method, we get this order in a list.


>>> [Link]()

[<class '__main__.TeachingAssistant'>, <class '__main__.Student'>, <class


'__main__.Teacher'>, <class '__main__.Person'>, <class 'object'>]

To find the MRO through an instance dynamically, we can specify the __class__ attribute
before the __mro__ attribute.
>>> x = TeachingAssistant()
>>> x.__class__.__mro__

(<class '__main__.TeachingAssistant'>, <class '__main__.Student'>, <class


'__main__.Teacher'>, <class '__main__.Person'>, <class 'object'>)

The class TeachingAssistant has its own version of greet method so when we call
the method greet on an instance of TeachingAssistant, this method is executed.
>>> x = TeachingAssistant()

>>> [Link]()

I am a Teaching Assistant

Now let us delete the greet method from the TeachingAssistant class.
class TeachingAssistant(Student, Teacher):

pass

>>> x = TeachingAssistant()

>>> [Link]()

I am a Student

Now the interpreter did not find greet in TeachingAssistant, so it searched for it in
Student which is next in line in MRO. Let us delete greet from the Student class also.
class Student(Person):

pass

>>> x = TeachingAssistant()
>>> [Link]()

I am a Teacher

Next in the MRO hierarchy is Teacher, so this method is executed. Let us delete it from the
Teacher class also.
class Teacher(Person):

pass

>>> x = TeachingAssistant()

>>> [Link]()

I am a Person

Next in line was Person class, so this method is executed. If we delete the greet method
from the Person class, also, then next class in MRO will be object class, and it does not
have any greet method, so we will get an error.
>>> x = TeachingAssistant()

>>> [Link]()

Traceback (most recent call last):

File "<pyshell#17>", line 1, in <module>

[Link]()

AttributeError: 'TeachingAssistant' object has no attribute 'greet'


So, this is how Python looks for attributes in base classes. Although this order is called
method resolution order, it holds true while searching for data attributes also.

16.9 super and MRO


The method resolution order that we saw in the last section is used by the built in function
super() also. This function always invokes the next class in the MRO.
In section 16.4, we had seen that when we override a method in the derived class and need
to call the base class version in that method, we could use base class name or super(). In
single inheritance, there is not much confusion as every class has a single parent in the
inheritance chain, but in multiple inheritance, a class can have multiple parents and using
super avoids all problems. To understand this, we will again take the same example of
multiple inheritance that we saw in the previous section.
class Person:

def greet(self):

print('I am a Person')

class Teacher(Person):

def greet(self):

[Link](self)

print('I am a Teacher')

class Student(Person):

def greet(self):

[Link](self)
print('I am a Student')

class TeachingAssistant(Student, Teacher):

def greet(self):

[Link](self)

[Link](self)

print('I am a Teaching Assistant')

All the classes have defined the method greet. In the Teacher class, we have called the
base class version of greet in the overridden method. Similarly, in Student class also we
have called the base class version of greet. The class TeachingAssistant has two
base classes so we have called greet versions from both the base classes.
Now let us create a TeachingAssistant instance and call greet method on it.
>>> x = TeachingAssistant()

>>> [Link]()

I am a Person

I am a Student

I am a Person

I am a Teacher

I am a Teaching Assistant
The greet method of TeachingAssistant class is executed. Inside this method, first
the method [Link]() is executed, then [Link]() is executed and
then the message ‘I am a Teaching Assistant’ is printed. [Link] in turn calls
[Link], and [Link] also calls [Link]. So, the greet method
from Person class will be called two times. This repetition can be avoided if we replace
the base class names with super.
class Person:

def greet(self):

print('I am a Person')

class Teacher(Person):

def greet(self):

super().greet()

print('I am a Teacher')

class Student(Person):

def greet(self):

super().greet()

print('I am a Student')

class TeachingAssistant(Student, Teacher):

def greet(self):
super().greet()

print('I am a Teaching Assistant')

>>> x = TeachingAssistant()

>>> [Link]()

I am a Person

I am a Teacher

I am a Student

I am a Teaching Assistant

Now the greet from Person was executed only once, this is because the function super
follows MRO.
>>>help(TeachingAssistant)

| Method resolution order:

| TeachingAssistant

| Student

| Teacher

| Person
| [Link]

…………………………………………………………………

This is the MRO for TeachingAssistant class. When the super() function is invoked
in TeachingAssistant class, it refers to Student, because it is the next in MRO. In
Student if again super is present, it invokes Teacher class as it is the next in MRO. And
then super inside Teacher class invokes the class Person as it is the next in MRO. We
can see that super() follows MRO and it does not always call the parent of a class, it calls
the one that is next in line based on MRO.
So, we have seen how these super calls perfectly called the base class versions without
any repetition. The repetition that we get if we don’t use super, can cause hard to find
bugs when __init__ is called using base class names. There is an example in the exercise
which will help you understand this.
Now suppose we have an instance of Student class, and we call the greet method with
that Student instance.
>>> s = Student()

>>> [Link]()

I am a Person

I am a Student

The method super in Student class will invoke the greet method of Person class. This
is because in MRO of Student class, Person comes next.
>>> help(Student)

| Method resolution order:

| Student

| Person
| [Link]

Thus we have seen that the method resolution order is used by Python when searching for
attributes in base classes and it is also used by the built in function super.
It is good to use super() whether you are using single inheritance or multiple
inheritance. In multiple inheritance, the advantage is obvious. In single inheritance, also
super can be beneficial if there are some updates made in the future like changing the
name of the base class or switching to multiple inheritance. Thus, it makes the code more
maintainable.

16.10 Polymorphism
The three main features of object-oriented programming are - encapsulation, inheritance
and polymorphism. We have seen the first two, now let us see what is polymorphism. The
meaning of the word polymorphism is the ability to take many forms. In programming, this
means the ability of code to take different forms depending on the type with which it is
used. The behaviour of the code can depend on the context in which it is used. Let us
understand this with the help of an example.
def do_something(x):

[Link]()

[Link]()

We have this function do_something that has a parameter x. Inside this function, two
methods are called on x. We know that Python is a dynamically typed language; there are
no type declarations. The type of parameter x is not declared, we can send any type of
object to this function. We could send a list object or a str object, but in that case, we
will get error because str and list types do not support the methods move and stop.
The function do_something will work correctly as long as we send objects of those types
that support the two methods move and stop.
Next, we have defined three classes that have the methods move and stop. The
implementation for these methods is different in each one of them.
class Car:

def start(self):
print('Engine started')

def move(self):

print('Car is running')

def stop(self):

print('Brakes applied')

class Clock:

def move(self):

print('Tick Tick Tick')

def stop(self):

print('Clock needles stopped')

class Person:

def move(self):

print('Person walking')

def stop(self):

print('Taking rest')
def talk(self):

print('Hello')

Let us create instance objects of these classes.


>>> car = Car()

>>> clock = Clock()

>>> person = Person()

We can send all these instance objects to the do_something function since all three of
them support the move and stop functions.
>>> do_something(car)

Car is running

Brakes applied

>>> do_something(clock)

Tick Tick Tick

Clock needles stopped

>>> do_something(person)

Person walking

Taking rest
So, any object that supports the two operations move and stop can be sent to this
function. The behaviour of move and stop depends on the type of the object that they are
operating upon. This is polymorphism, the same code can take different forms. While
executing the code of function do_something, interpreter does not care about the type of
x; any object that supports the two methods move and stop will work regardless of its
specific type. Python is not concerned about what an object is, it just needs to know what
an object does. Let us see another example.
class Rectangle:

name = 'Rectangle'

def __init__(self, length, breadth):

[Link] = length

[Link] = breadth

def area(self):

return [Link] * [Link]

def perimeter(self):

return 2 * ([Link] + [Link])

class Triangle:

name = 'Triangle'

def __init__(self, s1, s2, s3):

self.s1 = s1
self.s2 = s2

self.s3 = s3

def area(self):

sp = (self.s1 + self.s2 + self.s3) / 2

return ( sp*(sp-self.s1)*(sp-self.s2)*(sp-self.s3) ) ** 0.5

def perimeter(self):

return self.s1 + self.s2 + self.s3

class Circle:

name = 'Circle'

def __init__(self, radius):

[Link] = radius

def area(self):

return 3.14 * [Link] * [Link]

def perimeter(self):
return 2 * 3.14 * [Link]

def find_area_perimeter(shape):

print([Link])

print('Area : ', [Link]() )

print('Perimeter : ', [Link]() )

r1 = Rectangle(13, 25)

r2 = Rectangle(14, 16)

t1 = Triangle(14, 17, 12)

t2 = Triangle(25, 33, 52)

c1 = Circle(14)

c2 = Circle(25)

We have three classes named Rectangle, Triangle, and Circle. All the three classes
have the methods named area and perimeter and all of them have a class variable
name. In the Rectangle class, we have two instance variables length and breadth and
the area is calculated by multiplying them and the perimeter by the formula 2 (length +
breadth). In the class Triangle, we have three instance variables which represent the
three sides, the area is calculated using Heron’s formula and the perimeter is calculated by
adding the three sides. In the Circle class, there is only one instance variable which is the
radius, area is πr2 and perimeter is 2πr. We have created a polymorphic function
find_area_perimeter and created two instance objects of each class. Let us call the
function with these instance objects as a parameter.
>>> find_area_perimeter(t2)
Triangle

Area : 330.0

Perimeter : 110

>>> find_area_perimeter(c1)

Circle

Area : 615.44

Perimeter : 87.92

>>> find_area_perimeter(r2)

Rectangle

Area : 224

Perimeter : 60

We can see that the code inside the function find_area_perimeter could take different
forms depending on the type of shape.
Now, suppose we have a list of these objects, and we want to find out the total area and
perimeter of all the shapes in this list:
shapes = [r1, r2, t1, t2, c1, c2]

total_area = 0

total_perimeter = 0
for shape in shapes:

total_area += [Link]()

total_perimeter += [Link]()

print(total_area, total_perimeter)

In the for loop, we are iterating over the list and calling the area and perimeter
methods on each object. After that, we print the total area and perimeter. This is again an
example of polymorphic code.
Other object-oriented languages might need these classes to be derived from a common
base class to exhibit this polymorphic behaviour. However, in Python there is no such
restriction, polymorphism in Python does not depend on inheritance. For polymorphism to
occur you just need to define different classes which have commonly named methods.
Python’s polymorphism is based on duck typing, which comes from the old saying, ‘If it
walks like a duck and quacks like a duck, then it is a duck.’ Different objects that have
common method names can be treated in the same general way. Let us see some benefits of
polymorphism.
You can write generic code that can work with objects of different classes. When this
generic code is executed, Python uses polymorphism to call the appropriate method for
each instance object.
Polymorphism makes your code concise and flexible and provides a sort of abstraction.
When writing the generic code, a programmer need not think about the specific classes that
will use the code.
The code becomes easy to update also, you can easily add new types. The functions that are
already written can work with new types that you define in future as long as those new
types support the required operations. For example, in future you can add a Rhombus class
with area and perimeter methods, and you can easily use it with the polymorphic code
that we have seen before.
The behavior shown by overloaded operators is also polymorphism. An overloaded
operator takes different forms depending on the type it is operating upon. For example, the
+ operator can be used with integers, strings, and lists. Its behavior varies based on the
type it interacts with, thus exhibiting polymorphism. The following function can take
different forms depending upon the type of objects a and b.
def func(a, b):
print(a + b)

print(a * b)

It will work correctly for objects of any type that support addition and multiplication.

16.11 Abstract Base classes


We have seen that if we have to define a group of classes that have similar features and
show common behavior, we can define a base class and then inherit the classes from it. In
the derived classes, we have the choice to either use the base class version of a method or
override it. There can be scenarios when it does not make sense to implement some
methods in the base class. We need to define a method in the base class just to provide a
common interface for the derived classes. We do not need such a base class to be
instantiated.
In the following example we have defined a base class Shape from which different classes
like Rectangle, Triangle can be derived.
class Shape:

def area(self):

pass

def perimeter(self):

pass

def draw(self):

pass

class Rectangle(Shape):
def __init__(self, length, breadth):

[Link] = length

[Link] = breadth

def area(self):

return [Link] * [Link]

def perimeter(self):

return 2 * ([Link] + [Link])

def draw(self):

print('Drawing a rectangle')

class Triangle(Shape):

def __init__(self, s1, s2, s3):

self.s1 = s1

self.s2 = s2

self.s3 = s3

def area(self):
sp = (self.s1 + self.s2 + self.s3) / 2

return ( sp*(sp-self.s1)*(sp-self.s2)*(sp-self.s3) ) ** 0.5

def perimeter(self):

return self.s1 + self.s2 + self.s3

def draw(self):

print('Drawing a triangle')

The base class Shape has three methods, each with an empty body. Each derived class will
implement the area, perimeter and draw methods in its own way and so it will
override these methods. It is not possible to provide a definition for these methods in the
base class, since they will perform different operations depending on the type of object.
The definition of these methods in the base class provides a common interface for the
derived classes. With a common base class, your program becomes easier to extend. When
developers have to add new classes like Circle, Rhombus, they can inherit from the
Shape class and maintain a common interface.
We can create an instance of Shape class, but it does not make much sense. There is no use
of instantiating a Shape class, its purpose is just to serve as a base class for the other
classes.
Such types of classes that are meant to be inherited and not instantiated should be marked
as abstract base classes. An abstract base class generally represents a model or an abstract
concept – something that has no physical form; for example, Shape is an abstract concept,
while Rectangle and triangle represent real things. To mark a class as an abstract base
class, we have to make use of the abc module of the standard library.
from abc import ABC, abstractmethod

class Shape(ABC):

@abstractmethod
def area(self):

pass

@abstractmethod

def perimeter(self):

pass

@abstractmethod

def draw(self):

pass

The Shape class now inherits from the ABC class of abc module. The methods area,
perimeter, and draw are now decorated with the abstractmethod decorator, so they
are abstract methods. By making a method abstract we force all the derived classes to
implement that method. If the derived class does not implement an abstract method, then
there will be an error while instantiating that class.
To make a class an abstract class we have to inherit from the [Link] class and it should
have at least one abstract method in it. To make a method an abstract method we have to
apply the @abstractmethod decorator from the abc module. We cannot create any
instance object of an abstract class, and any class that inherits from an abstract class should
override all its abstract methods.
In our example, Shape class is an abstract class so it cannot be instantiated. The derived
classes that can be instantiated are called concrete classes. The abstract class provides a
sort of blueprint or a template for its subclasses. It defines the methods that the subclasses
should implement. An abstract class is not meant to be instantiated; it exists only to be used
as a base class that provides a basic foundation for the derived classes. Derived classes that
implement the abstract methods are concrete classes that represent the real things that are
modeled, and they can be instantiated.
We have seen in polymorphism that if objects have common method names, they can be
treated in the same general way. Abstract base classes provide a strict common interface
that has to be followed by the subclasses. They force the subclasses to use the same method
names for similar types of tasks, and hence, it becomes easier to maintain the class
hierarchies and achieve polymorphism.
In our next example program, Employee is an abstract class, while the classes
PartTimeEmployee, FullTimeEmployee and TemporaryEmployee are concrete
classes.
from abc import ABC, abstractmethod

class Employee(ABC):

def __init__(self, name, phone):

[Link] = name

[Link] = phone

def contact_details(self):

print([Link], [Link])

@abstractmethod

def compute_salary(self):

pass

class PartTimeEmployee(Employee):

def __init__(self, name, phone, hours):

super().__init__(name, phone)
[Link] = hours

def compute_salary(self):

print('Calculating salary of part time employee')

class FullTimeEmployee(Employee):

def compute_salary(self):

print('Calculating salary of full time employee')

class TemporaryEmployee(Employee):

def compute_salary(self):

print('Calculating salary of temporary employee')

e1 = PartTimeEmployee('Jack', 999909090, 4)

e2 = FullTimeEmployee('Jim', 989898989)

e3 = TemporaryEmployee('John', 789898989)

employees = [e1, e2, e3]

for e in employees:

e.contact_details()
e.compute_salary()

Since the method of computing the salary differs for each employee, there is no point in
implementing the method compute_salary in the base class. Every derived class should
override this method to give its own implementation. This is why it is marked as an
abstract method. Each subclass is expected to provide an implementation for
compute_salary method.
An abstract class can have non-abstract methods as well, which the derived classes do not
need to override. For example, in the Employee class, the method contact_details is
not an abstract method and so it is not necessary for the derived classes to override it. The
user has the choice to either use the base class implementation or override the method and
define its own implementation.
Most of the time, abstract methods have an empty body in the abstract class. They are there
only for defining the common interface, so their body generally contains a pass statement.
However, the abstract methods of an abstract class can contain some basic implementation
that the concrete subclasses can call by using super. Even if the abstract method is
implemented in the abstract base class, the subclass has to override it. The subclass can call
the base implementation by using super and then add its own code for any additional
tasks.
You can also declare property methods, class methods, or static methods as abstract:
@property

@abstractmethod

def name(self):

pass

@classmethod

@abstractmethod

def method1(cls):
pass

@staticmethod

@abstractmethod

def method2():

pass

There are many predefined abstract base classes available in the standard library. The
[Link], numbers, and io modules define abstract base classes that can be
inherited. When you want to define collections that share the same interface as that of
built-in types, you can inherit from one of the classes in [Link] module. This
module differs from the module abc that contains ABC and the abstractmethod
decorator.

16.12 Composition
We have seen how to reuse the code of an existing class by inheriting a class from it.
Another way to use the code of an existing class is composition (containership). Inheritance
and composition are two different design constructs or design concepts: inheritance is used
when you want to implement is-a relationship between classes while composition is used
when there is a has-a relationship between classes. For example, a car is-a vehicle but has-a
engine. An engine is not a kind of a Car but it is a part of a Car. There is a has-a relationship
between Car and Engine, so you have to use composition. When you use composition, you
embed one or more objects inside another object. So, we can make composite objects that
contain other objects called components; for example, a Car object can be viewed as a
composite object which has an engine, brakes, gears, etc. Let us see how we can achieve
composition in Python.
Till now, we have been using instance variables of built-in types in our classes. To
implement the concept of composition, we will make instance variables that refer to objects
of other user-defined classes. Whenever we want to use any attribute of the contained
class, we will have to use it through the instance. In the following program, we have made
the classes Engine and Brakes, and then inside the Car class we have instantiated these
classes. So, the Car class is the composite class, while the Engine and Brakes classes are
component classes.
class Engine:
def __init__(self,power):

[Link] = power

def start(self):

self.draw_current()

[Link]()

[Link]()

def draw_current(self):

print('Drawing current')

def spin(self):

print('Spinning')

def ignite(self):

print('Igniting')

class Brakes:

def __init__(self,weight):

[Link] = weight
def activate(self):

print('Activating brakes')

def release(self):

print('Releasing brakes')

class Car:

def __init__(self,name, engine, brakes):

[Link] = name

[Link] = engine

[Link] = brakes

def start(self):

[Link]()

def stop(self):

[Link]()

e = Engine(120)

b = Brakes(5)
car = Car('Breeze', e, b)

[Link]()

[Link]()

Output-
Drawing current

Spinning

Igniting

Activating brakes

In the __init__ of Car class, we have created two instance variables of type Engine and
Brakes, and used these instance variables inside the methods of the Car class. Through
these instance variables, we call the methods of the Engine and Brakes class and hence
get access to the implementation of these classes. When the Car object calls its start
method, the embedded Engine object calls its start method, in turn, and when the Car
object calls the stop method, the embedded Brakes object calls its activate method.
The composite class is the controller that passes calls to the contained objects.
Composition makes the class easier to understand and use. The composite class can focus
on the main task and can delegate different sub-tasks to the contained objects. So, each
class can focus on performing a specific task, instead of a single complex class performing
all the tasks. Composition also helps in reuse of code. You can use any class as a component
in different classes.
When your class becomes too lengthy with many instance variables and methods, you can
think of making a separate class for some of the parts of that class. Then you can include an
instance of that new class in your class. You can also make use of existing classes in your
class. For example, in the following class we have made use of the existing Person class
and [Link] class in our Book class. The [Link] class from the
standard library is used in the Person class also.
from datetime import date
class Person:

def __init__(self, name, y, m, d, address, phone):

[Link] = name

[Link] = address

self.date_of_birth = date(y, m, d)

[Link] = phone

def contact_details(self):

print([Link], [Link])

@property

def age(self):

return ([Link]() - self.date_of_birth).days // 365

class Book:

def __init__(self, title, pages, y, m, d, author):

[Link] = title

[Link] = pages
self.publishing_date = date(y, m, d)

[Link] = author

def display(self):

print(f'{[Link]} published in {self.publishing_date.year}, ', end='')

print(f'written by {[Link]}')

def author_details(self):

print(f'Author name : {[Link]}, age : {[Link]}')

[Link].contact_details()

def __lt__(self, other):

return (self.publishing_date) < (other.publishing_date)

author1 = Person('Devank', 2010, 4, 29, '122 Madhi Nath', 998998987)

author2 = Person('Devanshi', 1999, 5, 15, '256 Adyar', 878237288)

book1 = Book('Divine Dinosaurs', 200, 2020, 4, 29, author1)

book2 = Book('Rocket Science', 200, 2021, 4, 29, author1)

book3 = Book('How to overcome laziness', 500, 2010, 4, 29, author2)


books = [book1, book2, book3]

for book in books:

[Link]()

print()

print('List of books sorted by publishing date')

for book in sorted(books):

print([Link])

print()

print('List of books by young authors')

for book in books:

if [Link] < 18:

print([Link])

print()

print(f'Author details of "{[Link]}"')

book1.author_details()
Output-
Divine Dinosaurs published in 2020, written by Devank

Rocket Science published in 2021, written by Devank

How to overcome laziness published in 2010, written by Devanshi

List of books sorted by publishing date

How to overcome laziness

Divine Dinosaurs

Rocket Science

List of books by young authors

Divine Dinosaurs

Rocket Science

Author details of "Divine Dinosaurs"

Author name : Devank, age : 13

122 Madhi Nath 998998987

We have instantiated the Date class and the Person class in our Book class and used the
instances in the methods of the Book class.
Whenever you have to copy a composite object that contains other embedded objects, you
should perform a deep copy by using the deepcopy function from the copy module.

You might also like