The 4 principles of OOP in Python and how to implement them in your code

The 4 principles of OOP in Python and how to implement them in your code

Programming is about storing and utilizing data. We store data using primitive data types such as numbers and variables. We utilize that data (i.e., perform actions with it) using functions such as print(), range(), and len().

Object-oriented programming (OOP) is a powerful programming style that handles both tasks: it stores attributes as data and does something with that data using its own functions, called methods.

To learn OOP, you need to first understand its logic.

In this article, we look at the four basic principles of OOP in Python and how to implement them in your program.

OOP is a means of structuring data so that related behaviors and properties are bundled into individual objects. This approach has many benefits, including easier maintenance, better code organization, and greater flexibility.

OOP runs on four basic principles: inheritance, abstraction, polymorphism, and encapsulation. Before we dive deeper into these principles, let’s look at classes and objects and how we create them in OOP.

In Python, everything is an object and therefore a member of some class. Classes and objects are the fundamental components that makeup OOP.

A class is like a template, a blueprint, for objects. It has attributes (characteristics) and methods (behaviors). For example, the class Person does not refer to a specific person exactly. It only describes what Person is and can do, through its attributes and methods. When you name a specific person, such as Ben, you create an object of that class, with attributes that identify it as such.

Creating classes

In Python, a class is created using the class keyword, followed by the name of the class:

class Person:
    pass

This creates a new class called Person. The pass keyword is a placeholder for the class definition, which we’ll fill in later.

Defining attributes and methods

Once we have created a class, we can define its attributes and methods. Attributes are variables that store data, while methods are functions that perform actions on the data.

>>> class Person:
...     def __init__(self, name, age):
...         self.name = name.title()
...         self.age = age

We defined a class Person and set its attributes name and age with the __init__() method, a special function that tells the program how to initialize an object when it’s created. In our example, the method takes two parameters, name and age, which are used to set the object's attributes. The string method .title() is just to ensure that the name returned is capitalized.

Creating objects

Once we have defined a class, we can create objects of that class using the class name followed by parentheses.

Note: Since we defined our class with two parameters, we are required to provide an equal number of arguments to fill their place when we call the class.

Example:

>>> jerry = Person('jeremiah', 15)
>>> print(f"Hi, my name is {jerry.name}. I'm {jerry.age}.")
#Output
Hi, my name is Jeremiah. I'm 15.
>>>

First, we created a new object, jerry, of the class Person with two arguments, jeremiah and 15. Then, we call the print() method on the object, with its attributes set to jeremiah (as name) and 15 (as age). This prints a message introducing Jeremiah.

Implementing the principles of OOP in Python

Now let’s look at each of the principles we introduced earlier and some examples of how they can be implemented.

Inheritance

In Python, as in the real world with babies and their parents, a child class inherits the behavior and properties of its parent class. Inheritance is the practice of creating new classes from existing ones. The new class inherits all the attributes and methods of the parent class.

Check this out:

>>> class Pet:
...     def __init__(self, name, color):
...         self.name = name
...         self.color = color
...
>>> class Dog(Pet):
...     def speak(self):
...         print("Bark ... woof! woof!")
...
>>> tommy = Dog('Tommy', 'brown and white')
>>> print(f"My pet's name is {tommy.name}. She's {tommy.color}")
>>> tommy.speak()
My pet's name is Tommy. She's brown and white
Bark ... woof! woof!

The child class Dog acquires all attributes of the parent class Pet. That's why tommy, an object of Dog, can access all the properties of its parent class, Pet, having inherited them. And as the example shows, the object can also have an attribute that it doesn’t share with the parent, such as speak in our example.

Polymorphism

Polymorphism, which simply means “having different forms,” is the idea of having a single object that can be used in multiple ways. For example, we can use the + operator to add numbers and also concatenate strings.

Example:

nums = 1 + 2
oop = 'object-oriented' + ' programming.'
print(nums)
print(type(nums))
print(oop)
print(type(oop))
#Output:
3
<class 'int'>
object-oriented programming.
<class 'str'>

Check out polymorphism at work in class methods.

class Dog:
    def color(self):
        print('My dog is brown.')
    def speak(self):
        print('My dog barks!')

class Tigger:
    def color(self):
        print('This tigger is yellwo with black strips.') 
    def speak(self):
        print('This tigger roars!')
dog = Dog()
tigger = Tigger()
for info in dog, tigger:
    info.color()
    info.speak()

The output of the above code:

#Output
My dog is brown.
My dog barks!
This tigger is yellwo with black strips.
This tigger roars!

Encapsulation

Think about capsules in medicine. A range of dosage forms is enclosed in a stable shell called capsules, producing a single-unit drug dosage. Similarly in OOP, the attributes and functions we define remain enclosed in a capsule called class. The class’s data cannot be accessed except by the methods defined in that class. We use encapsulation to make data private or protected. With encapsulation, you can prevent sensitive data such as passwords and account info from being altered.

Most object-oriented languages use access modifiers to restrict access to the variables and methods of a class. In Python, we use single _ and double __ underscores to determine access control for data in a class. And a class in Python has three types of access modifiers: public, protected, and private. Public data is accessible from anywhere in the program. Protected data is accessible from within the class and its subclasses. Private data is accessible only within the class it’s defined. We use a single underscore to represent protected data, double to represent private data.

class User:
    def __init__(self, name, age, password):
       self.name = name # name is public data
       self._age = age  # age is protected data
       self.__password = password #password is private data
ben = User('Benny Mako', 30, 'my_password')
print(f"User's name is {ben.name}")
print(f"User's age is {ben._age}")
print(f"User's password is {ben.password}")

The output:

User's name is Benny Mako
User's age is 30
Traceback (most recent call last):
  File "c:\Users\USER\.vscode\py_crash_course\functions\oop.py", line 44, in <module>
    print(f"User's password is {ben.password}")
                                ^^^^^^^^^^^^
AttributeError: 'User' object has no attribute 'password'

Abstraction

Abstraction is about keeping your program simple and reducing the complexity of your code by hiding unnecessary background or implementation details from the user, showing them only what is important.

To use abstraction, we must first import the ABC method from the abc module. The unique thing about abstract classes is that they don’t have any implementation of their own, so the subclasses that inherit from them must have their own implementation of the method.

from abc import ABC
class Phone(ABC):
    def battery_capacity(self):
        pass
class Andriod(Phone):
    def battery_capacity(self):
        print("My LC7 battery is 6k mAh")
class Iphone(Phone):
    def battery_capacity(self):
        print('My iPhone 13 battery is 5k mAh')
class Pixel(Phone):
    def battery_capacity(self):
        print('My Pixel 7 battery is 4.3k mAh')
lc7 = Andriod()
iphone = Iphone()
pixel = Pixel()
lc7.battery_capacity()
iphone.battery_capacity()
pixel.battery_capacity()

Here, we have an abstract class Phone, signaled by the label ABC. The class Phone has an abstract method called battery_capacity(), but it has no definition because abstract methods are not defined. They remain empty, forcing the subclasses that are inheriting the abstract class to provide their own implementation for the method, as shown in the example.

OOP is a powerful programming paradigm with several benefits. Because it relies on real-world, concrete objects, it is therefore very relatable and easy to visualize. The concepts of objects, inheritance, and abstraction relate very closely to real-world experiences, as we have seen in the examples above. With OOP, programmers can write reusable code and large programs become far less complicated to work with or debug.