The Pythonic way to handle OOPs Saga

Object-oriented programming is a programming paradigm based on the concepts of objects and classes. Objects and classes both are important paradigms of OOP. If we define animals as a class, then different types of animals are objects of that class.

Why OOP?

  1. Easy to debug
  2. It is much faster and more efficient to use.
  3. Better code reusability
  4. Better code structure

Note: I have defined the outputs in the comment against each of the code lines. Preceding from the symbol <- is the reason for the output. You can check out the code here and don't forget to give it a 🌟 if you liked it.

Classes and Objects

A Class is a blueprint or schema which is used to define new objects. The class provides information about what properties and methods an object should have. Properties are termed as attributes while the behaviors are known as methods. When class is defined, only the description for the object is defined.

Objects are instances of the class. It is an entity that has a state and behavior. In a nutshell, it is an instance of a class that can access the data. e.g. Person is a class and Mike is an object of the Person class.

A class can be declared using the class keyword. by default, each class has a special __init__() method which we can override while defining the class. This special __init__() method also known as the constructor method gets called automatically when we create new objects using that class. This process is known as instantiation. If we want to pass additional information while creating the new objects of a particular class we can accept arguments in the constructor method.

Note: If you want to check whether an object is an instance of a class or not you can use python built-in function isinstance(object_name, class_name) which will evaluate to bool.

Example 1: Basic Structure of a Class

class Person:  # class declaration
    def __init__(self, name, age):  # constructor method
        self.name = name  # attributes
        self.age = age

    def set_age(self, new_age): # general methods
        self.age = new_age

mike = Person("Mike", 14)

print(mike.name) # Mike
mike.set_age(17)
print(mike.age) # 17

Types of Attributes

Attributes can be categorized into two types:

  1. Instance Attributes
  2. Class Attributes

class attributes are shared by all the objects of a class while the instance attributes are the exclusive property of the instance. We can access the instance attributes using the object while the class attributes can be accessed using the Class. We can also access the class attribute using the object by accessing the __class__ property. the class attribute is also known as the static attribute.

Example 2: How to access Attributes

class Person:
    # class attribute
    class_name = "Person"

    def __init__(self, name, age):
        # instance attribute
        self.name = name
        self.age = age

mike = Person("Mike", 15)
print(mike.age) # 15 <- Access the simple members
print(Person.class_name) # Person <- Accessing class variables using class
print(mike.__class__.class_name) # Person <- Accessing class variables using object

Types of Methods

Python offers three types of methods namely

  1. Instance Methods
  2. Static Methods
  3. Class Methods

Let’s peek into what happens behind the scenes for each method type.

Instance methods receive the instance of the class as an argument generally termed as self. It can take any number of arguments. Using the self parameter we can access the other attributes and methods of the class.

Note: using self.__class__, we can access the class methods and class attributes also.

Class methods are defined using the special decorator @classmethod. A class method can only have access to the class attributes but not to the instance attributes. They are bound to the class, not to the object. Similar to the instance method, a class method accepts the class itself as an argument generally termed as cls.

A Static method can neither modify the object state nor the class state. They are primarily a way to namespace our methods. Static methods are flagged as static by using the @staticmethod decorator. Static methods can be created for a function to be called on a specific type of object.

Example 3: Methods available in Python OOPs

from datetime import date

class Person:
    class_name = "Person"

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

    # instance method
    def print_name(self):
        print(self.name)

    # class method
    @classmethod
    def create_person_from_birthyear(cls, name, birth_year):
        print("Creating a new object of type:", cls.class_name)
        return cls(name, date.today().year - birth_year)

    # static method
    @staticmethod
    def is_adult(age):
        return age > 18

mike = Person("Mike", 15)
mike.print_name() # Mike <- Instance Method
james = Person.create_person_from_birthyear("James", 2001) <- Object creation using classmethod
james.print_name() # James
Person.is_adult(james) # True <- Static Method

Access modifiers

The access modifiers are used to modify the default scope of variables. There are three types of access modifiers in Python: public, private, and protected.

Variables with the public access modifiers can be accessed anywhere inside or outside the class, the private variables can only be accessed inside the class, while protected variables can be accessed within the same package.

To create a private variable, you need to prefix double underscores with the name of the variable. To create a protected variable, you need to prefix a single underscore with the variable name. For public variables, you do not have to add any prefixes at all.

Example 4: Access Modifiers Example

class Person:
    def __init__(self, name, age, salary):
        self.name = name  # public member
        self._age = age  # protected member
        self.__salary = salary  # private member


mike = Person("Mike", 17, 15000)
print(mike.name) # Mike
print(mike._age) # 17
print(mike.__salary)  # Attribute Error

Note: If you want to access or change the private class member you can do it by objectName._className__memberName = new_value but it is avoided.

print(mike._Person__salary) # 15000 <- Accessing private variable outside
mike._Person__salary = 25000 <- Changing value of private variable
print(mike._Person__salary) # 25000

Pillars of OOPs

