# Object Oriented Programming (OOPS) in Python

Object oriented programming is an effective way of writing code. You create classes which are python objects, that represented meaningful entities which defines its own behaviour (via methods) and attributes. Let’s understand what a class is and the concepts behind Object Oriented Programming in Python

Everything you have encountered so far in Python, such as lists, dictionaries, etc are classes.

# check the type of various objects
print(type({}))
print(type(()))
print(type([]))
print(type(1))


Output:

<class ‘dict’>
<class ‘tuple’>
<class ‘list’>
<class ‘int’>

While these are in-built classes, Python allows you to create your own class as well. You can define your own custom methods and attributes that forms the ‘behaviour’ of the class.

## What exactly is a class in python?

User defined objects are created using the class keyword. You can think of class as a blueprint that defines the nature of an object. Inside a class you can define your attributes and your own methods (functions of a class).

Once you create a class, you can create several instances of it. All of which, will have all the functionalities you had defined for the class.

For example, let’s take a real world car. The car model “Toyota Corolla” has certain specs and functionalities which it’s designers created as a template. From that template, the company manufactures several instances of the car. Here, the design template of ‘Toyota Corolla’ can be considered as the ‘class’ and the numerous actual cars on the road are unique ‘instances’ of the class.

So basically, an instance is a specific object created from a particular class. Alright, let’s create one.

# Create a empty class
class Car:
pass

# Instance of example
car1 = Car()
car2 = Car()

car1


<main.Car at 0x1ac92ea5760>

# check type
print(type(car1))


<class ‘main.Car’>

Each instance is a different object.

# Check id's
print(id(car1))
print(id(car2))


1840710834016
1840710835648

The id’s of the instances will be different, because each is a different object.

Also notice the naming convention.

## Want to become awesome in ML?

Hi! I am Selva, and I am excited you are reading this!
You can now go from a complete beginner to a Data Science expert, with my end-to-end free Data Science training.
No shifting between multiple books and courses. Hop on to the most effective way to becoming the expert. (Includes downloadable notebooks, portfolio projects and exercises)

Start free with the first course 'Foundations of Machine Learning' - a well rounded orientation of what the field of ML is all about.

Enroll to the Foundations of ML Course (FREE)

Typically, the class name starts with Upper case (Car) and the instance starts with lower case (car). This is not a rule, but a naming convention developers follow for easier understanding.

## Attributes

An attribute is a value stored in an object, whereas, a method is an function we can perform with the object. Both can be accessed using the dot notation beside the name of the object.

The syntax for creating an attribute is:

self.attribute_name = value

Where self refers to the instance of the class you are creating. We create attributes this way so, you can access the attributes from anywhere within the class.

Let’s understand this more by creating a special method __init__(), also called the constructor method and define some attributes.

## The Constructor method: init()

Typically, every class in Python defines a special method called:

__init__()

This method acts as a constructor. Why is it called so?

Because it is called whenever a new instance of the class is created. You typically define all the attributes you want the instances of the class to carry in this method, so that every time a class instance is created, it contains these attributes.

So, basically it run every time you create an instance of the class.

What arguments does __init__ take?

It takes atleast one argument: self (which represents the class instance) and also can take additional arguments.

Since, the init is called at the time of creating a class instance, the argument you define with the init method, is passed at the time of initializing a class instance.

# Create a Car class and create an instance
class Car:
def __init__(self, make, model):
self.make = make
self.model = model

# Car instances
car1 = Car(make='Toyota', model="Corolla")


At the time of creating car1 the __init__() method is already run, so car1 will contain both the attributes: make and model.

Now, these two are attributes that will be characteristic of every Car, thereby, constructing the personality of the class object Car in the process.

car1.make, car1.model


#> (‘Toyota’, ‘Corolla’)

Couple of key points to note:

1. The arguments you define for __init__ are the same arguments you use when you create a class instance.
2. As a convention (not a rule), you define the class name starting with an upper case letter (Car) and the instances of the class will have similar names, but start with a lower case.

The upper case, helps developers understand that the object refers to a class object and you can create instances out of it.

## Dunder Methods aka Magic Methods

Dunder methods are special methods that you can define in a class, the governs certain special aspects of working with the class.

