Object Oriented Programming In Python


Object-oriented programming (OOP) centers software design around objects, which are instances of classes. Transforming abstract ideas or real-world items into objects with attributes and functions offers a mechanism to structure and organize code. By classifying code into reusable objects with clear relationships, object-oriented programming encourages modular, scalable, and maintainable code. Due to its capacity to represent complicated systems, improve code reusability, and promote cooperation in extensive software development, it has grown in popularity. There are many programming languages that support OOP paradigms, including Python.

Classes and Objects

Classes can be referred to as user-defined data types serving as the building blocks for specific objects, attributes, and methods. They describe what a certain type of object will look like.
Objects are instances of a class that were generated using data that was precisely defined. Objects can be abstract concepts or real-world things. The description is the only item that is first defined when a class is created.

Declaring a Class in Python

Class in Python is declared using the class keyword with the following syntax:

class SomeClassName:

# class body

In the declaration above, SomeClassName is the name of the class. The class body is indented as per Python's PEP8 standard and contains the attributes and methods of the class. For more details on Python classes, please refer to the Python documentation.

Implementing Properties in a Class

Python classes can define special methods that be accessed similarly to attributes using properties. They give you a level of abstraction and control over the behavior of these attributes by allowing you to encapsulate the access and modification of class attributes. The @property decorator is used to implement properties, and optional setter and deleter methods are also available. If interested, read more about Python decorators. The essential elements of properties are as follows:

Getter: The getter method is in charge of obtaining the property's value. It bears the same name as the property and is embellished with @property. The getter method is automatically invoked whenever you access the property.

Setter: To change a property's value, use the setter method. It has the decoration @<property_name> on it.setter where property_name is the property's name. When you give the property a value, the setter method is invoked.

Deleter: The property will be deleted by the deleter method. It has the decoration @<property_name> on it.deleter. When you delete the property, the deleter method is invoked.

Below is an implementation example of properties in a Python class:

class SomeClass:
    def __init__(self):
        self._some_property = None

    @property
    def some_property(self):
        return self._some_property

    @some_property.setter
    def some_property(self, value):
        # Additional logic
        self._some_property = value

    @some_property.deleter
    def some_property(self):
        # Cleanup logic
        del self._some_property

The above illustration shows the implementation only. A good example could be when defining the properties of a circle using the Circle class shown in the snippet below.

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

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value <= 0:
            raise ValueError("Radius must be positive.")
        self._radius = value

    @radius.deleter
    def radius(self):
        del self._radius

You may manage how attributes are accessible, changed, and removed in your class using properties, giving users of your class a clear and consistent user interface. To avoid naming conflicts while utilizing properties, keep in mind that it's crucial to name the property and the underlying attribute independently.

Initializing Objects

The __init__() special method is used to initialize objects in Python. Every time a new object is formed, a particular method called __init__() is called. It is employed to set the object's attributes to zero i.e. the initial state of the object and perform any necessary setup or initialization tasks..

A reference to the freshly generated object is the only input for the __init__() method. Usually, this parameter goes by the name self.

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

We can create instances of the Rectangle class above as shown below:

rectangle1 = Rectangle(123, 456)
rectangle2 = Rectangle(750, 550)

And the object attributes can be accessed by printing the instances created above:

print(rectangle1.length) # output will be length of first rectangle, 123

print(rectangle2.width) # output will be width of second rectangle, 550

You can make sure that objects of your class are created with the relevant data or state by configuring the __init__() method.
An effective tool for initializing an object's characteristics is the __init__() method. Any class you build should have a __init__() method, as this is a recommended practice.

Class and Instance Variables

Variables are used to store data within a class. The two types of variables in Python serve different purposes and they are:

Class variables: All instances of a class share class variables. They are prefixed by the class keyword and defined outside of any methods or constructors. Class variables are used to store data that is common to all instances of the class. Below is an illustration of the class variable species.

Instance variables: Each instance of a class has its own instance variables. They are prefaced by the self keyword and declared within a method or constructor. Instance variables are typically used to store unique data or state for each object. The below example illustrates the instance variable inside the __init__() method.

