Object-Oriented Programming (OOP)

Object-Oriented Programming (OOP)

Python

Introduction

In this tutorial, I assume that this is the first time for you with OOP, I will start from scratch and illustrate all the OOP terminologies and concepts.

Firstly, before OOP and as a beginner in programming we had a procedure for programming that divided any program to punch of variables and functions operating those variables. This way was not effective, when you take a glance at your code you will find it ambiguous, not reusable, and maybe contain a lot of copied functions, which means that your code is against the coding principles : keep your code clean, reusable, and don't repeat your self(DRY).

OOP comes to solve these problems, OOP combine(encapsulate)related variable, which are called attributes, and functions, which called methods, into individual objects.

In this tutorial, you’ll learn how to:

  • Create a class, which is like a blueprint for creating an object
  • Use classes to create new objects
  • Model systems with class inheritance
  • SOLID principles in object-oriented programming.

Python Object-oriented Programming

In Python, everything is an object. An object has a state and behaviors. To make an object, you must first declare a class. Then you can generate one or more objects from the class. The objects are instances of a class.

Classes and Instances

A class is a blueprint for the object. We can think of class as a sketch of a car ,for example, with labels. the class specifies all the details about the name, colors, size and all the necessary for defining a car but it doesn’t actually contain any data. Based on these descriptions, we can build the car. Here, a car is an instance or object.

In this tutorial, you’ll create a Car class that stores some information about the properties and behaviors that an individual car can have.

class Car:
    pass

This creates a new Dog class with no attributes or methods.

The class keyword comes first in all class definitions, followed by the class name -which are written in CapitalizedWords notation by convention- and a colon. Any code below the class definition that is indented is considered part of the class's body.

While the class is the blueprint, an instance is an object that is built from a class and contains real data. To initiate an object we need a method in the class that runs as soon as you call the class, initialize an instance and define an attribute for it.

__init__()

To initialize an instance and define an attribute for all instances of a class, we use the __init__ method. The __init__method is similar to constructors in JS.

Constructors are used to define the state of an object. Constructors are responsible for initializing (assigning values) to the data members of a class when an object of that class is created. A constructor, like methods, includes a collection of statements that are executed when an object is created. It is called whenever a class object is instantiated. The figure bellow illustrates the class, constructor ,and instance relation.

oopcar.png

Let’s update the Car class with an __init__() method that creates name and color attributes:

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

    # sample method to understand the constractor :
    def car_propety(self):
        print('Hello , this car name is {}  and it is  {}'.format(self.name , self.color))

if __name__ == '__main__':
    car = Car('Toyota' , 'Red')
    car.car_propety()

OUTPUT <- Hello , this car name is Toyota  and it is  Red

Understanding the code

In the above example, a car name Toyota has a red color is created. While creating a car, “Toyota” and “Red” are passed as an argument, this argument will be passed to the __init__ method to initialize the object. Similarly, many objects of class Car can be created by passing different names and colors as arguments.

self

The self is used to represent the instance of the class. With this keyword, you can access the attributes and methods of the class in python. It binds the attributes with the given arguments.

Self is always pointing to Current Object and must be provided as a First parameter to the Instance method and constructor, any time you don't provide it, it will cause this error: TypeError: __init__() takes 2 positional arguments but 1 was given

In our example when we create a car instance car = Car('Toyota' , 'Red') we didn't pass self as argument, but this is consistence with what we considered above. How ?

When you instantiate a Car object, Python creates a new instance and passes it to the first parameter of __init__(). This essentially removes the self parameter, so you only need to worry about the other instance attributes.

If you try to execute this part car = Car('Toyota' , 'Red') The result will be <__main__.Car object at 0x7f96c9522fd0> . This collection of letters and numbers is a memory address that indicates where the Car object is stored in your device memory. The address you see on your screen will be different surly.

The memory address isn’t very helpful. You can change what gets printed by defining a special instance method called __str__() , we will discuss it within the next sections.

Class Attributes vs Instance Attributes

Attributes created in __init__() are called instance attributes. An instance attribute’s value is specific to a particular instance of the class. All Car objects have a name and a color, but the values for the name and color attributes will vary depending on the Car instance.

