The Essence of Objects (Part II): A Deeper Dive into Python’s OOP Paradigm

Ayoub_Ali
Level Up Coding
Published in
7 min readJun 28, 2023

--

This blog is the second part of a two-part comprehensive guide on object-oriented programming (OOP) in Python. In the first part, The Essence of Objects (Part I): Unlocking Python’s OOP Paradigm, we covered essential concepts such as classes, objects, attributes, methods, and object variables. Now, we will continue our exploration deeper and delve into additional key aspects of OOP such as overriding methods, encapsulation, abstraction, inheritance, and interfaces.

Encapsulation: Safeguarding Object Data

The concept of encapsulation refers to the ability to hide the internal data of an object, safeguarding it from unwanted modifications. This protective measure prevents unintended side effects and promotes a more reliable and secure code. It involves bundling the data and related methods together within a class, allowing access to the data only through designated interfaces.

In Python, encapsulation is achieved using access modifiers. Access modifiers are keywords that define the level of visibility and accessibility of object attributes and methods. The three commonly used access modifiers in Python are public, private, and protected.

  • publicattributes and methods are accessible from anywhere, both within and outside the class. They are typically defined without any access modifiers, and their names follow standard naming conventions.
  • privateattributes and methods are indicated by prefixing them with double underscores (e.g., “__attribute”). These attributes and methods are intended to be accessed only within the class itself. Python enforces name mangling to make private attributes and methods less accessible outside the class, but they can still be accessed if the correct syntax is used.
  • protectedattributes and methods are designated by prefixing them with a single underscore (e.g., “_attribute”). While they are not entirely private, they are considered a convention that indicates that the attribute or method should be treated as internal to the class or its subclasses. Python does not enforce any restrictions on accessing protected members, but it is considered a best practice to treat them as non-public.

Let’s consider an Employeeclass as an example:

class Employee:
def __init__(self, name, salary):
self._name = name # Protected attribute
self.__salary = salary # Private attribute
    def _calculate_bonus(self):
return self.__salary * 0.1 # Protected method
def __display_salary(self):
print("Salary:", self.__salary) # Private method
def get_name(self):
return self._name # Public method
def set_salary(self, new_salary):
self.__salary = new_salary # Public method

In the example, we have an Employee class with attributes _name (protected) and __salary (private). The class also has methods _calculate_bonus (protected), __display_salary (private), andget_name and set_salary are public methods.

By encapsulating data and using access modifiers effectively, we can ensure that the internal state of an object is protected from unintended modifications. This promotes data integrity, code reliability, and security by preventing external entities from directly manipulating or altering critical object data. Encapsulation also allows for better code organization, as the internal workings of an object are hidden, and only the necessary interfaces are exposed for interaction.

Inheritance: Extending Classes and Reusability

It allows a derived class (subclass) to inherit the attributes and behaviors of a base class (superclass). The derived class establishes an “is-a” relationship with the base class, acquiring its properties while having the flexibility to add additional attributes and behaviors.

By inheriting from a base class, the derived class gains access to all the public and protected attributes and methods of the base class. This inheritance mechanism promotes code reuse, as the derived class can utilize and build upon the existing functionality provided by the base class.

In Python, inheritance is achieved by specifying the base class in the declaration of the derived class. The derived class then has the ability to override inherited methods or add new attributes and methods specific to its needs. This flexibility allows for the customization and extension of behavior while maintaining the core functionality inherited from the base class.

Let’s consider the following example where the Carclass and Motorcycleclass are subclasses of the superclass Vechile :

class Vehicle:
def __init__(self, brand, color):
self.brand = brand # Set the brand attribute to the provided brand parameter
self.color = color # Set the color attribute to the provided color parameter
def drive(self):
print(f"The {self.color} {self.brand} is now driving.")
# Print a message indicating that the vehicle is driving
 
class Car(Vehicle):
def __init__(self, brand, color, num_doors):
super().__init__(brand, color)
# Call the parent class constructor to initialize brand and color attributes
self.num_doors = num_doors
# Set the num_doors attribute of the car to the provided num_doors parameter

