whenever life put's you in a tough situtation, never say why me! but, try me!

OOP: Encapsulation and Abstraction

Encapsulation and abstraction are two foundational concepts in Object-Oriented Programming (OOP) that help make code more modular, maintainable, and secure. In this module, we will explore these concepts in detail, focusing on how encapsulation restricts access to certain parts of an object, and how abstraction simplifies complex systems by hiding implementation details.


Subtopic 1: Understanding Encapsulation

Encapsulation refers to the practice of restricting direct access to some of an object's attributes and methods, and instead exposing only necessary functionalities through public methods. This allows for better data protection and separation of concerns.

In Python, encapsulation is typically achieved using private and protected attributes/methods, though Python does not enforce strict encapsulation. It's more about convention and using underscores to signal restricted access.

Example of Encapsulation:
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner
        self.__balance = balance  # Private attribute

    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
        else:
            print("Deposit amount must be positive.")

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds or invalid amount.")

    def get_balance(self):
        return self.__balance

# Creating an instance
account = BankAccount("John", 1000)

# Accessing public methods to interact with the private attribute
account.deposit(500)
print(account.get_balance())  # Output: 1500

account.__balance = 2000  # This won't work, as __balance is private
print(account.get_balance())  # Output: 1500 (still the correct value)

In this example:

  • __balance is a private attribute, and cannot be accessed directly from outside the class. Access is controlled via the deposit, withdraw, and get_balance methods.

Subtopic 2: Private and Protected Members

  • Private Members: These are variables and methods that are only accessible within the class. They are denoted with a double underscore prefix (__).
  • Protected Members: These are variables and methods that are intended to be used by subclasses. They are denoted with a single underscore prefix (_).

While Python does not strictly enforce the private/protected rule, using these prefixes is a convention.

Example of Private and Protected Members:
class Car:
    def __init__(self, make, model):
        self._make = make      # Protected member
        self.__model = model   # Private member

    def get_model(self):
        return self.__model

class SportsCar(Car):
    def __init__(self, make, model, speed):
        super().__init__(make, model)
        self.__speed = speed

    def get_speed(self):
        return self.__speed

# Creating an object
car = SportsCar("Ferrari", "488", 210)

# Accessing protected member from subclass
print(car._make)  # Output: Ferrari (accessible, but should be treated as internal)

# Accessing private member from subclass will fail
# print(car.__model)  # This will raise an AttributeError

In this example:

  • The _make attribute is protected, meaning it can be accessed and modified by subclasses.
  • The __model attribute is private, so it cannot be accessed directly by any class or subclass outside Car.

Subtopic 3: Property Decorators

In Python, the @property decorator allows you to define methods that can be accessed like attributes. This is useful when you want to encapsulate a computation or access logic while keeping a clean, readable interface.

Property decorators allow you to define a method as a "getter," and if needed, a "setter" for the attribute. This helps in controlling access to attributes (read-only or read-write).

Example of Property Decorators:
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value > 0:
            self._radius = value
        else:
            print("Radius must be positive.")

# Creating an object
circle = Circle(5)

# Accessing the radius using the property method
print(circle.radius)  # Output: 5

# Setting a new radius
circle.radius = 10
print(circle.radius)  # Output: 10

# Trying to set a negative radius (will trigger the setter's validation)
circle.radius = -5  # Output: Radius must be positive.

In this example:

  • The radius property allows controlled access to the _radius attribute. If an invalid value is provided, the setter method prevents it.

Subtopic 4: Abstract Classes and Methods

Abstraction is the concept of hiding implementation details and exposing only the essential features of an object. This can be achieved using abstract classes and abstract methods in Python. An abstract class cannot be instantiated directly, and it may contain one or more abstract methods that must be implemented by its subclasses.

To create an abstract class in Python, you use the abc (Abstract Base Class) module.

Example of Abstract Classes and Methods:
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

# Creating objects
dog = Dog()
cat = Cat()

print(dog.speak())  # Output: Woof!
print(cat.speak())  # Output: Meow!

In this example:

  • Animal is an abstract class with an abstract method speak(), which must be implemented by any non-abstract subclass.
  • Dog and Cat implement the speak() method, making them concrete classes.

Tasks

  1. Task 1: Implement Encapsulation in a Bank Account

    • Create a BankAccount class with private attributes for balance. Implement methods to deposit, withdraw, and check balance. Prevent direct modification of the balance attribute from outside the class.
  2. Task 2: Create a Protected Member

    • Create a Vehicle class with a protected attribute speed. Create a Car subclass that inherits from Vehicle and displays the speed using the protected member.
  3. Task 3: Implement Property Decorators

    • Create a Person class with a _name attribute. Use the @property decorator to define a name method that returns the name in uppercase. Use a setter to ensure that the name is always in uppercase.
  4. Task 4: Abstract Shape Class

    • Define an abstract class Shape with an abstract method area(). Create two subclasses, Rectangle and Circle, that implement the area() method. Instantiate both and calculate their area.
  5. Task 5: Private Members and Accessor Methods

    • Create a Student class with a private attribute __grade. Implement a method to set the grade (only if it is between 0 and 100) and another method to get the grade.
  6. Task 6: Animal Hierarchy with Abstraction

    • Create an abstract Animal class with an abstract make_sound() method. Create two subclasses, Dog and Bird, that override the make_sound() method to return the respective sounds of these animals.