Python as an Object Oriented Programming Language

Python as an Object Oriented Programming Language

Whether you’re a newbie at programming or have a certain level of expertise, you’ve probably heard about Python. It’s a very popular programming language and for good reasons.

Python is known for its easy-to-understand syntax, as it closely resembles human language. The programming language has a large, active community. Hence, there is a great supply of documentation, tutorials, and forums for beginners. It’s not surprising that most beginners take Python as their first programming language.

Also, Python is quite versatile, compared to other programming languages. It’s used in areas such as:

  • Web development

  • Data analysis

  • Artificial intelligence

  • Scientific computing

  • Machine learning and more

Python is an object-oriented programming (OOP) language. The language was built with object-oriented principles in mind and these OOP features allow programmers to build complex applications and systems using clear logical organizations.

Therefore, as a Python programmer, it’s essential to understand OOP concepts as they are deeply connected to how Python handles data and functions under the hood.

This article will help you:

  • Understand the basic principles of OOP in Python

  • Learn how to work with classes and objects

  • Learn how to implement inheritance and other core OOP concepts

Prerequisites

To sail smoothly with this article, you’ll need the following:

Basics of Object-Oriented Programming in Python

Object-oriented programming (OOP) is a programming paradigm that involves building applications using objects that contain data and methods rather than using functions and logic. This feature allows programmers to create modular and reusable code. Programs built using OOP concepts are more manageable and efficient, as there is less redundancy.

Furthermore, OOP in Python helps manage complexity by modeling real-world units as software objects that have some data or operations associated with them. For example, you could create a program that models a car object. This car could have features or attributes like color, model, and mileage. Your car could also perform operations like start, stop, drive, and honk.

Don't worry if you still need to get a good grasp of the concepts. You'll get a clearer picture when we discuss classes and objects.

What are Classes and Objects in Python?

A class is simply a template for creating objects. Python programmers use classes to define data structures by putting together attributes and methods (functions) as a single unit. The methods operate on the data.

Python classes don't contain actual data. Think of it as a blueprint that programmers use to build objects that will contain data.

You can create a class using the 'class' keyword with the class name and a colon. Python identifies whatever you indent below the colon as part of the class. For example, let's create a simple car class below:

# oop.py

class Car:
    pass

For now, your Car class has a single statement, the pass keyword. Python programmers use this keyword as a placeholder for where code will go later. If you run this simple program on your computer, it'll not return any error. The code looks boring now, but you'll spruce it up shortly with properties a car object should possess.

Now that you have a good idea of Python classes, what are Python objects?

Python Objects

Remember, you learned before now that classes are simply templates, right? Well, objects are instances created from these templates. You can think of classes as the blueprint or floor plan for a house. Objects are the buildings erected using the blueprint, having all the characteristics defined in the plan.

In addition, when you create a class, your computer doesn't allocate any memory space until you create an instance of the class (object).

Now, you'll spruce up the Car class you created above. You'll achieve this by adding the .__init__() method. It's a special method, and it's also called a “constructor”. Its primary purpose is to declare and initialize each attribute every instance of your class would have.

Your Car class would have attributes such as color, model, and mileage. These attributes are called instance attributes. However, you can also create attributes outside the .__init__() method. This kind of attribute comes directly under the class, and it's called a class attribute. You'll learn more about class attributes later.

Example

Update your Car class and add the color, model, and mileage attributes. Then, create an instance of the class:

# oop.py

class Car:
    def __init__(self, color, model, mileage):
        self.color = color
        self.model = model
        self.mileage = mileage

# Creating an instance of Car
car_1 = Car("blue", "XVZ23", 124)

# Accessing attributes
print(car_1.color)
print(car_1.model)
print(car_1.mileage)

Output:

blue
XVZ23
124