class attributes are attributes that have the same value for all class instances. You can define a class attribute by assigning a value to a variable name outside of __init__(), usually it is defined directly at the first line after the class name.

In our example, the following Car class has a class attribute called Type with the value Vehicles:

class Car:
    #class attributes
    Type = 'Vehicle'

    def __init__(self , name , color):
        #instance attributes
        self.name = name 
        self.color = color

When you access an attribute via an instance of the class, Python searches for the attribute in the instance attribute list. If the instance attribute list doesn’t have that attribute, Python continues looking up the attribute in the class attribute list. Python returns the value of the attribute as long as it finds the attribute in the instance attribute list or class attribute list. However, if you access an attribute, Python directly searches for the attribute in the class attribute list.

You can access the instance attributes as well as the class attributes using dot notation, also you can update the values of it by the same way since the object is mutable.

Use class_name.class_attribute or object_name.class_attribute to access the value of the class_attribute and use object_name.instance_attribute to access the value of the instance_attribute.

As well as that the class attributes is used for storing class constants, track data across all instances, and setting default values for all instances of the class ,you update the class attributes for one instance by overridden it, it will be changed only for that object.

print(car.name)
<- Toyota
print(car.Type)
<- Vehicle

One of the most significant benefits of using classes to organize data is that instances will always have the attributes you expect. Because the name, color and Type properties are present in all Car instances, you may use them with confidence, knowing that they will always return a value.

Instance Methods

Instance methods are functions defined within a class that can only be called from a class instance. An instance method's first parameter is always self. In our example car_propety() is an instance method that returns a string containing the name and color of the car.

class Car:
    #class attributes
    Type = 'Vehicle'

    def __init__(self , name , color):
        #instance attributes
        self.name = name 
        self.color = color

    # sample method to understand the constractor :
    def car_propety(self):
        print('Hello , this car name is {}  and it is  {}'.format(self.name , self.color))


    def car_year(self , year):
        print('car"s manufactured date is {}'.format(year))

The car_year() is an instance method that take one parameter year and returns a string.

Dunder or Magic Methods

Dunder or magic methods in Python are the methods having two prefix and suffix underscores in the method name. Dunder here means “Double Under (Underscores)”. These are commonly used for operator overloading. Few examples for magic methods are: __init__, __add__, __len__, __str__ and __repr__.

__str__():

In this part, we will learn how to use the __str__ method to make a string representation of a class.

You read above about the execution of the instance that resulted to <__main__.Car object at 0x7f96c9522fd0> which is the location in memory that the instance saved in. This is unreadable and not user friendly. The __str__() method is used for creating a readable output for end user.

Internally, Python will call the __str__ method automatically when an instance calls the str() method.

Note that the print() function converts all non-keyword arguments to strings by passing them to the str() before displaying the string values, and when you use the print() function to print out an instance of the Car class, Python calls the __str__ method defined in the Car class.

def __str__(self):
   return self.name+ ' is ' + str(self.__class__.__name__) +"'s attribute. its " + self.color +'.'
if __name__ == '__main__':
    car = Car('Toyota' , 'Red')
    print(car)

Output <- Toyota is Car attribute. its Red.

Static Methods

Unlike instance methods, static methods aren’t bound to an object. In other words, static methods cannot access and modify an object state.

In practice, you use static methods to define utility methods or group functions that have some logical relationships in a class. To define a static method, you use the @staticmethod decorator:

class className:
    @staticmethod
    def static_method_name(param_list):
        pass

To call the static method, we use this syntax : className.static_method_name()

Inheritance

The process through which one class inherits the attributes and methods of another is known as inheritance. Child classes are newly generated classes, while parent classes are the classes from which child classes are produced. Inheritance allows a class to reuse the logic of an existing class.

the parent class called also the base class or the super class and the child class called also the derived class or the subclass.

Single Inheritance Syntax :

class ParentClass:
  Body of Parent class
class ChildClass(ParentClass):
  Body of Child class

Parent classes' attributes and methods can be overridden or extended by child classes. In other words, child classes inherit all of their parent's properties and methods, but they can also define their own attributes and methods.

