Home ยป Python Encapsulation: A Tutorial

Python Encapsulation: A Tutorial

Java SE 11 Programmer I [1Z0-815] Practice Tests
Oracle Java Certification
Java SE 11 Developer (Upgrade) [1Z0-817]
Java SE 11 Programmer II [1Z0-816] Practice Tests
Spring Framework Basics Video Course
1 Year Subscription

Encapsulation is one of the fundamental principles of object-oriented programming (OOP) in Python.

It is the process of bundling data (variables) and methods (functions) into a single unit or class, while restricting access to certain attributes to prevent accidental modification.

Encapsulation helps protect data integrity and provides a clear structure for how data and functions interact within a class.

What is Encapsulation?

Encapsulation refers to restricting direct access to some of an object's components and only allowing manipulation of these components via methods (getters and setters). This provides control over how data is modified and accessed.

Key Concepts in Encapsulation:

Private Attributes and Methods: In Python, you can make an attribute or method private by prefixing it with an underscore (_) or double underscore (__).
Getters and Setters: Special methods that provide controlled access to private attributes.

1. Encapsulation in Python with Private Attributes

In Python, private attributes and methods are not truly private but are intended to be accessed only within their class. They are denoted by a single underscore (_) or double underscore (__).

Single Underscore (_attribute): This is a convention to indicate that an attribute is intended for internal use. It can still be accessed outside the class, but it should be considered private by convention.
Double Underscore (__attribute): This causes name mangling, which makes it harder (but not impossible) to access the attribute from outside the class.

Example: Encapsulation with Private Attributes

class Car:
    def __init__(self, brand, speed):
        self.brand = brand
        self._speed = speed  # Protected attribute (single underscore)

    def drive(self):
        return f"The {self.brand} is driving at {self._speed} km/h."

# Create an instance of Car
my_car = Car("Toyota", 120)

# Access the protected attribute (not recommended, but possible)
print(my_car._speed)  # Output: 120

# Access using the method
print(my_car.drive())  # Output: The Toyota is driving at 120 km/h.

In this example:

The _speed attribute is marked as protected (by convention), but it can still be accessed directly, though it's discouraged. The preferred way to access it is through the drive() method.

2. Encapsulation with Private Attributes (Double Underscore)

Double underscores make an attribute or method harder to access from outside the class by using name mangling. Python internally changes the name of the attribute to make it harder to access directly.

Example: Using Double Underscore for Private Attributes

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

    def deposit(self, amount):
        self.__balance += amount

    def get_balance(self):
        return self.__balance

# Create an instance of BankAccount
account = BankAccount("Alice", 1000)

# Accessing private attribute directly will raise an error
# print(account.__balance)  # AttributeError: 'BankAccount' object has no attribute '__balance'

# Access the balance using the getter method
print(account.get_balance())  # Output: 1000

# Name mangling: You can still access the private attribute if you know the mangled name
print(account._BankAccount__balance)  # Output: 1000

In this example:

The __balance attribute is private. It cannot be accessed directly, but it can be accessed indirectly using the get_balance() method.
The private attribute can be accessed using name mangling (_ClassName__attribute), but this is not recommended.

3. Getters and Setters in Python

Getters and setters are methods that allow you to read and modify private attributes in a controlled way. This provides more control over how attributes are accessed and modified, while keeping the data encapsulated.

Example: Using Getters and Setters

class Student:
    def __init__(self, name, age):
        self.__name = name  # Private attribute
        self.__age = age  # Private attribute

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

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

    # Getter for age
    def get_age(self):
        return self.__age

    # Setter for age
    def set_age(self, age):
        if age >= 0:
            self.__age = age
        else:
            print("Invalid age")

# Create an instance of Student
student = Student("John", 20)

# Access and modify the private attributes using getters and setters
print(student.get_name())  # Output: John
print(student.get_age())  # Output: 20

# Modify the name and age
student.set_name("Alice")
student.set_age(21)
print(student.get_name())  # Output: Alice
print(student.get_age())  # Output: 21

# Trying to set an invalid age
student.set_age(-5)  # Output: Invalid age

In this example:

The __name and __age attributes are private, and you can only access and modify them using the getter and setter methods.
The setter for age includes validation logic to ensure that the age is valid, demonstrating the power of encapsulation in controlling how attributes are modified.

4. Using Properties in Python

Python provides a cleaner way to use getters and setters using the property() function or the @property decorator. Properties allow you to define methods that behave like attributes, making your code more readable.

Example: Using the @property Decorator

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

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

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

    # Getter for salary
    @property
    def salary(self):
        return self.__salary

    # Setter for salary with validation
    @salary.setter
    def salary(self, salary):
        if salary >= 0:
            self.__salary = salary
        else:
            print("Invalid salary")

# Create an instance of Employee
employee = Employee("John", 5000)

# Access and modify using properties
print(employee.name)  # Output: John
print(employee.salary)  # Output: 5000

# Modify the salary and name
employee.name = "Alice"
employee.salary = 6000
print(employee.name)  # Output: Alice
print(employee.salary)  # Output: 6000

# Trying to set an invalid salary
employee.salary = -100  # Output: Invalid salary

In this example:

The @property decorator is used to define the getter and setter methods, which allow us to access and modify private attributes as if they were public attributes.

The setter for salary includes validation logic to ensure the salary cannot be negative.

5. Advantages of Encapsulation

Data Protection: Encapsulation prevents direct access to variables, reducing the risk of accidentally modifying or corrupting data.
Control: You can control how the attributes are accessed or modified through getter and setter methods.
Flexibility: You can change the internal implementation of a class without affecting external code that uses the class.
Maintainability: Encapsulation makes code easier to maintain and understand by providing a clear structure and separation between internal logic and the interface.

6. Encapsulation in Real-World Examples

Example 1: Banking System

Encapsulation can be used in a banking system to protect sensitive information like the account balance and to provide controlled access to the balance through deposit and withdrawal methods.

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

    # Getter for balance
    @property
    def balance(self):
        return self.__balance

    # Deposit method
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
        else:
            print("Invalid deposit amount")

    # Withdraw method with validation
    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Invalid or insufficient funds")

# Create an instance of BankAccount
account = BankAccount("12345", 1000)

# Access balance using property
print(account.balance)  # Output: 1000

# Deposit and withdraw
account.deposit(500)
print(account.balance)  # Output: 1500

account.withdraw(200)
print(account.balance)  # Output: 1300

# Trying to withdraw more than the balance
account.withdraw(2000)  # Output: Invalid or insufficient funds

Example 2: Car Object with Encapsulation

class Car:
    def __init__(self, model, year, price):
        self.model = model
        self.year = year
        self.__price = price  # Private attribute

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

    # Setter for price with validation
    @price.setter
    def price(self, price):
        if price > 0:
            self.__price = price
        else:
            print("Invalid price")

# Create an instance of Car
my_car = Car("Toyota Corolla", 2020, 20000)

# Access and modify the price using property
print(my_car.price)  # Output: 20000
my_car.price = 22000
print(my_car.price)  # Output: 22000

# Trying to set an invalid price
my_car.price = -1000  # Output: Invalid price

Summary

Encapsulation is the process of bundling data and methods within a class while restricting direct access to some attributes.
You can create private attributes using underscores (_ and __), but access to them can be controlled through getter and setter methods.
Properties provide a cleaner way to define getters and setters using the @property decorator.
Encapsulation protects data integrity, provides control over how data is accessed or modified, and improves the maintainability of your code.

By using encapsulation effectively, you can design robust and secure classes that protect sensitive data and offer a clear interface for interacting with that data.

You may also like

Leave a Comment

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More