In this example:

  • The Car class has a .__init__() method with self, and the parameters, color, model, and mileage

  • self refers to the current instance of the class (car_1 in this case), and this is used to access the class attributes (color, model, and mileage)

  • When creating the instance, car_1, the arguments, blue, XVZ23, and 124 are passed to .__init__()

  • self.color, self.model, and self.mileage are initialized with these values

What are the Core OOP Principles?

Object-oriented programming is built on 4 core principles, and the Python programming language adheres to them. You’ll learn briefly about these principles in this section and get more details later in the article.

The principles are:

  1. Inheritance: You can create two different classes and have one of them access the attributes and methods of the other. This situation is called inheritance and allows code reuse, among other functionalities. The class that inherits from the other is called the child class, while the one being inherited from is the parent or base class.

  2. Polymorphism: The term refers to a situation where something can exist in more than one form. Concerning OOP, polymorphism allows Python programmers to use a method or function to achieve more than one goal, depending on the specific type of object one is working with.

    For instance, think of yourself as a pet shop owner. You have dogs, cats, and birds in this shop. You can ask these animals to make a sound, and of course, each type of animal will sound differently. However, you don’t know how these animals make their sounds; you only give a command: “Make a sound,” and they do it in their way. Polymorphism in programming is similar.

  3. Encapsulation: This principle refers to the process of creating data structures by putting together data (attributes) and operations (methods) performed on these data as one unit (class). The class often restricts access to some of its components from external interactions. Think of it as protecting the integrity of the object by exposing what’s necessary.

  4. Abstraction: This principle means keeping away the complexity of a program and only exposing the necessary parts. For example, your car object represents a complex machine in the real world, but you’re able to simplify it in the software world using OOP. A typical example of abstraction is the fact that you don’t need to understand the technical intricacies of a car engine to drive a car.

How Do You Work with Classes and Objects?

The process of creating a new object from a class is called instantiating a class. You can instantiate a class by typing the name of the class with opening and closing parentheses:

House()

For each time you instantiate a class, a memory address is allocated to hold the object. You’ll understand this better with the simple demonstration below:

# test.py

class House:
    pass

print(House())

Output:

<__main__.House object at 0x1043b1430>

The output above indicates that you have created an object, House, at the memory address: 0x1043b1430. Note that the memory address will be different on your local machine.

Instantiate the House class on the Python console or run the test.py file once more to see what happens:

<__main__.House object at 0x1043b1430>

>>> House()
<__main__.House object at 0x1043b1b20>

This time, the House instance is in a new memory address. This instance is different from the first one you created. Your computer assigns a new memory address to every new instance of the class you create.

What are Attributes and Methods in Python?

Attributes are the features or properties possessed by an object. On the other hand, methods are functions defined within the scope of a class. Methods can operate or perform actions on the data of an object.

You define attributes using variables and define methods using functions inside the class. Every method has self as its first parameter, which represents an instance of the class.

Now spruce up your Car class with the methods, drive() and honk(), and run the oop.py file:

# oop.py

class Car:
    def __init__(self, color, model, mileage):
        self.color = color
        self.model = model
        self.mileage = mileage

    def drive(self):
        return "Vroom Vroom!"

    def honk(self):
        return "Beep Beep!"

# Creating an instance of Car
car_1 = Car("blue", "XVZ23", 124)

# Accessing attributes and calling methods
print(car_1.color)
print(car_1.mileage)
print(car_1.honk())
print(car_1.drive())

Output:

blue
124
Beep Beep!
Vroom Vroom!

This example shows how you can use methods to perform operations on the car object. The two methods return strings when you call them. Python programmers often create methods that return strings that offer useful information about the class instance.

Now, you’ll learn how class attributes differ from instance attributes.

Class Attribute Vs. Instance Attribute

There are times when you need to define attributes outside the .__init__() method. These kinds of attributes are shared by every instance of the class and are called class attributes.

Create a Dog class and assign a class attribute:

# dogs.py

class Dog:
    # Class attribute
    species = "Canis familiaris"

    def __init__(self, name):
        self.name = name

    def bark(self):
        return "Wooowoow"

