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.
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:
- The arguments you define for
__init__
are the same arguments you use when you create a class instance. - 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:
__str__
: Controls how the class instance is printed__repr__
: Controls how the class instance is shown in interpreter__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.