class Person:
    # Class variable
    species = "Human"

    def __init__(self, name):
        # Instance variable
        self.name = name

    def say_hello(self):
        print("Hello, my name is {} and I am a {}.".format(self.name, Person.species))

In OOP, instance variables and class variables are both crucial ideas. Instance variables are used to store data specific to each instance of a class, whereas class variables are used to store data shared by all instances of a class. Depending on the particular requirements of the software, either a class variable or an instance variable should be used.

Implementing Methods in a Class

Creating functions inside of a Python class is necessary for method implementation. These methods specify how the class's instances (or objects) should behave and what they can do. 

Types of Methods

There are three types of methods commonly used in Python classes: instance methods, class methods, and static methods.

Instance methods: Most commonly used types of methods, have access to instance variables and other instance methods. They take the self parameter which points to an instance of the class and which gives them a lot of power when it comes to modifying an object's state. In the example below, say_hello() and greet() are instance methods defined within the Person class. They operate on individual instances of the class and can access the instance variables (self.name) to perform actions or provide information.

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

    def say_hello(self):
        print(f"Hello, my name is {self.name}.")

    def greet(self, other_person):
        print(f"Hello, {other_person}! My name is {self.name}.")

# Creating instances of the Person class
person1 = Person("Joseph")
person2 = Person("Josephine")

# Calling instance methods
person1.say_hello()           # Output: Hello, my name is Joseph.
person2.say_hello()           # Output: Hello, my name is Josephine.
person1.greet("Eve")          # Output: Hello, Eve! My name is Joseph.
person2.greet("Mary")         # Output: Hello, Mary! My name is Josephine.

Class methods: These methods have access to class-level variables and can modify them. They are defined using the @classmethod decorator and take the cls parameter, referring to the class itself instead of the instance as in the previous. Let us create a factory method using the class method example shown below where we will create a Person class object using the class method.

from datetime import date

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

    @classmethod
    def compute_age(cls, name, year_of_birth):
        # set it as a age
        # return new object
        return cls(name, date.today().year - year_of_birth)

    def show(self):
        print(self.name + "'s age is: " + str(self.age))

joseph = Person('Joseph', 24)
joseph.show() # Output: Joseph's age is: 24

# create new object using the factory method
josephine = Person.compute_age("Josephine", 1996)
josephine.show() # Output: Josephine's age is: 27

Static methods: Such methods do not have access to the instance or class variables and are defined using @staticmethod decorator. They are independent of any instances or class and mainly act as utility functions or operations. An example of a static method is shown below which takes two parameters and returns their sum.

class MathUtil:
    @staticmethod
    def add_numbers(a, b):
        return a + b

# Calling static method
result = MathUtils.add_numbers(5, 3)
print(result)  # Output: 8

In conclusion to types of methods, instance methods can access the instance through self and require a class instance. A class instance is not necessary for class methods. They are unable to access the instance (self), but they can use cls to access the class as a whole. Cls and self are not accessible to static methods. They function similarly to conventional functions but are part of the class's namespace. Static and class methods exchange information and, to an extent, enforce the developer's intent for class design. This may be advantageous for maintenance. An essential component of object-oriented programming is methods. You may put functionality into reusable sections of code thanks to them. Your code will become more organized and easier to maintain as a result.

Information Hiding

In OOP, information hiding refers to the technique of limiting access to specific data or methods of a class from outside the class. Access modifiers, which regulate the visibility of class members, can be used to achieve this. Information hiding is achieved in Python using the concept of Encapsulation.

Encapsulation

Encapsulation is the practice of limiting access to data (attributes) and methods that operate on those attributes within a class. By merely exposing the essential interface to the outside world, you can hide the internal implementation and intricacies of a class. This concept can be achieved in Python using the following:

Getters and Setters: They have been illustrated in previous examples. You can provide public methods (getters) to retrieve the values and (setters) to modify the values instead of granting direct access to the class attributes. Before gaining access to or changing the data, you can enforce validation or any extra logic using getters and setters. You can conceal the internal representation of the data and increase its security by utilizing getters and setters.