If you define these methods explicitly, you change something fundamental about the way this class behaves. For example: defining a __str__() will determine what gets printed out when you use print on the class instance.

Personally, the following three dunder methods are commonly defined.

Three important Dunder methods you need to know are:

1. __str__ : Controls how the class instance is printed
2. __repr__: Controls how the class instance is shown in interpreter
3. __call__: Controls what happens if a class instance is called.

For more detailed list, see the python documentation.

# Create a car class and define the dunder methods.
class Car:
def __init__(self, make, model):
self.make = make
self.model = model

def __str__(self):
"""Controls how the class instance is printed"""
return 'Make is ' + str(self.make) + ', Model is ' + str(self.model)

def __repr__(self):
"""Controls how the class instance is shown"""
return 'Make ' + str(self.make) + ', model: ' + str(self.model)

def __call__(self):
"""Controls what happens when the class inst is caller."""
print("Calling the function!")
return 'Make: ' + str(self.make) + ', Model: ' + str(self.model)

car1 = Car(make='Toyota', model="Corolla")
car2 = Car(make='Fiat', model="Punto")


Notice something interesting happening here.

The self.make and self.model are defined inside __init__() method. So, it should be accessible only by the __init__() inside it’s local name. They should et destroyed once __init__() has finished execution. Isn’t it? Then, how come they are accessible inside the other methods like __str__() etc?

This is possible via the self keyword.

By defining it as self.make instead of make, we are attaching the attribute to the class. And every time your define another method, you pass in this self as the first argument of those methods. See __str__, __repr__, __call__.

print(car1)


#> Make is Toyota, Model is Corolla

car1()


#> Calling the function!
#> ‘Make: Toyota, Model: Corolla’

Notice how the class receives the make (the same argument defined for __init__) as an argument.

Each attribute in a class definition begins with a reference to the instance object. It is by convention named self. The species is the argument.
self.model = model

It represents that the instance of the class itself.

In the above example we have two instants of the class

print(car1.make, car1.model)
print(car2.make, car2.model)


#> Toyota Corolla
#> Fiat Punto

## Methods – Defining your own functions associated with a class

Methods are functions defined inside the body of a class. They are used to perform operations with the attributes of our objects.

You can basically think of methods as functions that is attached to Object. This attachment is done by the self argument.

## How developers write classes practically?

When you start writing classes, define at a overall level, what all methods / logics you want the class to have. Leave it empty in the beginning, with only the docstring and pass.

Once you’ve planned it through, come back and fill in the logics.

# Create a car class and define the methods for future. Keep it empty for now.
class Car:
"""Define a class that represents a real life car."""
def __init__(self, make, model):
self.make = make
self.model = model
self.gear = 0
self.speed = 0

def start(self):
"""Start the vehicle on neutral gear"""
pass

def shift_up(self):
"""Increment gear and speed"""
pass

def shift_down(self):
"""Decrease gear and speed"""
pass

def accelerate(self):
"""Increase speed"""
pass

def check_speed_and_gear(self):
"""See the car speed"""

def stop(self):
"""Apply brakes and stop. Bring to neutral gear"""
pass

def start_drive(self):
"""Check if vehicle is in neutral, shiift up and drive."""
pass

def __str__(self):
"""Controls how the class instance is printed"""
return 'Make is ' + str(self.make) + ', Model is ' + str(self.model)

def __repr__(self):
"""Controls how the class instance is shown"""
return 'Make ' + str(self.make) + ', model: ' + str(self.model)

def __call__(self):
"""Controls what happens when the class inst is caller."""
print("Calling the function!")
return 'Make: ' + str(self.make) + ', Model: ' + str(self.model)


Now, we have a fair idea, define the logics via methods and attributes.

# Now start filling up the logics.
class Car:
"""Define a class that represents a real life car."""
def __init__(self, make, model):
self.make = make
self.model = model
self.gear = 0
self.speed = 0

def start(self):
"""Start the vehicle on neutral gear"""
if self.gear==0:
print("...VROOOOM....Started!")

def shift_up(self):
"""Increment gear and speed"""
self.gear += 1
self.speed += 5

def shift_down(self):
"""Decrease gear and speed"""
self.gear -= 1
self.speed -= 5