# Creating instances
dog_1 = Dog("Max")
dog_2 = Dog("Leo")

# Accessing class attribute
print(dog_1.species)
print(dog_2.species)

Output:

Canis familiaris
Canis familiaris

Here, species is a class attribute that every instance of the class has access to. Its definition is at the class level.

You’re now acquainted with class attributes. So, what are instance attributes?

Instance Attributes

These are attributes that are unique to specific instances of a class. Instance attributes are defined within the .__init__() method and involve the self keyword. You’re already familiar with this type of attribute.

Create another Dog class, but this time, only use instance attributes:

# dogs2.py

class Dog:
    def __init__(self, name, species):
        # Instance attributes
        self.name = name
        self.species = species

    def bark(self):
        return "Wooowoow"

# Creating instances
dog_1 = Dog("Max", "Canis familiaris")
dog_2 = Dog("Leo", "Canis lupus")

# Accessing class attribute
print(dog_1.species)
print(dog_2.species)

Output:

Canis familiaris
Canis lupus

Unlike the dogs.py example, in dogs2.py, species is an instance attribute, and it’s unique to each Dog instance.

How to Implement Inheritance and Polymorphism in Python

You’ve learned quite a bit about inheritance and polymorphism. You know that inheritance in OOP allows a class to have access to the properties and behaviors of an existing class. Also, polymorphism allows different class instances to be treated as instances of a common superclass.

You’ll learn how to implement these principles.

Inheritance

Different types of animals make up the animal kingdom. Although these animals may differ in a lot of ways, they still share some characteristics. Use this simple analogy to implement the inheritance principle.

Create an Animal class alongside two other classes, Dog and Cat. The first class will be a superclass (parent class), while the other two classes will be subclasses (child classes):

# inheritance.py

# A superclass that other classes inherits from
class Animal:
    def speak(self):
        pass

# A subclass that inherits from Animal
class Dog(Animal):
    def speak(self):
        return "woof!"

# A subclass that inherits from Animal
class Cat(Animal):
    def speak(self):
        return "meow!"

In inheritance.py, the Dog and Cat classes inherit the speak() method from the Animal superclass. However, each of the subclasses can speak in their specific ways.

Polymorphism

Think of shapes like rectangles and circles. You can calculate the area of each shape, but the formula for calculating the area of both shapes isn’t the same. Rectangles use length x width, while circles use π × radius².

However, by applying the principle of polymorphism, you can calculate the area of the shape using a common method, calculate_area(). The method acts differently depending on the shape you’re working on. You’ll see this in action below:

# polymorphism.py

class Rectangle:
    def __init__(self, length, width):
        self.length = length
        self.width = width

    def calculate_area(self):
        return self.length * self.width

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def calculate_area(self):
        return 3.14 * self.radius * self.radius

# Using polymorphism
shapes = [Rectangle(5, 4), Circle(6)]
for shape in shapes:
    print("Area: ", shape.calculate_area())

Output:

Area:  20
Area:  113.03999999999999

In polymorphism.py above, the calculate_area() method acted differently with the Rectangle and Circle classes. Although the method has the same name for both classes, its action isn’t the same. When you call it on a rectangle, it uses length x breadth, and when you call it on a circle, it uses π × radius². That’s what polymorphism is about.

How to Implement Encapsulation and Abstraction in Python

Before now, you’ve had a brief introduction to encapsulation and abstraction in Python. You’ll recall that encapsulation means putting attributes and methods together as a single entity. At the same time, abstraction means hiding the details of a class and showing only essential parts of the object.

You’ll see these principles in practice.

Encapsulation

Think of this OOP principle as a treasure chest. It protects valuable items from outside interference, keeping them safe and secure inside.

Have you ever wondered how the bank stores your money securely? You can access your funds via the ATM or online banking, but you can’t go to the bank’s vault to modify your balance. Encapsulation works similarly.

