Encapsulation and Data Hiding

Introduction to Encapsulation

Encapsulation is a fundamental concept in Object-Oriented Programming (OOP) that involves wrapping data (variables) and methods (functions) within a single unit (class). It helps in data protection and restricting direct access to the internal workings of an object.

For example, a bank account system does not allow direct modification of balance; instead, you use functions like deposit() and withdraw().

Key Benefits of Encapsulation

Data Security – Prevents unintended modifications. 
Data Hiding – Hides sensitive information from direct access. 
Modularity – Keeps code organized and manageable. 
Reusability – Allows easy modification without breaking other parts of the program.


Encapsulation in Python: Public, Protected, and Private Members

Python does not strictly enforce encapsulation but provides naming conventions to control access to class members.

Access ModifierPrefixAccessibility
PublicNo prefixAccessible everywhere
Protected_single_underscoreShould be accessed within the class and subclasses
Private__double_underscoreHidden from outside access

1. Public Members (Default Access)

By default, all class attributes and methods are public, meaning they can be accessed from anywhere.

Example: Public Variables

class Car:
    def __init__(self, brand, speed):
        self.brand = brand  # Public variable
        self.speed = speed  # Public variable

car1 = Car("Tata", 120)
print(car1.brand)  # Output: Tata
print(car1.speed)  # Output: 120

Here, brand and speed are public, so they can be accessed directly.


2. Protected Members (_underscore)

Protected members are indicated by a single underscore (_). They can be accessed outside the class but should not be modified directly.

Example: Protected Variables

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self._salary = salary  # Protected variable

class Manager(Employee):
    def show_salary(self):
        print(f"Manager's salary: {self._salary}")

mgr = Manager("Raj", 75000)
mgr.show_salary()  # Output: Manager's salary: 75000
print(mgr._salary)  # Accessible but should not be modified directly

_salary is protected, meaning it can be accessed but should not be modified directly outside the class.


3. Private Members (__double_underscore)

Private members are indicated by __double_underscore. These cannot be accessed directly from outside the class.

Example: Private Variables

class BankAccount:
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.__balance = balance  # Private variable

    def deposit(self, amount):
        self.__balance += amount
        print(f"Deposited {amount}. New balance: {self.__balance}")

    def get_balance(self):
        return self.__balance  # Accessing private variable through a method

acc = BankAccount(101, 5000)
print(acc.account_number)  # Output: 101
# print(acc.__balance)  #  AttributeError: 'BankAccount' object has no attribute '__balance'
print(acc.get_balance())  #  Correct way to access private data

Here, __balance cannot be accessed directly, but we can use get_balance() to retrieve its value.


4. Name Mangling in Python

Even though private members are not directly accessible, Python uses name mangling to make them accessible in special cases.

print(acc._BankAccount__balance)  # Output: 5000 (Accessing private variable using name mangling)

Note: Name mangling should not be used in regular practice, as it breaks encapsulation principles.


5. Encapsulation with Getter and Setter Methods

To control access to private data, we use getter and setter methods.

Example: Implementing Getter and Setter

class Student:
    def __init__(self, name, marks):
        self.name = name
        self.__marks = marks  # Private variable

    def get_marks(self):  # Getter method
        return self.__marks

    def set_marks(self, new_marks):  # Setter method
        if 0 <= new_marks <= 100:
            self.__marks = new_marks
        else:
            print("Invalid marks! Must be between 0 and 100.")

student1 = Student("Amit", 85)
print(student1.get_marks())  # Output: 85
student1.set_marks(90)  # Setting new marks
print(student1.get_marks())  # Output: 90
student1.set_marks(110)  # Invalid input

Getters retrieve private values, and setters validate before modifying them.


6. Encapsulation in Real-World Applications

Encapsulation is widely used in banking systems, e-commerce platforms, and software security.

Example: ATM System

class ATM:
    def __init__(self, pin):
        self.__pin = pin  # Private variable

    def change_pin(self, old_pin, new_pin):
        if old_pin == self.__pin:
            self.__pin = new_pin
            print("PIN changed successfully.")
        else:
            print("Incorrect PIN!")

atm1 = ATM(1234)
# atm1.__pin  #  AttributeError
atm1.change_pin(1234, 5678)  # PIN changed successfully

Sensitive data like PIN should be encapsulated to prevent direct access.


Summary

Encapsulation helps in data security, modularity, and reusability.
Python provides public, protected, and private access levels using naming conventions.
Private variables (__var) cannot be accessed directly, but can be accessed using getter and setter methods.
Name mangling (_ClassName__var) allows private variables to be accessed but should not be used in practice.
Encapsulation is widely used in real-world applications like banking, security, and software engineering.