def accelerate(self):
"""Increase speed"""
self.speed += 5

def check_speed_and_gear(self):
"""See the car speed"""
print("I'm driving at:", self.speed, "in gear:", self.gear)

def stop(self):
"""Apply brakes and stop. Bring to neutral gear"""
self.speed = 0
self.gear = 0

def start_drive(self):
"""Check if vehicle is in neutral, shiift up and drive."""
if self.gear==0:
self.shift_up()
print("Shift Up and Drive.")
print("I am driving at ", self.speed, "mph")

def __str__(self):
"""Controls how the class instance is printed"""
return 'Make is ' + str(self.make) + ', Model is ' + str(self.model)

def __repr__(self):
"""Controls how the class instance is shown"""
return 'Make ' + str(self.make) + ', model: ' + str(self.model)

def __call__(self):
"""Controls what happens when the class inst is caller."""
print("Calling the function!")
return 'Make: ' + str(self.make) + ', Model: ' + str(self.model)


Initialize a car instance

car1 = Car(make='Toyota', model="Corolla")
car1


#> Make Toyota, model: Corolla

Start the car

# Start the car
car = Car(make="Toyota", model="Camry")

# Start driving
car.start()


#> …VROOOOM….Started!

Drive some

# Accelerate
car.accelerate()

# Shift up
car.shift_up()

# Accelerate
car.accelerate()

# Shift Up
car.shift_up()

# Check speed
car.check_speed_and_gear()


I’m driving at: 20 in gear: 2

Drive some more..

# Accelerate
car.accelerate()

# Accelerate
car.accelerate()

# Check speed
car.check_speed_and_gear()


#> I’m driving at: 30 in gear: 2

Drive even more

# Shift up
car.shift_up()

# Accelerate
car.accelerate()

# Shift up
car.shift_up()

# Check speed
car.check_speed_and_gear()


#> I’m driving at: 45 in gear: 4

Stop the car.

# shift down
car.shift_down()

# Stop
car.stop()

# Check speed
car.check_speed_and_gear()


#> I’m driving at: 0 in gear: 0

Hope the you are now clear with how to create a class, instantiate it, define constructors, dunder methods, regular methods and attributes. Now, let’s understand class inheritance.

## Class Inheritance

You can make classes inherit the properties of other classes, then you can extend it to give additional attributes and methods.

The new class that inherits from the parent class is called the child class.

Now, let’s make an SUV that is going to inherit the characteristics of a Car. To do that, just pass in the parent class name (Car in this case) inside the brackets.

class SUV(Car):
def __init__(self, make, model):
self.segment = "SUV"
super().__init__(make, model)
print("Init success!!")


Create an instance now.

suv = SUV(make="Honda", model="CRV")


#> Init success!!

Contains the newly created attribute

suv.segment


#> ‘SUV’

Also contains all the attributes and methods of a car.

Let’s take the car for a quick test drive. After all, SUV is also a car.

suv.start_drive()


#> Shift Up and Drive.
#> I am driving at 5 mph

Check speed

suv.check_speed_and_gear()


I’m driving at: 5 in gear: 1

Stop the SUV

suv.stop()
suv.check_speed_and_gear()


#> I’m driving at: 0 in gear: 0

## Overriding the methods of a parent class (super class)

You can over ride the methods of the parent class as well.

For example, for SUVs, when you accelerate, the speed increase by 10 instead of 5 as seen in cars.
In that case, just redefine the methods that need to be modified.

class SUV(Car):
def __init__(self, make, model):
self.segment = "SUV"
super().__init__(make, model)
print("Init success!!")

def accelerate(self):
self.speed += 10


Start and drive

suv = SUV(make="Honda", model="CRV")
suv.start_drive()
suv.check_speed_and_gear()


#> Init success!!
#> Shift Up and Drive.
#> I am driving at 5 mph
#> I’m driving at: 5 in gear: 1

Stop the car

suv.stop()
suv.check_speed_and_gear()


#> I’m driving at: 0 in gear: 0

The new logic did reflect for accelerate() method. As simple as that.

Usage in Machine Learning: A strong use-case of creating models is to design the machine learning models that you will later on learn, which has it’s own methods to read data, handle missing values, plots, training ML models, tuning, evaluation etc.

Course Preview