Python is an “Object Oriented Programming language” or OOP for short. It is a programming paradigm which focuses on reusable pattern of code, in contrast to procedural programming, which focuses on explicit sequenced instructions. When working on complex programs, object oriented programming let you reuse code and write code that is more readable, which in turn makes it more maintainable.

1. Classes and objects

Class is kind of blueprint or a template for creating objects. Objects has variables and behaviour associated with them. In python a class is created by the keyword class, similar to how we define functions by using the def keyword.

An object is created using the constructor of the class. This object will then be called the instance of the class.

Instance = class(arguments)

2. Attributes and methods in class

A class is of no use if there is no functionality associated with it. Functionalities are defined by setting attributes, which act as containers for data and functions related to those attributes. Those functions are called methods.

For example a dog is a class and it has property like size, color etc. these are attributes of class dog and the behaviour or actions associated with class dog like running, eating, sleeping etc are methods of dog’s class.

Now lets create a simple class Dog 🐶 with no functionalities.


class Dog:
    pass

tommy = Dog() #Instance of class dog 
print(tommy)
## <__main__.Dog object at 0x0000000041AD0898>

Attributes :

You can define the following class with the name Dog. This class will have attributes age and name.

class Dog:
   age  =  7
   name = "Tommy"

You can assign class to a variable. This is called object instantiation. You will be able to access the attributes that are present inside the class using the dot operator.

For example in the Dog example, you can access the attributes age and name of the class Dog.

# instantiate the class Dog and assign it to variable tommy
tommy = Dog()

# access the class attribute age and name inside the class Dog
print(tommy.age)
## 7
print(tommy.name)
## Tommy

Methods

  • Once there are attributes that “belong” to the class, you can define functions (methods) that will access the class attributes.
  • When you define methods, you will need to always provide the first argument to the method with a self keyword. In this way the object gets passed to the methods.

class Dog:
    age = 7
    name = "Tommy"
    
    def change_age(self, new_age):
        self.age = new_age
    def change_name(self, new_name):
        self.name = new_name

Now instantiate this class Dog with a variable tommy.

tommy = Dog()
print(tommy.age)
## 7
print(tommy.name)
## Tommy

Here tommy object gets passed to the methods change_age and change_name because the keyword self was a parameter of the methods as defined in the Dog class. The self keyword ensures that the methods have a way of referring to object attributes.

#Change the age with the method change_age() and name by change_name().  

tommy.change_age(10)
print(tommy.age)
## 10
tommy.change_name("jacky")
print(tommy.name)
## jacky

👉 Methods are like functions the only difference is that they belong in a given class/object/instance.


3. Instance attribute

You can also provide the values for the attributes at runtime. This is done by defining the attributes inside the __init__ method also known as constructor method. It is run as soon as an object of a class is instantiated.

Let’s take an below example

class Dog:
    # constructor method  
    def __init__(self, age, name):
       self.age = age
       self.name = name
       
    # method change_age   
    def change_age(self, new_age):
        self.age = new_age
    
    # method change_name  
    def change_name(self, new_name):
        self.name = new_name

now you can directly define separate attribute values for separate objects. For example :-

tommy = Dog(7, "Tommy")
print(f"The age of dog is {tommy.age} and name is {tommy.name}")
## The age of dog is 7 and name is Tommy

tommy.change_age(10)
tommy.change_name("Jacky")

print(f"The changed age of dog is {tommy.age} and changed name is {tommy.name}")
## The changed age of dog is 10 and changed name is Jacky

Why self keyword required while initializing attributes ?

self is also an instance of class. Since instances of a class has varying values we could state Dog.name = name rather than self.name. But since not all dogs share the same name, we need to be able to assign different values to different instances. Hence the need for the special self variable, which will help to keep track of individual instances of each class.

👉 You will never have to call the __init__() method; it gets called automatically when you create a new ‘Dog’ instance.


4. Class attribute

Instance attributes are specific to each object, class objects are the same for all instances.

class Dog:
    #class attribute
    species = "mammal"  
    
    #Initializer/instance attribute or constructor method  
    def __init__(self,name, age): #name and age are instance attribute
        self.name = name   
        self.age  = age 

So while each dog has a unique name and age, every dog will be a mammal.


5. Object inheritance

  • Inheritance is the process by which one class takes on the attributes and methods of another.
  • Newly formed classes are called child classes, and the classes that child classes are derived from are called parent classes.
  • A child classes inherit all of the parent’s attributes and behaviors but can also specify different behavior to follow. (override or extend the functionality)

Lets take an example

Suppose we want to differentiate between different dogs we can choose breed for differentiation among different dogs.Since different breed of dogs behave differently.

# Parent class
class Dog:
    # Class attribute
    species  = 'mammal'
    
    # Initializer / Instance attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    
    # instance method
    def description(self):
        return print(f"{self.name} is {self.age} years old")
    # instance method
    def speak(self, sound):
        return print(f"{self.name} says {sound}")



# Child class (inherits from Dog class)       
class Bulldog(Dog):
    def run(self, speed):
        return print(f"{self.name} runs {speed}")
        
# Child class (inherits from Dog class)   
class Goldenretriever(Dog):
    def run(self, speed):
        return print(f"{self.name} runs {speed}")   
    

Let’s create an instance of bulldog class


# Child classes inherit attributes and behaviors from the parent class
jacky = Bulldog("Jacky", 10)
jacky.description()
## Jacky is 10 years old
jacky.speak("howwwww")
## Jacky says howwwww
jacky.run("slowly")

# Child classes have specific attributes  and behaviors as well
#print(jacky.run("slowly"))
## Jacky runs slowly

Let’s create an instance of Goldenretriever class

tommy = Goldenretriever("Tommy", 7)
tommy.description()
## Tommy is 7 years old
tommy.speak("bow-bow")
## Tommy says bow-bow
tommy.run("fast")
## Tommy runs fast

6. Parent vs child Class

class Dog:
    species = 'mammal'
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    #instance method
    def description(self):
        return print(f"{self.name} is {self.age} years old") 
        
    def speak(self, sound):
        return print(f"{self.name} says {sound}")


#child class
class Bulldog(Dog):
    def run(self, speed):
        return print(f"{self.name} runs {speed}")

class Goldenretriever(Dog):
    def run(self, speed):
        return print(f"{self.name} runs {speed}")
        
jacky = Bulldog("Jacky", 10)
jacky.description()
## Jacky is 10 years old
jacky.run("slowly")
## Jacky runs slowly
print(isinstance(jacky, Dog))
## True
julie = Dog("Julie", 12)
print(isinstance(julie, Dog))
## True
tommy = Goldenretriever("Tommy", 7)
print(isinstance(tommy, Bulldog))

      
## False

Both jacky and julie are instance of Dog() class, while tommy is not an instance of Bulldog() class.

👉 Objects can also have other objects that belong to them, each with their own methods and attributes.

bulldog.tail.wags()          #bulldog an instance of dog has another object tail having method wags() associated with it.  
bulldog.tail.type = "small"  #bulldog an instance of dog has another object tail which has type attribute small 
bulldog.head.mouth.teeth.canine.hurts() #bulldog has another object head which has another object mouth which has another object teeth which has another object canine which has method hurts().