6.6 Encapsulation and Information Hiding in Python

Encapsulation is one of the fundamental principles of Object-Oriented Programming (OOP). It refers to the bundling of data (attributes) and methods (functions) that operate on the data into a single unit, i.e., a class. Encapsulation helps in information hiding, allowing objects to protect their internal state and only expose essential aspects to the outside world.

In this section, we’ll explore the concepts of encapsulation, information hiding, and how to control access to attributes and methods in Python using access modifiers like public, protected, and private attributes.


6.6.1 What is Encapsulation?

Encapsulation in OOP is the practice of keeping an object's internal state (attributes) safe from unintended modification by restricting direct access. Instead of directly accessing attributes, you provide controlled access through methods. This ensures that objects maintain their integrity and behave consistently.

Key Points:

  • Encapsulation helps in organizing related attributes and methods in a single unit (the class).
  • It enforces data hiding, meaning sensitive information is protected from being accessed or modified directly.
  • Controlled access is provided via getter and setter methods.

6.6.2 Public, Protected, and Private Attributes

In Python, encapsulation is primarily achieved through the use of access modifiers that control how attributes and methods are accessed:

  • Public attributes/methods: Can be accessed from anywhere (both inside and outside the class).
  • Protected attributes/methods: Indicated by a single leading underscore (_attribute). These are intended for internal use but can still be accessed from outside the class.
  • Private attributes/methods: Indicated by two leading underscores (__attribute). These are strongly intended for internal use and are not accessible directly from outside the class.

6.6.3 Public Attributes and Methods

Public attributes and methods can be accessed from anywhere, both inside and outside the class. By default, all attributes and methods in Python are public.

Example:

class Car:
    def __init__(self, brand, model):
        self.brand = brand  # Public attribute
        self.model = model  # Public attribute

    def start(self):
        print(f"The {self.brand} {self.model} is starting.")

# Accessing public attributes and methods
car = Car("Toyota", "Corolla")
print(car.brand)  # Output: Toyota
car.start()       # Output: The Toyota Corolla is starting.

In this example:

  • The brand and model attributes are public, meaning they can be accessed directly from outside the class.
  • The start() method is also public and can be called from outside the class.

6.6.4 Protected Attributes and Methods

Protected attributes and methods are intended to be accessed only within the class and its subclasses. In Python, protected members are marked by a single underscore (_). While they can still be accessed from outside the class, this is discouraged as they are meant for internal use.

Example:

class Person:
    def __init__(self, name, age):
        self._name = name  # Protected attribute
        self._age = age    # Protected attribute

    def _show_details(self):  # Protected method
        print(f"Name: {self._name}, Age: {self._age}")

# Accessing protected attributes and methods (not recommended)
person = Person("Alice", 30)
print(person._name)        # Output: Alice (but discouraged)
person._show_details()     # Output: Name: Alice, Age: 30 (but discouraged)

In this example:

  • The attributes _name and _age are protected, which indicates they are intended for internal use within the class and its subclasses.
  • The _show_details() method is also protected and should ideally not be accessed directly from outside the class.

6.6.5 Private Attributes and Methods

Private attributes and methods are strictly intended for use within the class and cannot be accessed directly from outside the class. In Python, private members are marked by two leading underscores (__), which trigger name mangling to prevent direct access from outside the class.

Example:

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

    def deposit(self, amount):
        self.__balance += amount  # Modify private attribute internally

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

    def get_balance(self):  # Public method to access the private attribute
        return self.__balance

# Creating an instance of BankAccount
account = BankAccount(1000)

# Accessing private attributes indirectly via public methods
account.deposit(500)
account.withdraw(300)
print(account.get_balance())  # Output: 1200

# Direct access to private attribute will raise an AttributeError
# print(account.__balance)  # Raises AttributeError

In this example:

  • The __balance attribute is private, meaning it cannot be accessed directly from outside the class.
  • Access to the private attribute is controlled through public methods like get_balance(), deposit(), and withdraw().
  • Attempting to access __balance directly will result in an AttributeError due to name mangling.

