Virtual assistance

Python Encapsulation

Encapsulation is the bundling of data and methods that operate on that data within a single unit. Learn access modifiers, properties, data hiding, and encapsulation principles.

Python Encapsulation

What is Encapsulation?

Encapsulation is one of the fundamental concepts in object-oriented programming. It refers to the bundling of data (attributes) and methods that operate on that data within a single unit (class), while restricting direct access to some of the object's components.

"Encapsulation hides the internal state and functionality of an object from the outside world."

Access Modifiers in Python

Python uses naming conventions for access control:

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner          # Public attribute
        self._balance = balance     # Protected attribute (convention)
        self.__pin = "1234"         # Private attribute (name mangling)
    
    def get_balance(self):          # Public method
        return self._balance
    
    def _validate_pin(self, pin):   # Protected method (convention)
        return pin == self.__pin
    
    def __change_pin(self, old_pin, new_pin):  # Private method
        if self._validate_pin(old_pin):
            self.__pin = new_pin
            return True
        return False

account = BankAccount("John", 1000)
print(account.owner)        # John (public)
print(account._balance)     # 1000 (protected, but accessible)
# print(account.__pin)      # AttributeError: 'BankAccount' object has no attribute '__pin'
print(account._BankAccount__pin)  # 1234 (name mangling)

Properties for Data Encapsulation

Use properties to control attribute access:

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius
    
    @property
    def celsius(self):
        """Getter for celsius"""
        return self._celsius
    
    @celsius.setter
    def celsius(self, value):
        """Setter for celsius with validation"""
        if value < -273.15:
            raise ValueError("Temperature cannot be below absolute zero")
        self._celsius = value
    
    @property
    def fahrenheit(self):
        """Read-only property for fahrenheit"""
        return self._celsius * 9/5 + 32
    
    @fahrenheit.setter
    def fahrenheit(self, value):
        """Setter for fahrenheit"""
        self._celsius = (value - 32) * 5/9

temp = Temperature(25)
print(f"Celsius: {temp.celsius}")      # 25
print(f"Fahrenheit: {temp.fahrenheit}") # 77.0

temp.celsius = 30
print(f"New Celsius: {temp.celsius}")  # 30
print(f"New Fahrenheit: {temp.fahrenheit}") # 86.0

temp.fahrenheit = 100
print(f"Celsius from Fahrenheit: {temp.celsius}")  # 37.777...

# This will raise ValueError
# temp.celsius = -300

Data Hiding and Validation

Encapsulate data with validation:

class Student:
    def __init__(self, name, age, grade):
        self._name = name
        self._age = age
        self._grade = grade
    
    @property
    def name(self):
        return self._name
    
    @name.setter
    def name(self, value):
        if not isinstance(value, str) or len(value.strip()) == 0:
            raise ValueError("Name must be a non-empty string")
        self._name = value.strip()
    
    @property
    def age(self):
        return self._age
    
    @age.setter
    def age(self, value):
        if not isinstance(value, int) or value < 5 or value > 100:
            raise ValueError("Age must be an integer between 5 and 100")
        self._age = value
    
    @property
    def grade(self):
        return self._grade
    
    @grade.setter
    def grade(self, value):
        valid_grades = ['A', 'B', 'C', 'D', 'F']
        if value.upper() not in valid_grades:
            raise ValueError(f"Grade must be one of {valid_grades}")
        self._grade = value.upper()

student = Student("Alice", 16, "A")
print(f"Name: {student.name}, Age: {student.age}, Grade: {student.grade}")

student.name = "Bob"
student.age = 17
student.grade = "b"  # Will be converted to "B"
print(f"Updated: {student.name}, {student.age}, {student.grade}")

Private Methods and Internal Logic

Hide implementation details:

class EmailValidator:
    def __init__(self, email):
        self.email = email
    
    def is_valid(self):
        """Public method that uses private helper methods"""
        if not self.__has_valid_format():
            return False
        if not self.__has_valid_domain():
            return False
        return True
    
    def __has_valid_format(self):
        """Private method for format validation"""
        return '@' in self.email and '.' in self.email.split('@')[1]
    
    def __has_valid_domain(self):
        """Private method for domain validation"""
        domain = self.email.split('@')[1]
        valid_domains = ['gmail.com', 'yahoo.com', 'outlook.com']
        return domain in valid_domains

validator = EmailValidator("user@gmail.com")
print(validator.is_valid())  # True

# Private methods are not accessible directly
# validator.__has_valid_format()  # AttributeError

Class Variables and Encapsulation

Encapsulate class-level data:

class Employee:
    _company_name = "Tech Corp"  # Protected class variable
    __employee_count = 0         # Private class variable
    
    def __init__(self, name, salary):
        self.name = name
        self._salary = salary
        Employee.__employee_count += 1
    
    @classmethod
    def get_employee_count(cls):
        """Public class method to access private class variable"""
        return cls.__employee_count
    
    @classmethod
    def get_company_name(cls):
        """Public class method to access protected class variable"""
        return cls._company_name
    
    @classmethod
    def set_company_name(cls, name):
        """Public class method to modify protected class variable"""
        cls._company_name = name

