Abstraction And Encapsulation In Python OOP: Complete Explanation

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πŸ§‘β€βš•οΈ…

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πŸ‘‹πŸ˜Š

Leave a Reply