6.6.6 Name Mangling in Python

Python uses name mangling to prevent access to private attributes and methods from outside the class. This process modifies the attribute’s name by adding the class name as a prefix.

Example of Name Mangling:

class Example:
    def __init__(self):
        self.__private_attr = "I'm private"

# Attempting to access the private attribute
example = Example()
# print(example.__private_attr)  # Raises AttributeError

# Accessing the mangled name
print(example._Example__private_attr)  # Output: I'm private

In this example:

  • The __private_attr is not directly accessible because it is a private attribute.
  • However, Python uses name mangling, and the attribute can be accessed using _Example__private_attr. This underscores that private attributes are not truly hidden but are discouraged from being accessed directly.

6.6.7 Getters and Setters

In Python, getter and setter methods are used to control access to private attributes. They provide a way to get (retrieve) or set (update) private data, ensuring that the internal state is modified in a controlled manner.

Example:

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

    # Getter method for name
    def get_name(self):
        return self.__name

    # Setter method for name
    def set_name(self, name):
        self.__name = name

    # Getter method for salary
    def get_salary(self):
        return self.__salary

    # Setter method for salary
    def set_salary(self, salary):
        if salary >= 0:
            self.__salary = salary
        else:
            print("Salary must be positive.")

# Creating an instance of Employee
emp = Employee("John", 50000)

# Accessing private attributes through getter and setter methods
print(emp.get_name())  # Output: John
emp.set_name("Alice")
print(emp.get_name())  # Output: Alice

# Modifying the salary
emp.set_salary(60000)
print(emp.get_salary())  # Output: 60000

# Attempting to set a negative salary
emp.set_salary(-1000)  # Output: Salary must be positive.

In this example:

  • Getter methods (get_name() and get_salary()) allow controlled access to private attributes.
  • Setter methods (set_name() and set_salary()) allow controlled modification of private attributes.
  • The setter method for salary ensures that only valid values are assigned (e.g., salary must be positive).

6.6.8 Property Decorators

Python provides a cleaner way to implement getters and setters using property decorators (@property, @<attribute>.setter). This allows you to use attribute-like access for private attributes while maintaining the control provided by getter and setter methods.

Example:

class Product:
    def __init__(self, name, price):
        self.__name = name       # Private attribute
        self.__price = price     # Private attribute

    # Getter for name using @property decorator
    @property
    def name(self):
        return self.__name

    # Setter for name using @name.setter decorator
    @name.setter
    def name(self, name):
        self.__name = name

    # Getter for price using @property decorator
    @property
    def price(self):
        return self.__price

    # Setter for price using @price.setter decorator
    @price.setter
    def price(self, price):
        if price > 0:
            self.__price = price
        else:
            print("Price must be positive.")

# Creating an instance of Product
product = Product("Laptop", 1000)

# Accessing and modifying attributes using property decorators
print(product.name)

  # Output: Laptop
product.name = "Smartphone"
print(product.name)  # Output: Smartphone

# Accessing and modifying the price
print(product.price)  # Output: 1000
product.price = 1200
print(product.price)  # Output: 1200

# Attempting to set an invalid price
product.price = -500  # Output: Price must be positive.

In this example:

  • The @property decorator allows you to use attribute-style access for the name and price attributes.
  • The setter methods ensure validation of the price attribute before modifying it.

6.6.9 Summary

  • Encapsulation is the bundling of data and methods that operate on the data into a single unit (the class). It restricts access to certain attributes and methods to protect the internal state of objects.
  • Public attributes and methods are accessible from anywhere, protected attributes (prefixed with _) are intended for internal use, and private attributes (prefixed with __) are not accessible from outside the class due to name mangling.
  • Getters and setters provide controlled access and modification of private attributes, ensuring that the object's state is modified safely.
  • Property decorators (@property and @<attribute>.setter) provide a more Pythonic way to define getters and setters, allowing attribute-like access while maintaining control over attribute access and modification.

Encapsulation and information hiding ensure that an object’s internal state is well-protected and that access to this state is controlled, making code more secure, maintainable, and robust.