Hello, Pythonistas πββοΈ welcome back. Today we are gonna be learning encapsulation and abstraction in python OOP.
.
Imagine you have a severe headacheπ€. You go to a doctor π§ββοΈ and he takes out a medicine π and starts explaining π£ how it will cure your headache by going into your stomach step by step, slowly.
What would you do?βοΈβοΈ
Well, I might even want to smash π€― his/her head with a chair πΊ and will say give me the medicine, I don’t want to know anything else.
This is what encapsulation and abstraction will do in OOP.
I mean not testing your patience till you smash them. Instead, they will save your’s and your fellow programmer’s time by letting you and them know what’s really required.
Let’s see how these can save you from becoming like that doctorπ§ββοΈ…
Contents
Previous post’s challenge’s solution
# It's a class that creates a window with a list of radio buttons that when clicked, displays the
# calories and price of the dessert.
from customtkinter import *
class App(CTk):
def __init__(self):
"""
The function creates a window with a title, geometry, and a list of desserts with their calories
and price.
"""
super().__init__()
self.title("Desserts, Calories, and $_$")
self.geometry("400x300")
# A list having desserts, their calories, and their price.
self.D = [
("Doughnuts", "300, 0.99"),
("Pumpkin Pie", "243, 5.24"),
("Fudge", "411, 14"),
("Brownies", "466, 8.47"),
("Cheesecake", "321, 13"),
]
# A customtkinter string variable to store calories and price of the selected dessert
self.selected = StringVar()
for dessert, calories in self.D:
# It's creating radio buttons that when clicked, will display the calories and price
# of the dessert.
self.rb = CTkRadioButton(self, text=dessert, variable=self.selected, value=calories, command=self.show_calories)
# Display the buttons with a padding(spacing) of 10 pixels
self.rb.pack(pady=10)
# Create an empty label to display the calories later
self.show = CTkLabel(self, text="")
self.show.pack()
def show_calories(self):
"""
It takes the dessert(radio button) selected, splits its calorie and price into a list, and then displays the first
item in the list (the calories) and the second item in the list (the price) in a label
"""
# Remove the previous display of the calories and price ($) (if any)
self.show.pack_forget()
# split calories and price($) into two
temp = self.selected.get().split(", ")
# Create a label to display the calories and price ($)
self.show = CTkLabel(self, text=f"I have {temp[0]} calories! And you would need ${temp[1]} to have me!")
# Display the label with a padding of 10 pixels
self.show.pack(pady=10)
if __name__ == "__main__":
app = App()
app.mainloop()
The solution is well ππ» documented so read the comments to understand it.
If you find it difficult to deal π€π» with then read the previous post first.
And if you still got any doubts π€ or want me to explain this in detail then comment in the comment section below.
What is Encapsulation?
Any object in OOP knows π§ something and does πββοΈ something. We saw this when we started the concept of OOP.
An object knows something because of variables and does something because of methods.
The concept of encapsulation
is binding data(variables) with methods.
This means we will have methods to access instance variables instead of directly accessing them.
To do this we need:
- a getter and
- a setter method.
As we either need to get or set the value of any variables. (even if we want to operate on them.)
We will also see a deleter here.
Let’s see the application of encapsulation in terms of working code using getter and setter methods.
getters and setters
Let’s make an employee class.
class Employee:
def __init__(self):
self.salary = 0
self.empnm = ""
# This is a setter for salary var
def set_salary(self, salary):
self.salary = salary
# This is a setter for empnm var
def set_name(self, name):
self.empnm = name
# This is a getter for empnm and salary var
def show(self):
print(f"Employee's name is {self.empnm} and the salary is {self.salary}")
emp1 = Employee()
emp1.set_name("John")
emp1.set_salary(10_000)
emp1.show()
Output:
Employee's name is John and the salary is 10000
Here we have made a class with βοΈ two variables and 3οΈβ£ three methods.
The β two variables are not β directly accessed instead we have used setter methods to give them some value.
And we have used a getter method to get the values.
One thing to be clear is that getter and setter are not compulsory names for methods.
These words just show that there are methods that get the values of variables and ones which set the values of variables.
I hope it’s clear just stick with the example if the words sound like a tongue twister.
Protected and Private variables
Public variables
The variables self.empnm
and self.salary
are both public variables. What I mean is even if we have methods to deal π€ with them we can directly access them outside the class.
Let’s try:
emp1 = Employee()
emp1.empnm = "John"
emp1.salary = 10_000
print(emp1.empnm)
print(emp1.salary)
Output:
John
10000
Such variables are called public variables.
But we do not need them in this case. Right?β
Now, how can we stop them from being called outside the class?
Protected variables
To make any public variable or method protected we need to add an underscore (“_
“) before them:
class Employee:
def __init__(self):
self._salary = 0
self._empnm = ""
def set_salary(self, salary):
self._salary = salary
def set_name(self, name):
self._empnm = name
def show(self):
print(f"Employee's name is {self._empnm} and the salary is {self._salary}")
Now, this is just a convention used by programmers, this doesn’t make the variables or methods inaccessible outside the class.
For making them more protected we have private π variables. Let’s take a look at them.
Private variables
Private π variables are still allowed to be accessed outside the class but not directly as in the above two methods.
To understand this in detail we will first make the two variables private. To do that add a double underscore (“__
“) before them:
class Employee:
def __init__(self):
self.__salary = 0
self.__empnm = ""
# This is a setter for salary var
def set_salary(self, salary):
self.__salary = salary
# This is a setter for empnm var
def set_name(self, name):
self.__empnm = name
# This is a getter for empnm and salary var
def show(self):
print(f"Employee's name is {self.__empnm} and the salary is {self.__salary}")
emp1 = Employee()
print(emp1.__empnm)
print(emp1.__salary)
The last two lines should print ""
and 0
.
As we have not given any values to these two variables yet so it will take the default value.
Output:
Traceback (most recent call last):
File "c:\python-hub.com\opps_encapsulation.py", line 20, in <module>
print(emp1.__empnm)
AttributeError: 'Employee' object has no attribute '__empnm'
We got an error instead of the desired π€© output.
Why??
Mangling
Because of name mangling in python.
It means that behind the scenes python changes self.__salary
or self.__empnm
to _Employee__salary
or _Employee__empnm
.
This isn’t the only way name mangling can happen.
- If you give more than 2 underscores before the attribute name or
- no more than 1 underscore after the attribute name the same will happen.
This means you can declare a private variable(attribute) in the following ways:
self.__salary
self.____salary
self.salary_
@property decorator
Encapsulation is all about binding methods and attributes together. The above ones are good ways to do this.
But there’s a better and more Pythonic way to get this done.
That is using the property decorator(It has nothing to do with lands). I’ll explain what decorators are later in this tutorial.
For now, they are used to modify the behavior of methods.
In encapsulation, we have getters and setters which are methods to access the attributes.
Here is, how we can use the @property
decorator:
class Employee:
def __init__(self):
self.__empnm = ""
# This is a getter for empnm var
@property
def empnm(self):
return self.__empnm
# This is a setter for empnm var
@empnm.setter
def empnm(self, name):
self.__empnm = name
emp1 = Employee()
emp1.empnm = "John"
print(emp1.empnm)
Remember to use the property decorator we need a protected or a private variable it doesn’t work with public variables.
Output:
John
The property decorator has made the method empnm
behave like an attribute. If you try to call it as a function it will throw an errorβΌοΈ.
This means we have an empnm
attribute and we can get and set its value without using getter and setter methods.
We can also give it a deleter which will delete the self.__empnm
attribute:
Output:
John
None
We have set the value of self.__empnm
to None as this is how we delete attributes in OOP.
If you want the last print statement to say "Name is not set yet"
. Then you can modify the empnm
method like this:
# This is a getter for empnm var
@property
def empnm(self):
if self.__empnm == None:
return "Name is not set yet"
return self.__empnm
So, we have seen what is encapsulation π and how to use encapsulation.
Let’s see why we need something like that…
why do we need to encapsulate?
There are a no. of benefits π to using encapsulation. I’ll elaborate on 3οΈβ£ three of them.
A change in name of the variable
Let’s say you change the name of an attribute(variable) from salary to empsalary.
Now you would need to make this change wherever you have used the salary variable directly.
It wouldn’t be a problem if you have used it in 2-5 places. But, imagine what if you have used it more than 100 or 1000 times?
It’s like if you have created an app like Instagram you need to make this change in around 547 million code blocks.
It would be impossible to make this change. Or say a pocket hazard πΈ to change this name.
Let’s say you can compromise π€ on changing the name and keep it the same.
Here’s the next problem…
Using Variables to compute value instead of storing
Every time you eat in a restaurant π¬ you have to pay a tax on your billπ΅.
Let’s define a taxpercent
variable in our restaurant class. (We have used this class in previous posts on oop we modified it too.)
self.taxpercent = 1
#You can use it directly from its object
cust1.taxpercent
This might seem to workπ. But the tax rates π are not the same. They depend upon the amount of bill that the customer has.
The tax might be calculated as:
def calculateTaxpercent(self):
if self.total_bill < 1000:
self.taxpercent = 1.0
elif self.total_bill < 5000:
self.taxpercent = 1.5
else:
self.taxpercent = 2.0
Now, the taxpercent
depends on the method instead of having a single value.
Say, the client directly accesses the taxpercent
variable. And the bill amount is more than 5000.
Now, the taxpercent
will have 1 instead of 2, as it is only defined once when the object is created. The logic inside calculateTaxpercent
method wouldn’t be executed at all.
Your business would doom to losses π in such a case.
It would work if you use a getter and a setter method for this simply or use a property decorator.
Data Validation
Any restaurant would have limited seats for customers. I mean there’s nothing like an infinity room, right?
So to get this let’s simplify the restaurant class so it becomes easier to understand the concept.
class Restaurant():
def __init__(self, RestName, maxcustomers):
self.restaurantName = RestName
self.maxcustomers = maxcustomers
self.customersList = []
def new_customer(self, name):
# Make sure that there is enough room left
if len(self.customersList) < self.maxcustomers:
self.customersList.append(name)
print('Welcome', name, 'to the', self.restaurantName, 'restaurant')
else:
print(f'Sorry {name}, but {self.restaurantName} already has the maximum of customers.')
Now let’s try to add 5 customers:
rest = Restaurant("Tasty Time", 5) #Yes only 5 customer's space
# Letting 5 customers inside
rest.new_customer("John")
rest.new_customer("Joe")
rest.new_customer("Fred")
rest.new_customer("Enid")
rest.new_customer("Susie")
Output:
Welcome John to the Tasty Time restaurant
Welcome Joe to the Tasty Time restaurant
Welcome Fred to the Tasty Time restaurant
Welcome Enid to the Tasty Time restaurant
Welcome Susie to the Tasty Time restaurant
Let’s try to add 6th one:
# Let's try to have 6th
rest.new_customer("Laura")
Output:
Sorry Laura, but Tasty Time already has the maximum of customers.
self.new_customer()
does all the validation needed to ensure that a call to have a new customer works correctly or generates an error message when needed.
Now imagine what if someone accesses the self.maxcustomers
directly in the client code and sets it to 500.
rest.maxcustomers = 500
Where do you think all these customers would go?? It’s 100 times what you have.
Another thing is what if the self.customersList
is directly accessed and a new customer is added this way:
rest.self.customersList.append("Jenny")
Where do you think Jenny will sit to eat??
Or even worse than all these. What if someone changes self.maxcustomers
to a string:
rest.maxcustomers = "5"
So now you know what’s encapsulation, how to implement it, and why it is so necessary.
Now, let’s see what’s abstraction and what it has got to do with encapsulation.
Abstraction
You’ll definitely be cooking food in your restaurant. But do you need to know how to grow veggies and grains or how to slaughter chicks?
No, because that’s unnecessary for you.
Abstraction says the same, hiding unnecessary details.
There are four major ways you can implement this concept (And not be like this doctor):
- Encapsulation
- Functional programming
- Libraries and frameworks
- Abstract classes
We already saw encapsulation.βοΈ
Functional programming is dividing your code into functions as we did before OOP.βοΈ
Libraries and frameworks are when you used the customtkinter without knowing the code that runs in the background when you use the class.βοΈ
We will take a look at abstract classes now.
@abstractmethod decorator
Let’s say you specialize in making weird π½ coffees β.
Namely:
- Unicorn π¦ Tears Latte
- The Nuclear Mocha
- Zombie π§ Brain Brew
- Fairy Dust Frappuccino
- Sasquatch Espresso
All of these are your specialty π.
But at the end of the day, all the coffees β have a small recipe in common: milkπ₯+coffeeπ«+sugarπ¬
So, let’s define a base coffee class and all of these as its subclass(don’t kill me we will cover inheritance in the very next post.)
To create an abstract class we need to use abc module’s ABC class. (kinda weird, maybe, it means abstract class):
from abc import ABC, abstractmethod
class coffee(ABC):
def __init__(self):
self.milk = "a cup"
self.coffee = "a table spoon"
self.sugar = "a tea spoon"
def make_base(self):
return f"{self.milk} of milk, {self.coffee} of coffee powder, and {self.sugar} of sugar powder."
@abstractmethod
def color(self):
pass
@abstractmethod
def defining_factors(self):
pass
Abstract classes have 2 types of methods:
- Concrete method: like __init__() and make_base() method. These methods can be directly used, without overriding, in the subclass(Unicorn Tears Latte and so on.)
- Abstract method: like color() and defining_factors(). These methods are meant to be overridden. That means you need to define them in all the subclasses.
Let’s define a UnicornTearsLatte
class to understand this more clearly.
class UnicornTearsLatte(coffee):
def color(self):
return "This special coffee's color would be pink and white."
utl = UnicornTearsLatte()
print(utl.color())
Output:
Traceback (most recent call last):
File "c:\python-hub.com\opps_encapsulation.py", line 5, in <module>
utl = UnicornTearsLatte()
TypeError: Can't instantiate abstract class UnicornTearsLatte with abstract method defining_factors
Why did this happen? You never called the defining_factors method with utl then why doesn’t it work?
This is because abstract methods are compulsory to be defined inside the subclass. If you add it:
class UnicornTearsLatte(coffee):
def color(self):
return "This special coffee's color would be pink and white."
def defining_factors(self):
return f"A magical, colorful latte that changes colors as you sip it. \n It is made of {self.make_base()}. \n And a special ingredient..."
utl = UnicornTearsLatte()
print(utl.color())
print(utl.defining_factors())
Output:
This special coffee's color would be pink and white.
A magical, colorful latte that changes colors as you sip it.
It is made of a cup of milk, a table spoon of coffee powder, and a tea spoon of sugar powder..
And a special ingredient...
This concept can be confusing π€ if you aren’t aware of inheritance. And that’s definite if you are following this tutorial.π
But in any case, you just need to read the next post to get a better understanding of this concept. (And don’t kill πͺβ οΈ me)
Here’s the official documentation of using abstract methods in python.
Conclusion
Here we come to an end π of this post.
Today we discussed that binding data with methods is encapsulation. And how this is done βοΈ in python. Along with that why in the world π do we need to implement it?
Then we saw abstraction π what is it and how and why it is used?
So, congratulations π you wouldn’t ever be like the doctor π¨ββοΈ to your fellow or senior or junior programmersπ©βπ».
Challenge π§ββοΈ
Your challenge for this post is to make a Shape class this should be an abstract class.
It should have two abstract methods:
- Calculate area
- Calculate perimeter
Also, make its subclass Rectangle and use it.
This was it for the post. Comment below suggestions if there and tell whether you liked it or not. Do consider sharing this with your friends.
See you in the next post till then have a great time. Bye Byeππ