TL;DR
Encapsulation refers to the process of hiding the implementation details of a process, as well as the data that an object might have.
We can encapsulate attributes (meaning, make them private) by using double underscore (__) in the name of the attribute.
class Person:
def __init__(self, public_name, private_name):
self.public_name = public_name
self.__private_name = private_nameWe can also encapsulate methods, using the double underscore technique. The use-case for method encapsulation, however, is different: private methods are generally intended for internal use only, such as helper methods that a client doesn’t need to know about:
class Recipient:
def __init__(self, name: str, email: str):
self.__name = name
if self.__check_email(email):
self.__email = email
else:
raise ValueError("The email address is not valid")
def __check_email(self, email: str):
# A simple check: the address must be over 5 characters long
# and contain a dot and an @ character
return len(email) > 5 and "." in email and "@" in emailExplanation
In object oriented programming the word client comes up from time to time. This is used to refer to a section of code which creates an object and uses the service provided by its methods. When the data contained in an object is used only through the methods it provides, the internal integrity of the object is guaranteed. In practice this means that, for example, a BankAccount class offers methods to handle the balance attribute, so the balance is never accessed directly by the client. These methods can then verify that the balance is not allowed to go below zero, for instance.
An example of how this would work:
class BankAccount:
def __init__(self, account_number: str, owner: str, balance: float, annual_interest: float):
self.account_number = account_number
self.owner = owner
self.balance = balance
self.annual_interest = annual_interest
# This method adds the annual interest to the balance of the account
def add_interest(self):
self.balance += self.balance * self.annual_interest
# This method "withdraws" money from the account
# If the withdrawal is successful the method returns True, and False otherwise
def withdraw(self, amount: float):
if amount <= self.balance:
self.balance -= amount
return True
return False
peters_account = BankAccount("12345-678", "Peter Python", 1500.0, 0.015)
if peters_account.withdraw(1000):
print("The withdrawal was successful, the balance is now", peters_account.balance)
else:
print("The withdrawal was unsuccessful, the balance is insufficient")
# Yritetään uudestaan
if peters_account.withdraw(1000):
print("The withdrawal was successful, the balance is now", peters_account.balance)
else:
print("The withdrawal was unsuccessful, the balance is insufficient")Maintaining the internal integrity of the object and offering suitable methods to ensure this is called encapsulation. The idea is that the inner workings of the object are hidden from the client, but the object offers methods which can be used to access the data stored in the object.
Maintaining state
Because the integrity of an object means that its state is always acceptable, we must ensure that the attributes of that object are acceptable. For example, an object that represents a date must never have 13 as the value of the month, or a person must not have negative values as their age.
Consider the following:
```python
class Student:
def __init__(self, name: str, student_number: str):
self.name = name
self.student_number = student_number
self.study_credits = 0
def add_credits(self, study_credits):
if study_credits > 0:
self.study_credits += study_creditsHere, the Student object offers its clients a method called add_credits, which allows credits to be added and to ensure that object.study_credits remains above zero.
Nonetheless, this can easy break, since it is still possible to access the study_credits attribute directly:
sally = Student("Sally Student", "12345")
sally.study_credits = -100 # This does work, but sets the attribute to an Undesired StatePrivate classes
In OOP, it is common for classes to hide their attributes by making them private. In Python this is accomplished by adding two underscores (__) to the beginning of an attribute name:
class CreditCard:
# the attribute number is private, while the attribute name is accessible
def __init__(self, number: str, name: str):
self.__number = number
self.name = nameTrying to access the double-underscored attribute will cause an error:
card = CreditCard("123456","Randy Riches")
print(card.__number)
AttributeError: 'CreditCard' object has no attribute '__number'Note
By design, Python has no truly private attributes, and there are ways around the double underscore method. Other languages, like Java, have truly private variables, but we can simply think of Python ones in the same way.