class SmallCar(Car):

    def __init__(self, name, color,price):
        super().__init__(name, color)
        self.price = price

    def __str__(self):
        return self.name+ ' is ' + str(self.__class__.__name__) +"'s attribute. its " + self.color + ' and costs $' + str(self.price) + '.'

if __name__ == '__main__':
    small_car = SmallCar('Peugeot' , 'yellow' , 5000)
    print(small_car)

Output <- Peugeot is SmallCar's attribute. its yello and costs $5000.

Here the instance of the SmallCar(Car) class inherent the name and the color attributes from the Car class. The super() is a built in method that returns a proxy object (temporary object of the superclass) that allows us to access methods of the base class.

Different forms of Inheritance:

  1. Single inheritance: When a child class inherits from only one parent class, it is called single inheritance.
  2. Multiple inheritance: When a child class inherits from multiple parent classes, it is called multiple inheritance.
  3. Multilevel Inheritance: In multilevel inheritance, features of the base class and the derived class are inherited into the new derived class.

Multiple Inheritance Syntax :

class Base1:
    pass

class Base2:
    pass

class MultiDerived(Base1, Base2):
    pass

Multilevel Inheritance Syntax :

class Base:
    pass

class Derived1(Base):
    pass

class Derived2(Derived1):
    pass

Abstract Class and Abstract Method

An abstract class can be considered as a blueprint for other classes. It enables you to define a set of methods that must be implemented in any child classes that are derived from the abstract class. An abstract class may or may not include abstract methods. The term abstract method refers to a method that has a declaration but no implementation, the Child class is responsible for providing an implementation for the parent class abstract method. We employ an abstract class for designing huge functional units. We utilize an abstract class to provide a standard interface for diverse implementations of a component.

As a summary, an abstract class is a class that cannot be instantiated. However, you can create classes that inherit from an abstract class while an abstract method is an method without an implementation.

Python doesn’t directly support abstract classes. But it does offer a module that allows you to define abstract classes. ABC is the name of the Python module that build the basis for defining Abstract Base Classes (ABC). The ABC module provides you with the infrastructure for defining abstract base classes. To define an abstract method, you use the @abstractmethod decorator:

Abstract class and Abstract method syntax:

from abc import ABC, abstractmethod


class AbstractClassName(ABC):
    @abstractmethod
    def abstract_method_name(self):
        pass

SOLID Principles in Object-Oriented Programming

What is SOLID?

SOLID is an abbreviation that stands for five software design principles compiled by Uncle Bob:

  • S – Single responsibility Principle
  • O – Open-closed Principle
  • L – Liskov Substitution Principle
  • I – Interface Segregation Principle
  • D – Dependency Inversion Principle

Single Responsibility Principle

According to the single responsibility principle (SRP), each class, method, and function should only have one purpose or cause to change.

The single responsibility principle's goals are to:

  • Create classes, methods, and functions that are very cohesive and robust.
  • Promote class composition
  • Duplicate code should be avoided. Now, we will introduce those principles briefly.

Open–closed principle

A class, method, or function should be open for extension but closed for modification, according to the open-closed principle.

The open-closed principle's goal is to make it simple to add new features (or use cases) to the system without having to modify the current code directly.

Liskov Substitution Principle

The Liskov substitution principle states that a child class must be substitutable for its parent class. Liskov substitution principle aims to ensure that the child class can assume the place of its parent class without causing any errors.

Interface Segregation Principle

In object-oriented programming, an interface is a set of methods an object must-have. The purpose of interfaces is to allow clients to request the correct methods of an object via its interface.

Python uses abstract classes as interfaces because it follows the so-called duck typing principle. The duck typing principle states that “if it walks like a duck and quacks like a duck, it must be a duck.” In other words, the methods of a class determine what its objects will be, not the type of the class.

The interface segregation principle states that an interface should be as small a possible in terms of cohesion. In other words, it should do ONE thing.

It doesn’t mean that the interface should have one method. An interface can have multiple cohesive methods.

Dependency Inversion Principle

The dependency inversion principle states that:

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
  • Abstractions should not depend on details. Details should depend on abstractions. The dependency inversion principle aims to reduce the coupling between classes by creating an abstraction layer between them.

References