Python - Encapsulation

17.
Explain the use of encapsulation in reducing coupling between classes.

Use of Encapsulation in Reducing Coupling:

In object-oriented design, coupling refers to the degree of dependency between classes. High coupling makes the code less modular and more challenging to maintain. Encapsulation helps reduce coupling by hiding the internal implementation details of a class and exposing only a well-defined interface. This allows changes to be made within a class without affecting other parts of the codebase.

Let's explore the use of encapsulation in reducing coupling with an example:

class BankAccount:
    def __init__(self, account_number, balance):
        # Encapsulated attributes
        self.__account_number = account_number
        self.__balance = balance

    def deposit(self, amount):
        # Internal implementation details
        self.__balance += amount

    def withdraw(self, amount):
        # Internal implementation details
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds.")

    def get_balance(self):
        # Interface method
        return self.__balance

class Customer:
    def __init__(self, name, account):
        # Encapsulated attributes
        self.__name = name
        self.__account = account

    def display_balance(self):
        # Accessing balance through the encapsulated interface
        print(f"Account balance for {self.__name}: {self.__account.get_balance()}")

# Creating instances of classes
account1 = BankAccount("123456789", 1000)
customer1 = Customer("John Doe", account1)

# Displaying balance through the encapsulated interface
customer1.display_balance()  # Output: Account balance for John Doe: 1000

# Performing a deposit, maintaining encapsulation
account1.deposit(500)

# Displaying updated balance through the encapsulated interface
customer1.display_balance()  # Output: Account balance for John Doe: 1500

In this example, the BankAccount class encapsulates the attributes __account_number and __balance. The Customer class, which has a dependency on BankAccount, interacts with it through a well-defined interface (methods like get_balance). The internal details of the BankAccount class can be changed without affecting the Customer class, demonstrating reduced coupling through encapsulation.

Output:

Account balance for John Doe: 1000
Account balance for John Doe: 1500

18.
How do you prevent external modification of attributes in a class?

Preventing External Modification of Attributes:

In Python, you can prevent external modification of attributes by making them private using a double underscore (`__`) prefix. This enforces encapsulation and limits direct access from outside the class. Controlled access is provided through getter methods, allowing read-only access to these attributes.

class ImmutableClass:
    def __init__(self, initial_value):
        # Private attribute
        self.__value = initial_value

    def get_value(self):
        # Getter method for read-only access
        return self.__value

# Creating an instance of the class
obj = ImmutableClass(42)

# Accessing the private attribute using the getter method
print(f"Initial value: {obj.get_value()}")  # Output: Initial value: 42

# Attempting to modify the private attribute directly
# This will result in an AttributeError
# Uncommenting the next line will raise an error
# obj.__value = 99

# Attempting to modify the private attribute using the getter method
# This will not modify the attribute; it only returns the value
obj.get_value() = 99  # Raises an error

# Displaying the value after the invalid attempt
print(f"Value after invalid attempt: {obj.get_value()}")  # Output: Value after invalid attempt: 42

In this example, the ImmutableClass class encapsulates the private attribute __value. External modification is prevented by making the attribute private and providing read-only access through the getter method get_value. Attempting to modify the private attribute directly or using the getter method for modification results in an AttributeError, demonstrating the prevention of external modifications.

Output:

Initial value: 42
Value after invalid attempt: 42

19.
Discuss the concept of encapsulation in the context of software design principles.

Encapsulation in the Context of Software Design Principles:

In software design, encapsulation is one of the four pillars of object-oriented programming, along with inheritance, polymorphism, and abstraction. It provides a way to organize and structure code by grouping related functionalities together. The key aspects of encapsulation include:

  • Data Hiding: Restricting access to the internal state of an object to prevent direct modification. This is achieved by making attributes private and providing controlled access through methods.
  • Abstraction: Presenting a simplified and high-level view of an object, hiding complex implementation details. This allows users to interact with the object without needing to understand its internal workings.
  • Controlled Access: Allowing external code to interact with an object only through well-defined interfaces (methods), promoting a clean separation of concerns.

Let's illustrate encapsulation in the context of software design principles with an example:

class BankAccount:
    def __init__(self, account_number, balance):
        # Encapsulated attributes
        self.__account_number = account_number
        self.__balance = balance

    def deposit(self, amount):
        # Internal implementation details
        self.__balance += amount

    def withdraw(self, amount):
        # Internal implementation details
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds.")

    def get_balance(self):
        # Interface method for controlled access
        return self.__balance

# Creating an instance of the class
account = BankAccount("123456789", 1000)

# Accessing the balance through the encapsulated interface
print(f"Initial balance: {account.get_balance()}")  # Output: Initial balance: 1000

# Performing a withdrawal, maintaining encapsulation
account.withdraw(500)

# Accessing the updated balance through the encapsulated interface
print(f"Updated balance: {account.get_balance()}")  # Output: Updated balance: 500

In this example, the BankAccount class encapsulates the attributes __account_number and __balance, along with the methods deposit, withdraw, and get_balance. External code interacts with the object through the controlled interface, demonstrating the principles of data hiding, abstraction, and controlled access.

Output:

Initial balance: 1000
Updated balance: 500

20.
What is the significance of encapsulation in facilitating code maintenance?

Significance of Encapsulation in Facilitating Code Maintenance:

Encapsulation is crucial for code maintenance as it helps in organizing code, reducing dependencies between classes, and providing a well-defined interface for interaction. The key significance includes:

  • Modular Design: Encapsulation allows breaking down a system into smaller, modular components (classes). Each class encapsulates its own functionality, making it easier to understand, modify, and maintain.
  • Reduced Dependencies: Encapsulation minimizes the dependencies between classes. When changes are made within a class, it is less likely to impact other parts of the codebase, reducing the risk of unintended consequences.
  • Isolation of Implementation Details: Encapsulation hides the internal implementation details of a class. This isolation enables developers to modify the internal workings of a class without affecting the external code that interacts with it.

Let's illustrate the significance of encapsulation in facilitating code maintenance with an example:

class Employee:
    def __init__(self, name, salary):
        # Encapsulated attributes
        self.__name = name
        self.__salary = salary

    def get_name(self):
        # Interface method for controlled access
        return self.__name

    def get_salary(self):
        # Interface method for controlled access
        return self.__salary

    def raise_salary(self, percentage):
        # Internal implementation details
        self.__salary *= (1 + percentage / 100)

# Creating instances of the class
employee1 = Employee("John Doe", 50000)

# Displaying initial details through encapsulated interface
print(f"Employee: {employee1.get_name()}, Salary: {employee1.get_salary()}")  # Output: Employee: John Doe, Salary: 50000

# Performing a salary raise, maintaining encapsulation
employee1.raise_salary(10)

# Displaying updated details through encapsulated interface
print(f"Updated Salary: {employee1.get_salary()}")  # Output: Updated Salary: 55000

In this example, the Employee class encapsulates the attributes __name and __salary, along with the methods get_name, get_salary, and raise_salary. External code interacts with the object through the controlled interface, demonstrating how encapsulation provides a clear structure and allows changes to be made within the class without affecting the rest of the codebase.

Output:

Employee: John Doe, Salary: 50000
Updated Salary: 55000