Access modifiers: Access modifiers are built into programming languages like Python, Java, and C++ to regulate class members' visibility and accessibility. Private members are not directly accessible outside of the class; only the class itself has access to them. Within the class and its descendant classes, protected members can be accessed. Access to public members is available throughout the code. Python does not impose rigorous access control, although, by convention, a single underscore (_) prefix is used to denote a private member.

Below is a more practical example, than the previous demonstrating information hiding using access modifiers and getters/setters in Python.

class Student:
    def __init__(self, name, age, grade):
        self._name = name  # Private attribute
        self._age = age  # Private attribute
        self._grade = grade  # Private attribute

    def get_name(self):
        return self._name  # Getter method for name

    def get_age(self):
        return self._age  # Getter method for age

    def get_grade(self):
        return self._grade  # Getter method for grade

    def set_grade(self, new_grade):
        if new_grade >= 0 and new_grade <= 100:
            self._grade = new_grade  # Setter method for grade
        else:
            print("Invalid grade.")

# Creating an instance of Student
student = Student("Alice", 18, 85)

# Accessing attributes using getters
print(student.get_name())  # Output: Alice
print(student.get_age())  # Output: 18
print(student.get_grade())  # Output: 85

# Modifying attribute using setter
student.set_grade(92)
print(student.get_grade())  # Output: 92

student.set_grade(110)  # Output: Invalid grade.

The single underscore prefix in the above example designates the Student class's private characteristics, which are _name, _age, and _grade. Access to these characteristics is provided by the public-getter methods get_name(), get_age(), and get_grade(). The setter method (set_grade()), which performs validation and makes sure that the grade is within a valid range, and regulates the alteration of the _grade attribute.

In order to ensure correct encapsulation and maintain data integrity, we hide the underlying workings of the class and grant controlled access to the attributes using access modifiers and getters/setters.

Inheritance

Definition

In OOP, inheritance is a potent idea that enables code reuse and the development of more intricate and adaptable programs. By making a new class that derives from an existing class, inheritance is accomplished in Python. The existing class is referred to as the base class or parent class, while the new class is referred to as the derived class or child class. All of the base class's characteristics and methods are inherited by the derived class. This implies that all of the code defined in the base class can be used by the derived class without having to rewrite it. This can save a ton of time and effort and improve the maintainability of the code.

The genetrc inheritance syntax in Python is as shown:

class ParentClass:
    # Parent class logic

class ChildClass(ParentClass):
    # Child class logic

In the syntax above, the Parent Class is the existing class from which you want to inherit which can also be called Base Class or Super Class. The Child Class is the class that inherits from the parent class also called Subclass or Derived Class.

Types of Inheritance

Single inheritance: Single inheritance enables code reuse and the addition of new features to exist code by allowing a derivate class to inherit properties from a single-parent class. A child class, in essence, derives from a single-parent class.

Multiple inheritance: Multiple inheritance is a type of inheritance where a class can be built from more than a single base class. Each attribute existing in the base classes has been transferred to the class that is derived from them when there are multiple inheritances. A child class inherits from many parent classes.

Multilevel inheritance: The characteristics of both the original class and the class from which it is descended are passed on to the new class in multilevel inheritance. It resembles the bond between grandparents and grandchildren. A child class, in essence, descended from a parent class, which had, in turn, descended from a previous parent class.

An example showing inheritance in Python.

class Vehicle:
    def __init__(self, brand):
        self.brand = brand

    def drive(self):
        print(f"{self.brand} is being driven.")

class Car(Vehicle):
    def honk(self):
        print(f"{self.brand} is honking.")

class Bicycle(Vehicle):
    def pedal(self):
        print(f"{self.brand} is being pedaled.")

# Creating instances of the child classes
car = Car("Audi")
bicycle = Bicycle("Ghost")

