Object Oriented Programming I Chapter Four
Encapsulation
4.1 Introduction
The most important principle of object orientation is encapsulation. The idea that
data inside the object should only be accessed through a public interface, that is,
the object’s methods. Encapsulation is the process of wrapping a piece of code in a
function, allowing you to take advantage of all the things functions are good for.
OOP enables us to hide the complexity of the internal working of the object which
is advantageous to the developer in the following ways:
• Simplifies and makes it easy to understand to use an object without knowing
the internals.
• Any change can be easily manageable.
Attributes Methods
Class
OOP relies heavily on encapsulation. The terms encapsulation and abstraction (also
called data hiding) are often used as synonyms. They are nearly synonymous, as
abstraction is achieved through encapsulation. Encapsulation provides us the
mechanism of restricting the access to some of the object’s components, this means
that the internal representation of an object can’t be seen from outside of the object
definition. Access to this data is typically achieved through special methods:
Getters and Setters. This data is stored in instance attributes and can be
manipulated from anywhere outside the class. To secure it, that data should only be
accessed using instance methods. Direct access should not be permitted.
In Python, encapsulation is not enforced by the language, but there is a convention
that we can use to indicate that a property is intended to be private and is not part
of the object’s public interface. This can be done by begin the name with an
underscore. It is also customary to set and get simple attribute values directly, and
only write setter and getter methods for values which require some kind of
calculation.
1
Object Oriented Programming I Chapter Four
4.2 Access Modifiers
One of the main concepts in OOP is access modifiers which represent the visibility
and accessibility of methods and attributes. Access modifiers can help in
determining the way that methods, and variables can be accessed and modified.
The encapsulation concept is implemented through access modifiers where the
access to class variables, and methods can be controlled and restricted to
authorized modifiers. In Python, the symbol “_” can be used to define the
accessibility to each class member and method.
There are three types of access modifiers:
- Public: the member can be accessed from anywhere.
- Private: the member can be access from within class only.
- Protected: the member can be accessed from within the class and subclasses.
4.2.1 Public Member
Python supports the notion of encapsulation through naming conventions. If the
identifier given to a class, or to a method or attribute of a class, begins with an
alphabetic character (e.g., color, getColor) that element is presumed to be of public
interest. For example:
class Car:
def __init__(self, make, model, year):
# Public attributes
[Link] = make
[Link] = model
[Link] = year
# Public method
def display_info(self):
print(f"{[Link]} {[Link]} {[Link]}")
# Creating an instance of the Car class
my_car = Car("Toyota", "Camry", 2022)
print(f"Make: {my_car.make}")# Accessing public
attributes
print(f"Model: {my_car.model}")
my_car.display_info()# Calling a public method
2
Object Oriented Programming I Chapter Four
So, all the attributes (make, model, year) and the method (display_info()) are
public since they can be accessed from anywhere.
4.2.2 Private Member
In Python, to designate something as private, we choose an identifier that begins
with double underscore characters (e.g., the __channel attribute of a Television).
This designation of privacy is supported by Python in several ways. When methods
of a class are named with a leading underscore, they are not displayed in
documentation generated by the help command or the pydoc utility. This keeps a
reader’s attention focused on the public aspects of the software. Furthermore, when
the wildcard form of an import is performed, only the public elements of the
module are loaded. Although these features help shield a user from knowledge of
the private details, Python does not strictly enforce the notion of privacy.
In order to protect the integrity of an object’s state, we generally encapsulate all
attributes of a class as private, preferring public access through designated
accessors and mutators. Since we allow the user to set arbitrary values, we might
have chosen a design with public data members that are directly manipulated. Yet
we still prefer to use encapsulation, as the required use of method calls affords us
some notion of control and monitoring.
Just as attributes can be designated as private by naming them with a leading
underscore, we can designate certain methods as private. While the public methods
are ones that we expect to be called by others, private methods are used for our
own convenience when implementing a class; they should only be called from
within the remainder of the class. For example:
class Television:
def __init__(self):
# Private attributes
self.__volume = 10 # Volume range is 0 - 100
self.__channel = 1 # Channel range is 1 - 100
self.__previous_channel = 1
# Public methods (accessors and mutators)
def set_volume(self, volume):
"""Set the volume of the TV, ensuring it's
within a valid range."""
3
Object Oriented Programming I Chapter Four
if 0 <= volume <= 100:
self.__volume = volume
else:
print("Volume must be between 0 and 100.")
def get_volume(self):
"""Return the current volume of the TV."""
return self.__volume
def set_channel(self, channel):
"""Set the TV channel, keeping track of the
previous channel."""
if 1 <= channel <= 100:
self.__previous_channel = self.__channel
self.__channel = channel
else:
print("Channel must be between 1 and 100.")
def get_channel(self):
"""Return the current channel."""
return self.__channel
def switch_to_previous_channel(self):
"""Switch back to the previous channel."""
self.__channel, self.__previous_channel =
self.__previous_channel, self.__channel
print(f"Switched to previous channel:
{self.__channel}")
# Usage Example
tv = Television()
tv.set_volume(30)
print(tv.get_volume()) # Output: 30
tv.set_channel(5)
print(tv.get_channel()) # Output: 5
tv.switch_to_previous_channel() # Output: Switched to
previous channel: 1
print(tv.get_channel()) # Output: 1
4
Object Oriented Programming I Chapter Four
Another example to explain the private member:
class PassKey:
""" A not-at-all secure way to store a secret
string. """
def __init__(self, plain_string, pass_phrase):
self.__plain_string = plain_string
self.__pass_phrase = pass_phrase
def decrypt(self, pass_phrase):
""" Only show the string if the pass_phrase is
correct."""
if pass_phrase == self.__pass_phrase:
return self.__plain_string
else:
return ""
4.2.3 Protected Member
A protected member can be accessed within a class and subclass. The protected
access modifier supports the inheritance. The protected access modifier supports
balance between private and public modifiers. Protected modifier provides the
same level of visibility to the base and subclass. To define the protected modifier,
use a single underscore as a prefix to the name of the member. For example:
class Car:
def __init__(self):
[Link] = ""
self._model = ""
self._year = 2024
def set_make(self, make):
[Link] = make
def set_model(self, model):
self._model = model
my_car = Car()
my_car.set_make("Toyota")
my_car.set_model("Camry")
print(my_car.make,my_car._model)
5
Object Oriented Programming I Chapter Four
4.3 @property Decorator for Encapsulation
Sometimes we use a method to generate a property of an object dynamically,
calculating it from the object’s other properties. Sometimes you can simply use a
method to access a single attribute and return it. You can also use a different
method to update the value of the attribute instead of accessing it directly. Methods
like this are called getters and setters, because they “get” and “set” the values of
attributes, respectively. In some languages you are encouraged to use getters and
setters for all attributes, and never to access their values directly and there are
language features which can make attributes inaccessible except through setters
and getters. In Python, accessing simple attributes directly is perfectly acceptable,
and writing getters and setters for all of them is considered unnecessarily verbose.
Setters can be inconvenient because they don’t allow use of compound assignment
operators. For example:
class Person:
def __init__(self, height):
[Link] = height
def get_height(self):
return [Link]
def set_height(self, height):
[Link] = height
Mohammed = Person(153) # Mohammed is 153cm tall
print([Link]) # Access attribute height getter
Mohammed.set_height(Mohammed.get_height()) # Getter
[Link] += 1 # Set height
Mohammed.set_height([Link] + 1) # Setter
As we can see, incrementing the height attribute through a setter is much more
verbose. Of course, we could write a second setter which increments the attribute
by the given parameter, but we would have to do something similar for every
attribute and every kind of modification that we want to perform. We would have a
similar issue with in-place modifications, like adding values to lists. Something
which is often considered an advantage of setters and getters is that we can change
the way that an attribute is generated inside the object without affecting any code
which uses the object.
For example, suppose that we initially created a Person class which has a fullname
attribute, but later we want to change the class to have separate name and surname
6
Object Oriented Programming I Chapter Four
attributes which we combine to create a full name. If we always access the
fullname attribute through a setter, we can just rewrite the setter, none of the code
which calls the setter will have to be changed. But what if our code accesses the
fullname attribute directly? We can write a fullname method which returns the
right value, but a method has to be called. Fortunately, the @property decorator
lets us make a method behave like an attribute:
class Person:
def __init__(self, name, surname):
[Link] = name
[Link] = surname
@property
def fullname(self):
return [Link] +" "+ [Link]
Mohammed = Person("Mohammed", "Kamil")
print([Link]) # no brackets!
There are also decorators which we can use to define a setter and a deleter for our
attribute (a deleter will delete the attribute from our object). The getter, setter and
deleter methods must all have the same name:
class Person:
def __init__(self, name, surname):
[Link] = name
[Link] = surname
@property
def fullname(self):
return [Link]+" "+[Link]
@[Link]
def fullname(self, fullname):
name, surname = [Link](' ', 1)
[Link] = name
[Link] = surname
7
Object Oriented Programming I Chapter Four
@[Link]
def fullname(self):
del [Link]
del [Link]
Ali = Person("Ali", "Samir")
print([Link])
[Link] = "Ali Sami"
print([Link])
print([Link])
print([Link])
4.4 Use Cases
This section list use cases that explain the encapsulation concept:
4.4.1 Restricting Direct Access
Encapsulation allows you to control access to certain data, ensuring that only
certain parts of the program can modify or access specific attributes. For example:
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private attribute
def deposit(self, amount):
if amount > 0:
self.__balance += amount
def withdraw(self, amount):
if 0 < amount <= self.__balance:
self.__balance -= amount
else:
print("Invalid withdrawal amount")
def get_balance(self):
return self.__balance
account = BankAccount(1000)
[Link](500)
8
Object Oriented Programming I Chapter Four
print(account.get_balance()) # Output: 1500
account.__balance = 10000 # This will not work because
__balance is private
print(account.get_balance())
Another example:
class Person:
def __init__(self, first_name, age):
self.first_name = first_name
self.__age = age
def show_age(self):
return self.__get_age()
def __get_age(self):
return self.__age
tk = Person('TK', 25)
print(tk.show_age()) # => 25
print(tk.__get_age())# AttributeError: 'Person' object
has no attribute '__get_age'
4.4.2 Data Validation
Encapsulation ensures that data assigned to class attributes is validated before
being set, improving data integrity. For example:
class Employee:
def __init__(self, salary):
self.__salary = salary
@property
def salary(self):
return self.__salary
@[Link]
def salary(self, value):
if value < 0:
print ("Error in input data!")
9
Object Oriented Programming I Chapter Four
else:
self.__salary = value
employee = Employee(5000)
print([Link]) # Output: 5000
[Link] = 7000 # Valid value
print([Link]) # Output: 7000
[Link] = -3000 # Raises ValueError
4.4.3 Restrict Access to Data
Using encapsulation, you can create attributes that are read-only, meaning they can
be accessed but not modified directly. For example:
class Product:
def __init__(self, name, price):
self.__name = name
self.__price = price
@property
def name(self):
return self.__name
@property
def price(self):
return self.__price
product = Product("Laptop", 1000)
print([Link]) # Output: Laptop
print([Link]) # Output: 1000
[Link] = 1200 # Raises AttributeError because
there's no setter
4.4.4 Restricting Access to Sensitive Data
Encapsulation helps protect sensitive information like passwords or account details
by making them private and only accessible via controlled methods. For example:
class BankAccount:
def __init__(self, balance):
self.__balance = balance # Private attribute
10
Object Oriented Programming I Chapter Four
def get_balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
else:
print("Invalid deposit amount")
def withdraw(self, amount):
if amount <= self.__balance:
self.__balance -= amount
else:
print("Insufficient funds")
account = BankAccount(1000)
[Link](500)
print(account.get_balance()) # Output: 1500
[Link](200)
print(account.get_balance()) # Output: 1300
11