So far, we have learned how classes and objects make programmers' lives easier by allowing them to write general definitions for the objects and instantiating them for implementation. We also learned how to extend classes and inherit their properties and behaviors in other classes. Now, it's time to see how to protect data from being accessed and hide the complex logic from the end user.
Encapsulation and abstraction are the other two fundamental pillars of object-oriented programming that ensure data safety. Encapsulation involves enclosing data and functions within classes and using access specifiers to control their access. On the other hand, abstraction implies hiding the complex program logic from the end user, so only the necessary information is accessible to them. In this article, we will learn how to implement these concepts in our programs.
Encapsulation: Bundling Data and Methods
Encapsulation is the process of enclosing data and methods within a single unit, known as a class. A key aspect of this concept is data hiding, i.e., restricting direct access to the object's data and manipulating it only through the defined methods.
Benefits of Encapsulation
- Ensures that an object's internal data is not accessed or manipulated by external code, either accidentally or maliciously (data protection).
- Since the related data and methods are enclosed in classes, any changes made to these attributes do not directly affect any outside code interacting with that class.
"Private" and "Protected" Members in Python
Unlike other programming languages (such as Java, C#, etc.), Python does not have strict access specifiers to prevent external access to attributes. Instead, it follows some conventions to suggest how data and methods should be accessed. By default, all the class members are public in Python.
Public Members
The public members are those with no leading underscore in their names. These members can be accessed from anywhere in the program.
class MyClass:
def public_method(self):
print("I am public.")
Private Members
Private members should only be accessed within the class itself. To make a member private, double-leading underscore (__
) must be used in the member's name. This makes Python perform a process called name mangling, which makes it hard to access the attributes from outside the class. Its main purpose is to avoid name clashes between subclasses rather than providing strict security to class members.
class MyClass:
def __init__(self):
self.__private_data = "Secret info"
def __private_method(self):
print("This is a private method.")
obj = MyClass()
#
# print(obj.__private_data) # This would raise an AttributeError
#
# print(obj._MyClass__private_data) # This technically works, showing it's not truly private
The Getters and Setters (Accessor and Mutator Methods)
The Getters and Setters are the public methods that allow you to read and write values of the private and protected attributes. The getter methods are called Accessors, and the setters are known as Mutators. These methods allow you to validate logic, log, and perform calculations wherever the data is accessed while maintaining data integrity.
class BankAccount:
def __init__(self, initial_balance):
if initial_balance < 0:
print("Invalid initial balance!")
self.__balance = initial_balance # "Private" attribute
def get_balance(self): # Getter method
"""Returns the current account balance."""
return self.__balance
def deposit(self, amount): # Setter-like method
"""Deposits a positive amount into the account."""
if amount > 0:
self.__balance += amount
print(f"Deposited {amount}. New balance: {self.__balance}")
else:
print("Deposit amount must be positive.")
account = BankAccount(300) # Initial Balance 300
account.deposit(500) # Deposited 500
account.get_balance() # Inquiring the balance
Output:
Deposited 500.
New balance: 800
The @property Decorator
Python provides a more efficient way to implement getters, setters, and deleters, specifically through the use of the @property
decorator. It allows you to access member methods as attributes, keeping the code cleaner and retaining control.
class Celsius:
def __init__(self, temperature=0):
self._temperature = temperature # Use protected convention
@property # The getter method
def temperature(self):
print("Getting value...")
return self._temperature
@temperature.setter # The setter method
def temperature(self, value):
if value < -273.15:
print("Temperature below absolute zero is not possible. ")
print("Setting value...")
self._temperature = value
c = Celsius(25)
print(c.temperature) # Calls the getter: "Getting value... \n 25"
c.temperature = 30 # Calls the setter: "Setting value... \n"
print(c.temperature) # Calls the getter: "Getting value... \n 30"
Output:
Getting value...
25
Setting value...
Getting value...
30
Abstraction: Hiding Complexity
Abstraction is the process of hiding complex logical details from the user and providing access to only the necessary details. It ensures that users only know what an object does rather than how it does it. We interact with the object at a higher level without knowing its intricate internal details.
To understand this concept, you can consider a car. When you drive a car, you use gears, brakes, accelerator, and clutch. You know what each part does, but you don't have to understand how it works. The car's design conceals these details, providing access to the necessary information through a straightforward interface.
Benefits of Abstraction
- Abstraction makes complex systems easier to understand and use by hiding complex details (simplicity).
- Changes made to the internal components of an abstracted component do not affect the external users (maintainability).
- Protects data from being accessed and changed by the users (protection).
Abstract Methods and Abstract Classes
Abstract methods and classes are typically incomplete, meaning they lack implementation details. Users can implement them as needed. An abstract class cannot be instantiated on its own. Instead, it provides a blueprint for other classes. An abstract method is declared inside an abstract class but does not have any implementation. The inheritor of the abstract class implements this method.
In Python, there is a separate module for creating abstract classes and methods, i.e., abc
module. You can import it and use abstract classes in your programs.
from abc import ABC, abstractmethod
class Shape(ABC): # Declaring Shape as an Abstract Base Class
@abstractmethod # This method must be implemented by subclasses
def area(self):
pass # No implementation here
@abstractmethod # This method must also be implemented
def perimeter(self):
pass
class Circle(Shape): # Circle is a concrete subclass of Shape
def __init__(self, radius):
self.radius = radius
def area(self): # Must implement abstract method 'area'
return 3.14159 * self.radius**2
def perimeter(self): # Must implement abstract method 'perimeter'
return 2 * 3.14159 * self.radius
# You CANNOT create an object of an abstract class directly:
# my_shape = Shape() # This would raise a TypeError
my_circle = Circle(5)
print(f"Circle area: {my_circle.area()}")
print(f"Circle perimeter: {my_circle.perimeter()}")
Output:
Circle area: 78.53975
Circle perimeter: 31.4159
You have now explored and mastered all four pillars of the object-oriented programming paradigm. The encapsulation allows you to enclose related members into classes, protecting them from external access. The abstraction helps you hide complex logic from the user, providing a simple interface. The encapsulation and abstraction work hand-in-hand. Polymorphism allows you to create different forms of one method, and inheritance allows you to derive more classes from an existing class to avoid code redundancy and repetition. In the next article, we will explore comprehensions, an advanced data structure in Python.