The whole OOPs paradigm is based upon three major pillars named as follows:

  1. Inheritance
  2. Polymorphism
  3. Encapsulation

Inheritance

Inheritance in object-oriented programming is pretty similar to real-world inheritance where a child inherits some of the characteristics from his parents, in addition to his/her unique characteristics. The basic idea of inheritance in object-oriented programming is that a class can inherit the characteristics of another class.

Example 5: Inheritance

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

    def read_age(self):
        print(self.age)

class Employee(Person):
    def __init__(self, name, age, position, salary):
        super().__init__(name, age)  # can be Person.__init__(self, name, age)
        self.position = position
        self.salary = salary

    def read_salary(self):
        print(self.salary)

employee1 = Employee("Mike", 23, "Software Engineer", 999999)
employee1.read_age()  # 23 <- Method present in the parent class
employee1.read_salary()  # 999999 <- Method present in child class

To implement inheritance in python while declaring the child class we pass the name of the base class as the argument. Now to inherit the properties and methods of the base class one has to call the super().__init__(*similar_arguments) function which in the case of single Inheritance can be replaced with baseClassName.__init__(self, *similar_arguments) and In case of multiple inheritances can be replaced by a series of baseClassName.__init__(self, *similar_arguments). Python allows multiple inheritances.

Note:

  1. by default, each class in python inherits the object class.
  2. If you want to check whether a class_B has inherited class_A you can use python built-in function issubclass(class_B, class_A) which evaluates to bool.

Polymorphism

In the context of object-oriented programming, polymorphism refers to the ability of an object to behave in multiple ways. It is also known as method overriding where we change the definition of the predefined method.

Example 6: Polymorphism

class Vehicle:
    def print(self):
        print("This is parent class Vehicle")

class Car(Vehicle):
    def print(self):
        print("This is child class Car")

class Cycle(Vehicle):
    def print(self):
        print("This is child class Cycle")

vehicle = Vehicle()
car = Car()
cycle = Cycle()

vehicle.print() # This is parent class Vehicle <- Method called from Vehicle class
car.print() # This is child class Car <- Method called from Car class
cycle.print() # This is child class Cycle <- Method called from Cycle class

Encapsulation

Encapsulation is the third pillar of object-oriented programming. Encapsulation simply refers to data hiding. For implementing this property we can use private members in the class and provide an interface for accessing and changing the members by implementing getter and setter methods in the class. A pythonic way to handle this type of situation is using the @property decorator. you can check that out here.

Example 7: Encapsulation

class Price:
    def __init__(self, price):
        self.__price = price

    def get_price(self):
        print("Price is", self.__price)

    def set_price(self, new_price):
        self.__price = new_price


amount = Price(-99)
amount.get_price()  # Price is -99
amount.set_price(99)
amount.get_price()  # Price is 99
print(amount.__price)  # Attribute Error <- evaluates to error as private members can't be accessed directly

The Abstract Class

An abstract class is a class, but you can't create objects from it directly. Its purpose is to define how other classes should look like, i.e. what methods and properties they are expected to have. The methods and properties defined in an abstract class are called abstract methods and abstract properties. An abstract method is a method that is declared but contains no implementation. Abstract classes cannot be instantiated, and require subclasses to provide implementations for the abstract methods. If any of the methods defined in the abstract class is not overridden in child one then it will throw an error. In python, we can create an abstract class by inheriting the ABC class from the abc module.

Example 8: Abstract Class

from abc import ABC, abstractmethod
class AbstractClassExample(ABC):
    @abstractmethod
    def do_something(self):
        pass

class AnotherSubclass(AbstractClassExample):
    def do_something(self):
        super().do_something()
        print("The changes from AnotherSubclass")

x = AnotherSubclass()
x.do_something()  # The changes from AnotherSubclass <- Implementation found in the child class

The MetaClass

A metaclass is a class whose instances are classes. Like an ordinary class defines the behavior of the instances of the class, a metaclass defines the behavior of classes and their instances. You can learn more about metaclasses here.

Operator Overloading

Class functions that begin with double underscore __ are called special functions in Python. For the custom classes, we can change the definition of built-in python operators using the special methods. for a custom class, we can change the definition of print() function using the __str__() special method. we can even make an object callable using the special method __call__(). There are plenty of special methods available. Special methods are known as magic methods or dunder methods too.

Example 9: Operator Overloading

class ComplexNumber:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        if self.y >= 0:
            return "{}+{}i".format(self.x, self.y)
        else:
            return "{}{}i".format(self.x, self.y)

    def __add__(self, c):
        self.x = self.x + c.x
        self.y = self.y + c.y

c1 = ComplexNumber(5, 3)
c2 = ComplexNumber(5, -3)
print(c1)  # 5+3i
print(c2)  # 5-3i
c1 + c2 # <- gets evaluated to c1.__add__(c2)
print(c1)  # 10+0i

Conclusion

Thanks for reading. I hope it will help you a lot. If you like it, don’t forget to follow me to get more amazing articles about programming and technologies! for any queries related to the blog ping me at 😊.