OOP stands for Object Oriented Programming. Like most modern programming languages, Python is also an object oriented language, in fact, it is a characteristic feature of the language. This might be a little tricky to understand for beginners, but I'll try my best. Buckle up! Because this is gonna be a bit challenging. If you are not confident enough, you can practice the previous lessons to get hold of the syntax.
Everything in Python is an object, and I mean EVERYTHING (almost). Now,
when you create a variable called fruit: str = "Apple"
, an
apple won't be produced in the computer, obviously. They are treated as
objects in the sense that they have certain attributes/properties or
qualities which describe that particular object. This is the core of
Python and is the underlying mechanism of everything you can do in Python.
Then, we have classes. Classes are blueprints which are like frameworks for different types of objects. If 'Car' is a class, it will have certain properties such as brand, model name, colour, seats, fuel capacity, mileage or range etc.. And every single car is an object; not "Mahindra Thar is an object", "every individual car is an object": if it is a Mahindra Thar, then it's brand will be Mahindra and model will be Thar. I hope you understand and this analogy will be used often in this course. Each car is an "instance" of the class Car. So, objects are instances of a particular class.
You might have noticed the word 'class' in result of the
type()
function:
>>> print(type(777))
< class 'int' >
Yes! 777
is an instance of the class int
. That's
what I meant by "everything in Python is an object". That "Apple" you
created was an object of type string; meaning all the data types are
classes! Even functions are objects of the class function()
.
Now let's see how we can create our own classes.
Classes are defined using the class
key word. Every class
must have the __init__
method in them. Speaking of methods,
objects have not only attributes, but also methods which are defined in
the respective class. Essentially, methods are functions specific to that
object; they act in some way on the object itself. If you convert 45 into
a string with str(45)
, that string function is a method of
class int. The .lower()
function is also a method, of type
str. (Note the dot notation of the method)
class Car: # A class with the name Car is defined
def __init__(self, brand: str, model: str, year: int, colour: str): #The parameters necessary to declare all the attributes
self.brand: str = brand #The attribute self.brand is declared and its value is set to the argument brand
self.model: str = model #The attribute self.model is declared and its value is set to the argument model
self.year: int = year #The attribute self.year is declared and its value is set to the argument year
self.colour: str = colour #The attribute self.colour is declared and its value is set to the argument colour
# Creating an instance of the class Car
my_car: Car = Car("Hyundai", "Venue", 2022, "White")
print(my_car)
print(type(my_car))
# As you might have seen in the type hint,
# 'Car' is now its own type!
# The object my_car is of the type Car
print(my_car.model)
# You can access specific attributes with the 'object.attribute' syntax
# You can also change attributes with this syntax
my_car.brand = "Rolls Royce"
my_car.model = "Spectre"
print(my_car.model)
Now, you must be asking, what the heck is self
? Self is the
parameter that refers to a particular instance of a class. In the above
example, 'Car' is a class and 'my_car' is an instance of that class, as
you must have understood by now. Every instance can have different values
for an attribute. E.g., neighbour_car
can have the brand
'Ford'. The self
parameter just tells the method or the
attribute to refer to that instance. When you printed my_car.model, the
my_car object was passed to the self parameter automatically and it
returned the model of my_car. If you had another car called neighbour_car,
and you asked for neighbour_car.model, it would return something like
Ford; because self would then become neighbour_car. A simple way to think
about it is that a specific object replaces the self
keyword.
You don't need to pass it when calling a method or accessing an attribute,
it is automatically passed. The first parameter is always self, and it is
just a convention to call it 'self'; you can call it anything but I
recommend not to.
Apart from attributes, classes can also have methods, which are just
class-specific functions. The __init__()
function is also a
method which runs when an instance of the class is created or initialised.
Let's see an example:
from datetime import datetime
# I will not include the complete context every time
# The following function should be placed under the class after __init__()
def age(self) -> int:
return datetime.now().year - self.year
# Gets the current year and subtracts the purchase year of the car
print(my_car.age(), "years")
We just created a method called age() which returns the age of the car in years. Here is another example to make you understand the concept:
# A Fruit class with an __init__() method
# and a price() method
class Fruit:
def __init__(self, name: str, calories: int, rate: float) -> None:
self.name: str = name
self.calories: int = calories
self.rate: float = rate
def price(self, weight) -> int:
return self.rate * weight # Returns weight times rate
apple: Fruit = Fruit("Apple", 530, 150.00)
banana: Fruit = Fruit("Banana", 800, 70.00)
print("₹", apple.price(2)) # Price of 2 kilos of apples
print("₹", banana.price(5)) # Price of 5 kilos of apples
There are special methods called as 'dunder methods' which is short for
'Double Underscore methods'. They are also called as magic methods in
Python because they provide most of the functionality to the language.
There are numerous dunder methods which you can look up on the Internet.
You will be surprised to know even basic arithmetic operations like
addition and multiplication are also dunder methods. Integer and float
objects have a method called __add__
which called when the +
operator is used. Here is an example:
# A money class that has attributes amount and currency
class Money:
def __init__(self, amount: float, currency: str):
self.amount: float = amount
self.currency: str = currency
# Checks whether the currency is the same and returns a new Money object
def __add__(self, other):
if self.currency != other.currency:
raise ValueError("Can't add amounts with different currencies.")
# Oh yes, you can raise exceptions on-demand!
return Money(self.amount + other.amount, self.currency)
balance: Money = Money(35000, "INR")
deposit: Money = Money(6000, "INR")
balance = balance + deposit
# The + operator calls the __add__ method
# The object to the right is passed as the second argument, 'other'
print(balance.amount)
# You can directly call the add method as well
deposit: Money = Money(2500, "INR")
balance = balance.__add__(deposit)
print(balance.amount)
# Create another object of a different currency and try to use the + operator
One of the most commonly used dunder method is the __str__() method. Without this, when you print a custom object, it just says that it is an object of a certain type; but nothing useful. This method/function is manually defined to output a user-friendly text. This method is automatically called when you print the object or convert it into text. For the Car class used earlier, this can be a string method:
def __str__(self):
return f"A/An {self.colour} {self.brand} {self.model}, purchased in {self.year}."
# You can now print the car objects or convert them to text
Just like properties are inherited from generation to generation in real life, attributes and methods can be inherited from one class to another. More specifically, we can create what are 'child classes' which have the attributes and methods of the parent class, as well as some of their own. And example will clarify it:
class Person:
def __init__(self, name: str, age: int):
self.name: str = name
self.age: int = age
def introduce(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")
class Student(Person): # The Person class is passed as a parameter to the Student class
# The Student class inherits the attributes and methods of the Person class
def __init__(self, name: str, age: int, standard: int, roll_no: int):
super().__init__(name, age) # Calls the __init__ method of the Person class
self.standard: int = standard # The Student class specific attribute
self.roll_no: int = roll_no
def introduce(self): # This method overrides the introduce() method of the Person class
print(f"Hello, my name is {self.name}. I am {self.age} years old and I am studying in standard {self.standard}")
class Teacher(Person):
def __init__(self, name: str, age: int, subject: str):
super().__init__(name, age)
self.subject: str = subject
# Though the introduce() method is not defined here, it is inherited and can be called
def teach(self): # A method unique to the Teacher class
return f"[Teaching {self.subject}...]"
In the above example, we first created a Person class with attributes
name
& age
and a method
introduce()
. We then created two child classes, Student and
Teacher. Both the Student and the Teacher are a Person and they inherit
the attributes and methods of the parent class, Person. When we defined
the __init__()
method of the child class and provided some
parameters, the parameters of the parent class were actually overridden.
But instead of declaring the attributes again, we use the
super()
function to refer to the parent class and call its
initialisation method. After that, we can declare the child class-specific
attributes or methods. Similar to attributes, methods will also be
overridden if defined again. Let us create some objects.
me: Person = Person("S S Shetty", 16)
rutherford: Student = Student("Ernest Rutherford", 154, 13, 001)
thomson: Teacher = Teacher("J J Thomson", 169, "Physics")
me.introduce()
rutherford.introduce()
thomson.introduce()
print(thomson.teach())
Try and analyse the output and figure out which methods are being called. Also, find out the types of the three objects to see how they created
from dataclasses import dataclass
@dataclass
class Car:
brand: str
model: str
year: int
colour: str
my_car: Car = Car("Hyundai", "Venue", 2022, "White")
print(my_car)
That's it! The above code is equivalent to the Car class we created
earlier, but with less boilerplate code. And some dunder methods (like
__str__
, __eq__
, etc.)are automatically
created. Although you can still define other methods, it is not generally
done.