# Calling inherited methods
car.drive()  # Output: Audi is being driven.
bicycle.drive()  # Output: Ghost is being driven.

# Calling subclass-specific methods
car.honk()  # Output: Audi is honking.
bicycle.pedal()  # Output: Ghost is being pedaled.

The Vehicle class is the parent class in the example above, and  Car and Bicycle classes are its children. The drive() function is inherited by both child classes from the Vehicle class. Additionally, the Bicycle class defines a pedal() function, while the Car class defines an additional honk() method.

Super Function

Python's super() function can be used to invoke a parent class method from a subclass. It enables you to access and use the parent class's methods and attributes, allowing you to modify or replace their actions while preserving the parent class's functionality. It makes it simpler to use the functionality defined in the parent class while extending or customizing it in the child class since it makes accessing and invoking parent class methods easier.

The super class uses the following syntax:

class ChildClass(ParentClass):
    def __init__(self, parameters):
        super().__init__(parameters)  # Call parent class's __init__() method

    def some_method(self):
        super().parent_method()  # Call parent class's method

 

Inheritance can reduce work and time requirements and improve the maintainability of the code. Inheritance can initially be a little puzzling if you are new to OOP. You will immediately realize how it can be utilized to produce robust and reusable code, though, with a little practice.

Polymorphism

The idea of polymorphism holds that a single type of entity can exist in numerous forms. This implies that a single entity in a programming language, like Python can behave in a variety of ways depending on the circumstances. It's comparable to how a word like "invite" can be used as an adjective, a noun, and a verb all in the same sentence. While the letters on the page remain in the same order, the meaning they bring to the statement varies significantly depending on the situation. Similarly to this, a single entity in polymorphic programming languages might behave differently in various situations. Polymorphism in Python can be implemented by inheritance (demonstrated in the previous section) or by method overriding and method overloading.

Polymorphism By Inheritance

Polymorphism can be achieved through inheritance by defining methods in the child class that have the same name as the methods in the parent class. The child class can then override the behavior of these methods, or it can simply use them as they are defined in the parent class. Here is an example

class Animal:
    def speak(self):
        print("Animal speaking")

class Dog(Animal):
    def speak(self):
        print("Dog barking")

class Cat(Animal):
    def speak(self):
        print("Cat meowing")

a = Animal()
d = Dog()
c = Cat()

a.speak()  # Prints "Animal speaking"
d.speak()  # Prints "Dog barking"
c.speak()  # Prints "Cat meowing"

In this case, the speak() method is an inheritance from the Animal class to the Dog, Cat, and Animal classes. The Dog and Cat classes, however, override the speak() method to print different messages. We may use the speak() method to invoke each new instance of the Animal, Dog, and Cat classes. The relevant message for the type of object we are invoking the talk() method on will be printed.

Polymorphism By Method Overloading

Method overloading refers to the definition of numerous methods in a class that have the same name but distinct parameters. The use of variable-length parameters (*args, **kwargs) and default parameter values in Python allow for method overloading. Python decides which version of the method to execute when a method is called with various arguments based on the quantity and nature of the arguments given. Multiple operations can be carried out with a single symbol. The addition operator + is one of the simplest illustrations of this. With regard to various data types, the addition operator in Python behaves differently. 
The + function operates on two integers; the additive outcome returns the two integers' sum. 

int1 = 15
int2 = 25
print(int1 + int2)
# returns 40
On the other hand, when the addition operator is applied to two strings, the strings are concatenated. Here's an illustration of how the plus sign (+) affects string data types.

str1 = "150"
str2 = "250"
print(str1 + str2)
# returns 150250

Polymorphism By Method Overriding

When a subclass defines a method with the same name as a method in its parent class, this is known as method overriding. The parent class method's implementation is overridden by the subclass method, enabling the subclass to create its own unique implementation. When a method is called on an object, the subclass version of the method is executed rather than the parent class version. Here is an example

class Animal:
    def make_sound(self):
        pass

class Donkey(Animal):
    def make_sound(self):
        return "hee-haw!"

class Cat(Animal):
    def make_sound(self):
        return "Meow!"

