Object Oriented Programming in Python
Tags: python, beginners
Published: 2023-02-16 06:48:16

## What is Object Oriented Programming?
Object-oriented programming(OOP) is a way of writing computer programs that revolves around the concept of objects. An object is a combination of data and the functions that operate on that data.

For instance, an object could represent a person with attributes like a name, age, emailId, address and behaviors like walking, breathing, talking and running.

By using OOP, we can create modular, reusable code that is easier to maintain and extend.

## How to define a Class:
The beginning of a class definition requires the use of the "class" keyword followed by the class name and a colon. Any code that is inside the indented block of this class definition is considered to be a part of the class's body.

Here's an example of Person class.
```
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
```

The \__init\__() method is a special method that gets called when a new instance of the class is created, and here it sets the initial values for the name and age attributes.
'self' is a reference to the instance of the class that is being created when the \__init\__ method is called.

To create an instance of the 'Person' class, you can simply call a function and pass in the necessary arguments:
```
person = Person('Alice', 25)
```
This will create a new Person object with the name "Alice" and age 25.

---
## Class and Instance Attributes:

In Python, a class attribute is a variable that is associated with a class, whereas an instance attribute is a variable that is associated with an instance of the class. Here are some examples of how to define and use class and instance attributes in Python:
```
class Person:
    species = "human"
   
    def __init__(self, name, age):
        self.name = name
        self.age = age
```
In this example, 
- 'species' is a class attribute of the Person class, which is associated with the class itself, not with any particular instance of the class. 
- The value of species is shared by all instances of the Person class. 
- We can access the class attribute using the class name and instance attributes using the instance variable. 

```
print(Person.species)  # "human"

person = Person("Alice", 25)
print(person.name) # "Alice"
print(person.age) # 25
```

### Types of methods
- **Instance Methods:** Instance methods are the most common type of method in Python. They are associated with an instance of the class and can access and modify the instance's state.

- **Class Methods:** Class methods are bound to the class rather than the instance. They can be used to modify the class state or perform operations that are not specific to any instance of the class. 

- **Static Methods:** Static methods do not depend on the class or instance state. They are often used for utility functions that do not modify the class or instance state
```
class Person:
    num_people = 0
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        Person.num_people += 1
    
    def say_hello(self):
        print(f"Hello, my name is {self.name}.")
    
    @classmethod
    def num_instances(cls):
        print(f"{cls.num_people} People instances are created.")
    
    @staticmethod
    def is_adult(age):
        return age >= 18
```
In this example,
- The class variable 'num_people' keeps track of the total number of Person objects that have been created.
- The \__init\__() method sets the name and age attributes for each Person object and increments the num_people class variable.
- The say_hello() method is an instance method that prints a message that includes the person's name.
- The num_instances() method is a class method that prints the total number of Person objects that have been created.
- The is_adult() method is a static method that takes an age argument and returns a Boolean value indicating whether the person is an adult.
```
person1 = Person("Alice", 30)
person2 = Person("Bob", 25)

person1.say_hello()  # Output: "Hello, my name is Alice."
person2.say_hello()  # Output: "Hello, my name is Bob."

Person.num_instances()  # Output: "2 People instances are created."

print(Person.is_adult(person1.age))  # Output: True
print(Person.is_adult(person2.age))  # Output: True
print(Person.is_adult(16))  # Output: False
```

---
## Inheritance: 
Inheritance allows one class to inherit properties and methods from another class. In Python, we can create a subclass by specifying the parent class in parentheses after the subclass name. 
### Types of Inheritance:
- **Single inheritance:** A subclass inherits from superclass.
```
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

class Student(Person):
    def __init__(self, name, age, id):
        super().__init__(name, age)
        self.id = id

student = Student("Alice", 20, "12345")

print(student.name)        # Output: Alice
print(student.age)         # Output: 20
print(student.student_id)  # Output: 12345
```
In this example, the Student class inherits from the Person class, so it gets access to the name and age attributes defined in the Person class.

- **Multiple inheritance:** A subclass inherits from multiple superclasses.
```
class A:
    def dothis(self):
        print('do this in A')

class B(A):
    pass

class C:
    def dothis(self):
        print('do this in C')

class D(B, C):
    pass

d_instance = D()
d_instance.dothis()  #do this in A
```

