Object-Oriented Programming (OOP) in Python
Tags: webdev, tutorial, python, beginners
Published: 2024-01-22 17:15:05

## Introduction to Object-Oriented Programming (OOP) 

Object-oriented programming(OOP) is a [programming paradigm](https://en.wikipedia.org/wiki/Programming_paradigm) that organizes code that mimics real-world entities by using `objects` and `classes`


In this article, you will delve into the principles of object-oriented programming, learning about the concept of:


1. <a href="#class_Objects">Classes and objects</a>
 Introduction to classes and objects and how they contribute to building flexible and scalable code

2. <a href="#Inheritance_Polymorphism">Inheritance and Polymorphism:</a>
 Understand how inheritance and polymorphism contribute to building flexible and scalable code.

3. <a href="#Encapsulation_Abstraction">Encapsulation and Abstraction:</a>
Explore the concepts of encapsulation and abstraction for effective code organization.

5. <a href="#creating_OOP">How Python Supports OOP Principles:</a>
Learn how Python facilitates the implementation of fundamental OOP principles.


<h2 id= 'class_Objects'>Class and objects(instances)</h2>

<h3>Class</h3>
In programming, a class allows you to create a user-defined data structure. Classes define a set of attributes and functions called methods, identifying the behaviors and actions an object created from the class can perform with its data.

Conceptually, think of a class as a blueprint or a template for creating objects, for example, an object could represent a Car with attributes like make, model, year, and method: "start engine".

In Python, you define a class by using the class keyword followed by a name and a colon. Then you use the `__init__()` to declare which attributes each instance(object) of the class should have:

```
class Car:
    def __init__(self,make,model, year):
        self.make = make
        self.model = model
        self.year = year
    def start_engine(self):
        print(f"The {self.make} {self.model}'s engine is running.")
```
 You can give the `__init__` method any number of parameters, but the first parameter will always be a [variable](https://www.w3schools.com/python/python_variables.asp) `self`. When you create a new class instance, the python automatically passes the instance to the `self` parameter in `__init__()` so that python can define the new attributes on the object.

<h3>Objects(instances)</h3>

Creating a new object from a class(blueprint) is called `instantiating` a class. You create an object by typing the name of the class, followed by opening and closing parentheses:

```
Class Car:
    pass

Class() # calling a class or instantiating
```
_You first create a new Car class with no attributes or methods, and then you instantiate the Car class to create a Car object._

```
# Creating instances (objects) of the Car class
car1 = Car("Toyota", "Camry", 2022)
car2 = Car("Honda", "Accord", 2021)

```

_In this example, car1 and car2 are objects (instances) of the Car class. Each object has its values for the attributes make, model, and year._ You can call the start_engine method on each object:

```
car1.start_engine()  # Output: The Toyota Camry's engine is running.
car2.start_engine()  # Output: The Honda Accord's engine is running.
```

<h2 id= 'Inheritance_Polymorphism'>Inheritance and Polymorphism</h2>

<h3>Inheritance</h3>

Inheritance is the process by which one class takes on the attributes and methods of an existing class. The existing class is often referred to as the `parent` or the `base` class, and the new class is the `child` or `derived` class. 

You inherit from the parent class by creating a new class and putting the name of the parent class in parentheses: 

```

class Animal:
    def __init__(self, species):
        self.species = species

    def make_sound(self):
        print("Some generic animal sound.")

# Dog inherits from Animal
class Dog(Animal):
    def __init__(self, breed):
        # Call the constructor of the parent class (Animal)
        super().__init__("Dog")
        self.breed = breed

    # Override the make_sound method
    def make_sound(self):
        print("Woof! Woof!")

# Cat inherits from Animal
class Cat(Animal):
    def __init__(self, breed):
        super().__init__("Cat")
        self.breed = breed

    def make_sound(self):
        print("Meow!")

    
```
_   In this example, Dog and Cat are derived classes that inherit from the base class Animal. They reuse the species attribute from Animal and provide their implementation for the make_sound method. Instances of Dog and Cat have access to both their attributes/methods and those inherited from Animal._

<h3>Polymorphism</h3>

Polymorphism is a method in an Object-oriented programming language that does different things depending on the class of the object that calls it.

For example, `animal_sounds(dog)` will return the sound`woof! woof!`, but `animal_sounds(cat)` might return the sound `Meow`. The method animal_sounds will return the sound according to which class object was called on.

```
class Animal:
    def make_sound(self):
        print("Some generic animal sound.")

class Dog(Animal):
    def make_sound(self):
        print("Woof! Woof!")

class Cat(Animal):
    def make_sound(self):
        print("Meow!")

# Using runtime polymorphism
def animal_sounds(animal):
    animal.make_sound()

# Creating instances of Dog and Cat
dog = Dog()
cat = Cat()

# Calls the make_sound method of the respective class
animal_sounds(dog)  # Output: Woof! Woof!
animal_sounds(cat)  # Output: Meow!
```
_In this example, the Animal class has a method make_sound, and both Dog and Cat classes override this method with their specific implementations. The animal_sounds function takes an Animal object as a parameter, but during runtime, the correct make_sound method is called based on the actual type of the object passed._

<h2 id= 'Encapsulation_Abstraction'>Encapsulation and Abstraction</h2>

<h3>Encapsulation</h3>

Encapsulation is one of the fundamental principles of Object-oriented programming. it describes the idea of bundling data(attributes) and methods(function) into one single unit. This puts restrictions on accessing variables and methods directly and can prevent the accidental modification of data. A class is an example of encapsulation as it encapsulates all the data that is member functions, variables, etc.

 

```
class Employee:
    def __init__(self, emp_id, name, salary):
        self.__emp_id = emp_id   # Private attribute
        self.__name = name       # Private attribute
        self.__salary = salary   # Private attribute

    # Getter methods to access private attributes
    def get_emp_id(self):
        return self.__emp_id

    def get_name(self):
        return self.__name

    def get_salary(self):
        return self.__salary

    # Setter method to modify the salary (with validation)
    def set_salary(self, new_salary):
        if new_salary > 0:
            self.__salary = new_salary
        else:
            print("Invalid salary value. Salary must be greater than 0.")

# Creating instances of the Employee class
employee1 = Employee(101, "John Doe", 50000)
employee2 = Employee(102, "Jane Smith", 60000)

# Accessing information using encapsulation
print("Employee 1 ID:", employee1.get_emp_id())
print("Employee 2 Name:", employee2.get_name())

# Attempting to directly access private attributes (will result in an error)
# print(employee1.__name)  # This line will produce an AttributeError

# Modifying salary using encapsulation (setter method)
employee1.set_salary(55000)
print("Updated Employee 1 Salary:", employee1.get_salary())

```

_In this example, the Employee class encapsulates employee information with private attributes `(__emp_id, __name, and __salary).` The use of double underscores makes these attributes private, restricting direct access from outside the class._

Getter methods `(get_emp_id, get_name, and get_salary) `provide controlled access to the private attributes. This encapsulation ensures that external code cannot directly modify or access these attributes, promoting data integrity and preventing unintended changes.

_The `set_salary` method acts as a setter, allowing controlled modification of the salary attribute with validation. This encapsulated approach ensures that salary changes adhere to specific rules (in this case, the salary must be greater than 0)._

<h3>Abstraction</h3>
Abstraction in Python is a programming concept that hides complex implementation details while exposing only essential information and functionalities to users.

Abstraction allows you to define a clear and concise interface that exposes only the functionalities relevant to the user
                    
An abstract class is a class in which one or more abstract methods are defined. When a method is declared inside the class without its implementation is known as an abstract method.

To create abstract method and abstract classes we have to import the “ABC” and `“abstractmethod”` classes from` abc (Abstract Base Class)` library
```
from abc import ABC, abstractmethod

# Abstract class representing a Vehicle
class Vehicle(ABC):
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model

    @abstractmethod
    def start_engine(self):
        pass

# Concrete class representing a Car
class Car(Vehicle):
    def start_engine(self):
        return f"The {self.brand} {self.model}'s engine is running."

# Concrete class representing a Motorcycle
class Motorcycle(Vehicle):
    def start_engine(self):
        return f"The {self.brand} {self.model}'s engine is revving."

# Using abstraction to interact with different types of vehicles
def operate_vehicle(vehicle):
    return vehicle.start_engine()

# Creating instances of Car and Motorcycle
my_car = Car("Toyota", "Camry")
my_motorcycle = Motorcycle("Harley-Davidson", "Sportster")

# Using abstraction to start the engines
print(operate_vehicle(my_car))        # Output: The Toyota Camry's engine is running.
print(operate_vehicle(my_motorcycle))  # Output: The Harley-Davidson Sportster's engine is revving.

```
In the example above:

- The `Vehicle class` serves as an `abstraction`, defining a common interface for all vehicles. It includes attributes like `brand` and `model` that are common to all vehicles, and an abstract method `start_engine`.
- The abstract method `start_engine` enforces that each concrete subclass must provide its implementation of starting the engine.

- The `Car` and `Motorcycle` classes inherit from the `abstract Vehicle class` and provide specific implementations for the `start_engine method`.
- Each concrete class `encapsulates` the details specific to its type of vehicle.

- The operate_vehicle function utilizes `abstraction` by accepting any object that adheres to the Vehicle interface. This function doesn't need to know the specific details of each vehicle type; it simply calls the `start_engine` method on the provided object.

<h2 id= 'creating_OOP'>How Python Supports OOP Principles:</h2>


Python is a powerful and flexible programming language that strongly supports the principles of Object-Oriented Programming (OOP). The language's design facilitates the implementation of key OOP concepts, making it an excellent choice for developers who want to create modular, reusable, and maintainable code

Classes and Objects:

- Easily define classes with attributes and methods for modeling real-world entities.

Inheritance and Polymorphism:

- Inheritance allows classes to inherit attributes and methods.
- Polymorphism is inherent, enabling interchangeable use of objects.

Encapsulation and Abstraction:

- Encapsulation is achieved through private attributes and methods.
- Abstraction is supported with abstract base classes (ABCs) for clear interfaces.