class Cow(Animal):
    def make_sound(self):
        return "Moo!"

# Create instances of different classes
donkey = Donkey()
cat = Cat()
cow = Cow()

# Call the make_sound() method on different objects
print(donkey.make_sound())  # Output: hee-haw!
print(cat.make_sound())  # Output: Meow!
print(cow.make_sound())  # Output: Moo!

Donkey, Cat, and Cow classes are the subclasses in the above example, with an Animal class acting as the parent class. Each subclass adds its own implementation to the make_sound() method that was inherited from the parent class.

Polymorphism allows us to treat objects of different classes as instances of the common superclass Animal by generating those objects and invoking the make_sound() function on each one of them. The make_sound() method specified in each subclass is called, but the appropriate version is used, therefore different sounds are returned.

In order to create flexible and reusable code, polymorphism through method overriding enables us to create programs that can operate with many objects interchangeably based on their common interface (in this case, the make_sound() method).

Abstract Base Class

An abstract base class (ABC) in Python is a class that cannot be directly constructed but offers a uniform interface for its subclasses. By defining a set of methods that its subclasses must implement, it acts as a model for other classes. Abstract base classes serve as a common API and are typically used to enforce specific behaviors or contracts on subclasses. Python includes the ABC class and additional tools for creating and using abstract base classes in the ABC module. You can subclass the ABC class from the ABC module and use the @abstractmethod decorator to mark methods as abstract to build an abstract base class. Here is an example borrowing from the previous Vehicle and Car example.

from abc import ABC, abstractmethod

class Vehicle(ABC):
    @abstractmethod
    def start(self):
        pass

    @abstractmethod
    def stop(self):
        pass

class Car(Vehicle):
    def start(self):
        print("Car started.")

    def stop(self):
        print("Car stopped.")

class Motorcycle(Vehicle):
    def start(self):
        print("Motorcycle started.")

    def stop(self):
        print("Motorcycle stopped.")

class Bicycle(Vehicle):
    def start(self):
        print("Bicycle started.")

    def stop(self):
        print("Bicycle stopped.")

# Creating instances of subclasses
car = Car()
motorcycle = Motorcycle()
bicycle = Bicycle()

# Calling abstract methods
car.start()  # Output: Car started.
car.stop()  # Output: Car stopped.
motorcycle.start()  # Output: Motorcycle started.
motorcycle.stop()  # Output: Motorcycle stopped.
bicycle.start()  # Output: Bicycle started.
bicycle.stop()  # Output: Bicycle stopped.

The vehicle is an abstract base class used in this illustration. Using the @abstractmethod decorator, it defines the abstract methods start() and stop(). The start() and stop() methods are in charge of starting and halting the vehicle, respectively.

Concrete subclasses of the Vehicle class include the Car, Motorcycle, and Bicycle classes. Based on the unique behavior of the vehicle type, each subclass offers its own implementation of the start() and stop() functions.

We may accomplish polymorphic behavior, where each object behaves differently according to its own implementation of the abstract methods defined in the abstract base class, by generating instances of the subclasses and calling the abstract methods.

Conclusion


This article is a summary of OOP concepts in Python, You should refer more from the Python documentation and other relevant sources. Once you have mastered the concepts in this article, and the ones included in the previous, then you can apply the knowledge and practice in developing simple to complex software and even work with frameworks such as Django, Flask, and Pyramid to craft solutions to several technical problems out there. In future articles, we may be taking a look at Django. If you find this article to be of help, don't hesitate to drop a comment or reach out via my socials, and sharing with others who may benefit from it.

Related articles

Comments

There are no comments yet.

Captcha* captcha
ABOUT ME
JosephKariuki

Hi, thanks for visiting! My name is Joseph Kariuki, a software developer and this is my website. I am consistently seeking solutions to various problems using coding. This site does not contain ads, trackers, affiliates, and sponsored posts.

DETAILS

Published: July 3, 2023

TAGS
fundamentals python
CATEGORIES
Fundamentals Python