In this example, the 'D' class inherits both the 'B' and 'C' classes, allowing it to access their attributes and methods.

- **Multilevel inheritance:** A subclass inherits from a superclass, which inherits from another superclass.
```
class Grandparent:
    def __init__(self, name):
        self.name = name

class Parent(Grandparent):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

class Child(Parent):
    def __init__(self, name, age, grade):
        super().__init__(name, age)
        self.grade = grade

child = Child("Alice", 25, "A")
print(child.name)  # Alice
print(child.age)   # 25
print(child.grade) # A
```

Here, the Child class inherits from the Parent class, which in inherits from the Grandparent class.

- **Hierarchical inheritance:** Multiple subclasses inherit from a single superclass.
```
class Teacher(Person):
    def __init__(self, name, age, subject):
        super().__init__(name, age)
        self.subject = subject

class Principal(Person):
    def __init__(self, name, age, school_name):
        super().__init__(name, age)
        self.school_name = school_name
```
Here, both the Teacher and Principal classes inherit from the Person class.

- **Hybrid inheritance:** A combination of two or more types of inheritance.
```
class A:
    def method(self):
        print("Method of class A")

class B(A):
    pass

class C(A):
    def method(self):
        print("Method of class C")

class D(B, C):
    pass
```
Here, the D class uses multiple inheritance to inherit from both the B and C classes, which in turn inherit from the A class.

---
## Encapsulation:
It is a mechanism that allows us to restrict access to an object's data and methods. When a class member is marked as private, it can only be accessed from within the class. A member can be marked as private by prefixing its name with two underscores.
```
class Book:
    def __init__(self, title, author, price):
        self.title = title
        self.author = author
        self.price = price
        self.__discount = 0.10

book1 = Book('Book 1', 12, 'Author 1', 120)

print(book1.title)     # Book 1
print(book1.author)    # Author 1
print(book1.price)     # 120
print(book1.__discount)# AttributeError: 'Book' object has no attribute '__discount'
```
We cannot access the '__discount' attribute as it is a private attribute. To access private attributes, we use getter and setter methods.

```
class Book:
    def __init__(self, title, author, price):
        self.title = title
        self.author = author
        self.price = price
        self.__discount = None

    def set_discount(self, discount):
        self.__discount = discount

    def get_discount(self)
        return self.__discount

book = Book('Book 1', 'Author 1', 200)
book.set_discount(0.10)
print(book.get_discount())    # 0.10
```

---
## Polymorphism:
Polymorphism refers to having many forms. In OOP, it refers to the functions having same names but does different things.
```
class Animal:
    def __init__(self, name):
        self.name = name
    
    def eat(self):
        print('eating')

class Dog(Animal):
    def show_affection(self):
        print(f'{self.name} wags tail')

class Cat(Animal):
    def show_affection(self):
        print(f'{self.name} purrs')


dog = Dog('Rover')
cat = Cat('Fluffy')

dog.show_affection() # Rover wags tail
cat.show_affection() # Fluffy purrs
```

---
## Data Abstraction:
Data Abstraction allows us to hide the internal implementations of a function and expose only its functionalities.
To create an abstraction class, first, we need to import the ABC class from abc module. ABC is an acronym for Abstract Base Class.
```
from abc import ABC, abstractmethod

class Vehicle(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def info(self):
        pass

vehicle = Vehicle("Honda") # TypeError: Can't instantiate abstract class Car with abstract method info
```
The reason we get the TypeError is we have not implemented the abstract method. Abstract methods are implemented in the child classes.
```
class Car(Vehicle):
    def info(self):
        print(f"{self.name} information")

car = Car("Honda")
car.info()    # Honda information.
```

---
## References:
- [All About OOPs Concepts in Python Programming](https://www.analyticsvidhya.com/blog/2020/09/object-oriented-programming/)
- [Object Oriented Python, TutorialsPoint](https://www.tutorialspoint.com/object_oriented_python/object_oriented_python_tutorial.pdf)
- [Object-Oriented Programming in Python](https://www.freecodecamp.org/news/object-oriented-programming-in-python/)
- [OOP in Python3](https://realpython.com/python3-object-oriented-programming/)