def honk(self):
print("Beep beep!")
# Print a message indicating that the car is honking

class Motorcycle(Vehicle):
def __init__(self, brand, color):
super().__init__(brand, color)
# Call the parent class constructor to initialize brand and color attributes

def wheelie(self):
print("Performing a wheelie")
# Print a message indicating that the motorcycle is performing a wheelie

In the example above, the superclass Vehicle has attributes brand and color, as well as a method drive(). The subclass Car adds an additional attribute num_doors and a method honk(). While the subclassMotorcycle does not add any new attributes but adds the method wheelie().

Abstraction and Polymorphism

Abstraction: Hiding Complexity with Simplicity

Abstraction is a concept that allows us to hide complex implementation details and provide a simplified interface for interacting with objects. It involves creating a “black box” around an object, where users can interact with it based on inputs and expected outputs, without needing to understand the intricate inner workings.

For example, let’s consider a car object. From a user’s perspective, interacting with a car involves basic actions such as starting the engine, accelerating, and braking. The user doesn’t need to understand the complex mechanisms involved in the engine combustion process or the transmission system. The internal details of the car are abstracted away, allowing users to focus on high-level actions and behaviors.

Polymorphism: The Power of Many Forms

Polymorphism allows objects of different classes to be treated as objects of a common base class, enabling flexibility and extensibility in code.
It enables us to write generic code that can operate on a wide range of objects, regardless of their specific implementations. Polymorphism in Python can be achieved through method overriding and interfaces.

Method overriding in Python allows a subclass to provide a different implementation of a method that is already defined in its superclass. By declaring a method with the same name in the subclass, we can customize the behavior of that method for objects of the subclass. This means that even though objects may belong to different classes, if they share a common base class and have overridden methods, they can be invoked using the same method name.

Interfaces: Enforcing Abstraction and Polymorphism

In object-oriented programming, abstraction is achieved through the use of classes and interfaces. While classes define the properties and behaviors of objects, interfaces provide a contract specifying the methods that must be implemented by a class. Also, interfaces provide a way to implement polymorphism by allowing multiple classes to implement the same interface, even if they have different internal implementations.

In Python, interfaces are not explicitly defined as a language construct like in some other programming languages (e.g., Java). However, the concept of interfaces can still be achieved through a combination of class inheritance and method signatures. Let’s consider an example to demonstrate how interfaces can be implemented in Python:

from abc import ABC, abstractmethod

# Define the Car interface using an abstract base class
class Car(ABC):
@abstractmethod
def start_engine(self):
pass

@abstractmethod
def drive(self, distance):
pass
# Implementing classes that adhere to the Car interface
class Sedan(Car):
def start_engine(self):
print("Starting the sedan's engine.")

def drive(self, distance):
print(f"Driving the sedan for {distance} kilometers.")

class SUV(Car):
def start_engine(self):
print("Starting the SUV's engine.")

def drive(self, distance):
print(f"Driving the SUV for {distance} kilometers.")
# Create objects of the implementing classes
sedan = Sedan()
suv = SUV()

# Accessing the interface methods
sedan.start_engine() # Output: Starting the sedan's engine.
sedan.drive(100) # Output: Driving the sedan for 100 kilometers.
suv.start_engine() # Output: Starting the SUV's engine.
suv.drive(150) # Output: Driving the SUV for 150 kilometers.

In the example, we define a Car interface using an abstract base class, ABC, from the abc module. The Car interface declares two abstract methods: start_engine() and drive(distance). Any class that wants to conform to the Car interface must implement these methods.

We then create two implementing classes: Sedan and SUV. Both classes inherit from the Car interface and provide concrete implementations of the start_engine() and drive(distance) methods.

By creating objects of the implementing classes (sedan and suv), we can access the interface methods start_engine() and drive(distance), regardless of the specific class type. This demonstrates how we can achieve a similar concept of interfaces in Python through class inheritance and method signatures.

Note that the abc module and the abstractmethod decorator are used to define abstract methods and enforce their implementation in the derived classes. The abstract base class ABC acts as the marker for the abstract class.

--

--