Python Intermediate and Advanced

Python Intermediate_016_Encapsulation in Python — Concepts, Examples, and Practical Application

codeaddict 2025. 4. 5. 21:36

1. Introduction

Encapsulation is a cornerstone of object-oriented programming (OOP) that bundles data (attributes) and the methods that manipulate it into a single unit — a class — while restricting direct access to the data.

Think of encapsulation as a secure vault. Inside the vault, you store sensitive items (data) and provide specific tools (methods) to interact with them. Only authorized personnel (the class’s methods) can access or modify the contents, while outsiders (external code) must use the provided interface.

In Python, encapsulation uses naming conventions:

  • Public: No prefix, accessible everywhere (e.g., data).
  • Protected: Single underscore (_data), a hint for internal or subclass use, though not enforced.
  • Private: Double underscore (__data), triggering name mangling to discourage direct access.

2. Encapsulation in Python: Public, Protected, and Private

Example 1: Public Members — Weather Data

class WeatherStation:
    def __init__(self, temperature):
        self.temp = temperature  # Public attribute

    def report(self):
        print(f"Current temperature: {self.temp}°C")

# Using the class
station = WeatherStation(25)
print(station.temp)  # Direct access
station.temp = -500  # Oops, unrealistic value
print(station.temp)
station.report()

Output:

25
-500
Current temperature: -500°C

Detailed Explanation:

  • temp is public, so anyone can read or write it (station.temp = -500).
  • Pros: Simple and straightforward for small, safe scenarios.
  • Cons: No validation means temp can be set to absurd values (e.g., -500°C), breaking the realism of a weather system.

Example 2: Protected Members — Inventory Tracker

class Inventory:
    def __init__(self, stock):
        self._stock = stock  # Protected attribute

    def check_stock(self):
        print(f"Items in stock: {self._stock}")

    def restock(self, amount):
        self._stock += amount
        print(f"Restocked {amount}. New stock: {self._stock}")

# Using the class
shop = Inventory(100)
print(shop._stock)  # Accessible, but discouraged
shop._stock = -10  # Can still modify, though it’s "protected"
print(shop._stock)
shop.check_stock()
shop.restock(50)

Output:

100
-10
Items in stock: -10
Restocked 50. New stock: 40

Detailed Explanation:

  • _stock with a single underscore is “protected,” signaling it’s for internal use or subclasses.
  • Python doesn’t block access (shop._stock = -10 works), but the underscore tells developers, “Hey, use the methods instead!”
  • Pros: Suggests a boundary, useful for teamwork or documentation.
  • Cons: No real protection — negative stock is still possible, which doesn’t make sense for an inventory.

Example 3: Private Members — Patient Records

class PatientRecord:
    def __init__(self, name, age):
        self.__name = name  # Private attribute
        self.__age = None   # Private attribute
        self.set_age(age)   # Use setter for validation

    def get_name(self):
        return self.__name

    def get_age(self):
        return self.__age

    def set_age(self, new_age):
        if isinstance(new_age, int) and 0 <= new_age <= 120:
            self.__age = new_age
        else:
            print(f"Error: Age must be an integer between 0 and 120. Keeping {self.__age}")

    def display(self):
        print(f"Patient: {self.__name}, Age: {self.__age}")

# Using the class
patient = PatientRecord("Emma", 30)
print(patient.get_name())  # Access via getter
patient.set_age(35)       # Update via setter
patient.display()
patient.set_age(-5)       # Invalid age
# print(patient.__age)     # AttributeError
patient.display()

Output:

Emma
Patient: Emma, Age: 35
Error: Age must be an integer between 0 and 120. Keeping 35
Patient: Emma, Age: 35

Detailed Explanation:

  • __name and __age are private,, making direct access tricky (patient.__age fails).
  • get_name, get_age, and set_age provide a controlled interface. set_age validates input, ensuring age stays realistic (0–120).
  • Pros: Protects sensitive data (like patient info) and enforces rules.

3. Why Encapsulation Matters: A Problem and Solution

Let’s apply encapsulation to a real-world data problem.

Problem: Managing a Budget Tracker

You’re creating a Budget class to track expenses. Without encapsulation, users could set the budget to negative values or bypass spending limits, breaking financial logic.

class Budget:
    def __init__(self, total):
        self.total = total  # Public attribute

    def spend(self, amount):
        self.total -= amount
        print(f"Spent {amount}. Remaining: {self.total}")

# Using the class
budget = Budget(1000)
print(f"Initial budget: {budget.total}")
budget.spend(300)
budget.total = -500  # Direct manipulation!
print(f"After tampering: {budget.total}")

Output:

Initial budget: 1000
Spent 300. Remaining: 700
After tampering: -500

Issue:

  • total is public, so it can be set to invalid values (e.g., -500), which doesn’t make sense for a budget.

Solution: Encapsulation with Private Members

class Budget:
    def __init__(self, total):
        self.__total = None  # Private attribute
        self.set_total(total)  # Validate initial value

    def get_total(self):
        return self.__total

    def set_total(self, amount):
        if isinstance(amount, (int, float)) and amount >= 0:
            self.__total = amount
        else:
            print(f"Error: Budget must be non-negative. Keeping {self.__total}")

    def spend(self, amount):
        if amount > 0 and amount <= self.__total:
            self.__total -= amount
            print(f"Spent {amount}. Remaining: {self.__total}")
        else:
            print(f"Error: Invalid spend amount or insufficient funds")

# Using the class
budget = Budget(1000)
print(f"Initial budget: {budget.get_total()}")
budget.spend(300)
budget.set_total(-500)  # Invalid attempt
# budget.__total = 2000   # AttributeError
budget.spend(200)
print(f"Final budget: {budget.get_total()}")

Output:

Initial budget: 1000
Spent 300. Remaining: 700
Error: Budget must be non-negative. Keeping 700
Spent 200. Remaining: 500
Final budget: 500

Explanation of the Solution:

  • __total is private, preventing direct changes (budget.__total = 2000 fails).
  • set_total ensures the budget stays non-negative, and spend checks for valid withdrawals.
  • get_total provides read-only access, keeping the interface clean.
  • Benefit: The budget stays logical and secure, avoiding negative or inconsistent states.