Python Encapsulation
Encapsulation is a way to keep data (attributes) and the functions that work with it together in a class, while controlling how that data can be accessed or modified. It hides the internal details of how an object works and allows interaction only through well-defined methods.
Benefits of Encapsulation
Encapsulation provides several key benefits:
- Data protection: Sensitive data is hidden from direct access, keeping it safe from accidental changes.
- Controlled access: Data is validated before updating, so all changes are safe and correct.
- Easier maintenance: It allows internal implementation details to change without affecting code that uses the class, making updates and debugging simpler.
- Clean code: Encapsulated classes are organized, easier to read, and simple to reuse.
Access Modifiers in Python
Unlike languages like Java or C++, Python does not use strict access modifiers like public, private, or protected. Instead, it relies on naming conventions (prefixes) to indicate how a variable or method should be accessed.
Here are the naming conventions used for Python encapsulation:
(1) Public Members
By default, all member variables and methods in a Python class are public and accessible from anywhere.
Syntax: No underscore prefix
class BankAccount:
def __init__(self, account_holder):
self.account_holder = account_holder
account = BankAccount("James")
print(account.account_holder) # Output: James
In this example, the account_holder variable defined inside the BankAccount class is a public instance variable. This means it can be accessed outside the class using an object of that class.
When the BankAccount object is created using the statement account = BankAccount("James"), Python calls the __init__ method of the BankAccount class automatically. The string "James" is passed as the value of the account_holder parameter, and inside the __init__ method, this value is assigned to self.account_holder.
Since account_holder is a public instance variable, it can be accessed outside the class using account.account_holder, which is why the program prints James.
(2) Protected Members
Protected members are intended for internal use within a class or its subclass, but they are accessible from outside the class.
Syntax: Single underscore prefix (_)
class BankAccount:
def __init__(self, account_holder):
self.account_holder = account_holder # Public
self._balance = 0 # Protected
def deposit(self, amount):
self._balance += amount
def get_balance(self):
return self._balance
# Subclass
class SavingsAccount(BankAccount):
def __init__(self, account_holder, interest_rate):
super().__init__(account_holder)
self._interest_rate = interest_rate
def apply_interest(self):
self._balance += self._balance * self._interest_rate
account = SavingsAccount("James", 0.04)
account.deposit(5000) # Deposit money into account
account.apply_interest() # Apply interest to balance
print(account.get_balance()) # Accessing balance using a publc method
# You can do like this, but you shouldn't
# print(account._balance)
Output:
5200.0
In this example, we have a protected variable _balance within the BankAccount class. It is used to store the account balance and is modifed within the deposit() method, where the deposit value is added to the balance.
In the SavingsAccount class, which inherits from BankAccount, the _balance variable is also used indirectly within the apply_interest() method.
Although the _balance variable can be accessed within the class and its subclasses, it is intended for internal use and should not be accessed or modified directly by external code. Encapsulation is preserved by providing controlled access through public methods such as get_balance(), which return the current balance without exposing the _balance attribute directly.
Thus, while you can access _balance variable using account._balance, this is generally discouraged, as it bypasses the intended encapsulation and exposes internal implementation details.
(3) Private Members
Private members are completely hidden from external access and should only be modified or accessed through methods within the class. They are intended to encapsulate internal implementation details that should remain untouched and unseen outside the class.
Syntax: Double undercore prefix (__)
class BankAccount:
def __init__(self, account_holder, pincode):
self.account_holder = account_holder
self.__pincode = pincode
# Method to validate pincode
def validate_pincode(self, input_pincode):
if input_pincode == self.__pincode:
print("Pincode is correct!")
else:
print("Incorrect pincode.")
account = BankAccount("James", 2468)
#Validating pincode
account.validate_pincode(1234) # Incorrect pincode
account.validate_pincode(2468) # Output: Pincode is correct!
In this example, the BankAccount class contains a private variable __pincode, which is accessible only within the class. It is used by the validate_pincode() method to check if an input matches the stored pincode. This demonstrates encapsulation, keeping sensitive data private and interacting with it only through controlled methods.
Name Mangling
Python implements a mechanism called name mangling for private members, which renames the attribute internally to _ClassName__attributeName. This makes direct external access more difficult and helps prevent name clashes in subclass, but is still not a true access block and still can be bypassed.
For example:
class MyClass:
def __init__(self, value):
self.__value = value
def get_value(self):
return self.__value
obj = MyClass(22)
# Access through a public method (recommended)
print(obj.get_value()) # Output: 22
# Direct access using the original name fails
# print(obj.__value) # Raises AttributeError
# Access using the mangled name (still possibly but highly discouraged)
print(obj._MyClass__value) # Output: 22
In this example, we define a private variable __value inside MyClass class. When we access it through the public method get_value(), everything works as expected. However, when we try to access it directly using its original name (obj.__value), it fails because Python internally renames __value to _MyClass__value. This process, known as name mangling, makes accidental access harder, even though it doesn’t completely prevent access.
When we use obj._MyClass__value, we are manually using the mangled name that Python created internally. Because this name matches how Python stores the variable, the value is successfully accessed. Even though this works, it is highly discouraged, as it breaks encapsulation and goes against the intended use of private variables.
Encapsulation Using The @property Decorator
In many programming languages, access to private variables is controlled through explicit getter and setter methods like get_variable() and set_variable(). While this approach works in Python, the more Pythonic solution is to use the @property decorator. It allows methods to be accessed like attributes, keeping the syntax clean and readable while still supporting validation, computation, or other logic behind the scenes.
For example:
class BankAccount:
def __init__(self, account_holder, balance):
self.account_holder = account_holder
self.__balance = balance # Private variable
# Getter: allows read-only access to private balance
@property
def balance(self):
return self.__balance
# Setter: controls how the balance can be updated
@balance.setter
def balance(self, amount):
if amount > 0:
self.__balance = amount
else:
print("Invalid Balance")
# Create a BankAccount object
account = BankAccount("James", 5000)
# Access the balance using the property (looks like an attribute)
print(account.balance)
# Update the balance using the setter
account.balance = 3000
# Print the updated balance
print(account.balance)
Output:
5000
3000
In this example, we define a private varaible __balance inside BankAccount class. The @property decorator allows the balance() method to be accessed like an attribute, providing safe, read-only access to the private variable. The @property.setter decorator (in our case, @balance.setter) controls how the private variables can be updated by enforcing validation rules, ensuring that the balance can only be modified in a controlled and secured manner. When account.balance = 3000 is executed, it doesn’t modify the variable but instead Python automatically invokes the setter method defined by @balance.setter decorator, allowing the update to pass through validation logic before the balance is changed.