emp1 = Employee("John", 50000)
emp2 = Employee("Jane", 60000)

print(Employee.get_employee_count())  # 2
print(Employee.get_company_name())    # Tech Corp

Employee.set_company_name("New Tech Corp")
print(Employee.get_company_name())    # New Tech Corp

Encapsulation with Composition

Use composition to hide complex objects:

class Engine:
    def __init__(self, horsepower):
        self.__horsepower = horsepower
        self.__running = False
    
    def start(self):
        self.__running = True
        return "Engine started"
    
    def stop(self):
        self.__running = False
        return "Engine stopped"
    
    def is_running(self):
        return self.__running
    
    def get_horsepower(self):
        return self.__horsepower

class Car:
    def __init__(self, make, model, horsepower):
        self.make = make
        self.model = model
        self._engine = Engine(horsepower)  # Encapsulated engine
    
    def start_car(self):
        return f"{self.make} {self.model}: {self._engine.start()}"
    
    def stop_car(self):
        return f"{self.make} {self.model}: {self._engine.stop()}"
    
    def get_engine_power(self):
        return self._engine.get_horsepower()

car = Car("Toyota", "Camry", 200)
print(car.start_car())           # Toyota Camry: Engine started
print(car.get_engine_power())    # 200

# Engine details are encapsulated
# car._engine.__horsepower  # Not directly accessible

Read-Only Properties

Create immutable attributes:

class Person:
    def __init__(self, name, birth_year):
        self._name = name
        self._birth_year = birth_year
    
    @property
    def name(self):
        return self._name
    
    @property
    def birth_year(self):
        return self._birth_year
    
    @property
    def age(self):
        """Read-only property calculated from birth year"""
        from datetime import datetime
        current_year = datetime.now().year
        return current_year - self._birth_year

person = Person("Alice", 1990)
print(f"Name: {person.name}")           # Alice
print(f"Birth Year: {person.birth_year}") # 1990
print(f"Age: {person.age}")             # Current age

# These will raise AttributeError (no setters defined)
# person.name = "Bob"
# person.birth_year = 1985
# person.age = 30

Encapsulation Patterns

# Data Transfer Object (DTO) Pattern
class UserDTO:
    def __init__(self, user_id, username, email):
        self._user_id = user_id
        self._username = username
        self._email = email
    
    @property
    def user_id(self):
        return self._user_id
    
    @property
    def username(self):
        return self._username
    
    @username.setter
    def username(self, value):
        if len(value) < 3:
            raise ValueError("Username must be at least 3 characters")
        self._username = value
    
    @property
    def email(self):
        return self._email
    
    @email.setter
    def email(self, value):
        if '@' not in value:
            raise ValueError("Invalid email format")
        self._email = value

# Builder Pattern with Encapsulation
class Computer:
    def __init__(self):
        self._cpu = None
        self._ram = None
        self._storage = None
    
    def set_cpu(self, cpu):
        self._cpu = cpu
        return self
    
    def set_ram(self, ram):
        self._ram = ram
        return self
    
    def set_storage(self, storage):
        self._storage = storage
        return self
    
    def build(self):
        if not all([self._cpu, self._ram, self._storage]):
            raise ValueError("All components must be set")
        return ComputerBuilt(self._cpu, self._ram, self._storage)

class ComputerBuilt:
    def __init__(self, cpu, ram, storage):
        self.__cpu = cpu
        self.__ram = ram
        self.__storage = storage
    
    def get_specs(self):
        return {
            'cpu': self.__cpu,
            'ram': self.__ram,
            'storage': self.__storage
        }

computer = Computer().set_cpu("Intel i7").set_ram("16GB").set_storage("512GB").build()
print(computer.get_specs())

Best Practices

  • Use private attributes for internal state: Prefix with __
  • Provide public properties for access: Use @property decorator
  • Validate data in setters: Ensure data integrity
  • Keep implementation details private: Hide complexity
  • Use protected attributes sparingly: Prefix with _ for inheritance
  • Document encapsulation decisions: Explain why data is hidden
  • Avoid over-encapsulation: Don't hide everything
  • Use composition for complex objects: Encapsulate subsystems
  • Provide meaningful interfaces: Clear public methods
  • Test encapsulation thoroughly: Verify data protection

Common Mistakes to Avoid

  • Accessing private attributes directly: Use name mangling only when necessary
  • Making everything private: Balance encapsulation with usability
  • Ignoring validation: Always validate input data
  • Exposing internal objects: Return copies, not references
  • Mixing access levels: Be consistent in your approach

Encapsulation is crucial for creating robust, maintainable code. It protects the internal state of objects and provides controlled access to data, making your code more secure and easier to modify.