Note that Python programmers can make an attribute inaccessible outside the class by adding a double underscore __ prefix to the name of the attribute. Such attributes are called private attributes.

Here’s a good example of encapsulation and how to make an attribute private:

# encapsulation.py

class BankAccount:
    def __init__(self, initial_balance):
        self.__balance = initial_balance

    def deposit(self, amount):
        self.__balance += amount

    def withdraw(self, amount):
        if amount <= self.__balance:
            self.__balance -= amount
        else:
            print("Insufficient funds")

    def get_balance(self):
        return self.__balance

    def balance(self):
        return self.__balance

# Instantiating the class and calling a method
my_account = BankAccount(1000)
my_account.deposit(500)
print(my_account.get_balance())

Output:

1500

In encapsulation.py:

  • A user can interact with the BankAccount class using the methods deposit(), withdraw(), and get_balance()

  • The user doesn’t need to understand the intricate details of how financial institutions store and manage funds in the background

However, you can’t directly access the __balance attribute outside the method. Attempting to access this attribute will throw an AttributeError.

Instantiate the BankAccount class once more, using the same initial_balance as the last example, and, try to access the __balance attribute directly:

# Attempting to directly access the _balance attribute
my_account = BankAccount(1000)
print(my_account.__balance)

Output:

Traceback (most recent call last):
  File "/Users/emmanueloyibo/Desktop/TechWriting/2. Python_OOP/encapsulation.py", line 31, in <module>
    print(my_account.__balance)
AttributeError: 'BankAccount' object has no attribute '__balance'

In the example above:

  • Attempting to access the __balance attribute using my_account._balance results in an error

  • __balance is a private attribute, and you can’t access it directly outside the method

Abstraction

Imagine you want to brew some coffee. You don’t need to know how the machine grinds the coffee or heats the water. You only need to know what button to press to get your cup of coffee. Similarly, in OOP, abstraction hides the complex inner workings of code and only reveals the part users need to interact with.

Now, you’ll model a coffee machine:

# abstraction.py

class CoffeeMachine:
    def __init__(self):
        self.water_level = 0
        self.beans_level = 0

    def add_water(self, amount):
        self.water_level += amount

    def add_beans(self, amount):
        self.beans_level += amount

    def make_coffee(self):
        if self.water_level >= 1 and self.beans_level >= 1:
            print("Coffee is brewing...")
            self.water_level -= 1
            self.beans_level -= 1
            print("Enjoy your coffee!")
        else:
            print("Sorry, not enough water or beans.")

# Creating an instance and calling methods
coffee_machine = CoffeeMachine()
coffee_machine.add_water(1)
coffee_machine.add_beans(1)
coffee_machine.make_coffee()

Output:

Coffee is brewing...
Enjoy your coffee!

In abstraction.py above:

  • Users interact with the CoffeeMachine class via the methods (add_water(), add_beans(), and make_coffee())

  • Users don’t need to know the complex code that makes each of the methods to run when called

Conclusion

In this piece, you’ve explored Python as an object-oriented language. Let’s recap the key takeaways:

  • Python, as an object-oriented language, allows programmers to organize their code around classes and objects.

  • The core OOP principles are inheritance, polymorphism, encapsulation, and abstraction.

  • Inheritance allows classes to access attributes and methods of other classes. Polymorphism allows objects of different classes to be treated as objects of a common class.

  • Encapsulation protects the internal states of objects, and abstraction hides the intricate details of objects.

As you continue on your learning journey, don’t hesitate to explore more concepts. Also, endeavor to collaborate and share your knowledge with the Python community.

References

You can explore the following resources to deepen your knowledge of OOP in Python further:

Thanks for reading! If you found this article helpful (which I bet you did 😉), got a question or spotted an error/typo... do well to leave your feedback in the comment section.

And if you’re feeling generous (which I hope you are 🙂) or want to encourage me, you can put a smile on my face by getting me a cup (or thousand cups) of coffee below. :)

Also, feel free to connect with me via LinkedIn.