OOP in Python

I have to switch temporarly, I hope, to a Python project in my company. Syntax of the language has been learned very quickly so I tried to use Object Oriented concepts. I’ve constated that many people using Python are “Data scientist/analyst”, people with maths background but not necessarly aware of these methods of conception.

I’ve written an example based on health calculators (Ideal Body Weight and Body Mass Index).

Weight has becoming a major problem for me since I stopped sport competition ๐Ÿ˜‰

You can find the code on my github here.

I’m going to introduce main concepts of OOP: encapsulation, inheritance, polymorphism and abstraction.

Encapsulation

First of all, encapsulation, the ability to hide internal stuff (variable or method) of a class doesn’t exist in Python.

In OOP and major languages like C# or Java, private variables or methods are not accessibles from outside the class where they are defined. ‘protected’ concept means variables and methods could be only used in derived classes.

In Python, every variable declared in a class is accessible from outside, keywords like ‘private’ or ‘protected’ are not part of the language. ๐Ÿ™

Meanwhile, we could use features to implement it, this allows us to design good code.

You can see below, in the code example, the ‘@property’ decorator and the getter, and a naming convention to specify private variable. The getter is just here a method which returns value of the private variable but you could imagine transforming the value with some calculation.

Even if it’s just “sugar code”, I recommend this usage.

from health_calculators.person import Person


class Woman(Person):
    def __init__(self, name):
        super().__init__(name)
        # naming convention for private variable with two _ at the beginning 
        # and one _ for protected variable
        self.__ideal_weight_factor = 2

    @property
    def ideal_weight_factor(self):
        return self.__ideal_weight_factor

    # getter is to read the value of the private variable
    # in Python, private or protected access doesn't exist
    # variables are public to caller
    @ideal_weight_factor.getter
    def get_ideal_weight_factor(self) -> int:
        return self.__ideal_weight_factor

Inheritance

Inheritance is very easy to understand. You create a base class with some common properties or methods, and after you create derived class which heritate from their parent, and add some specific stuff (specialization).

In my code example, I create a Person class, and Woman and Man classes, derived from Person.

Person has height and weight properties, common to woman and man. You can see ‘get_ideal_weight_factor’ method with the keyword ‘pass’, it means there’s no code here but in the same method in children classes.

class Person:
    # constructor with 1 parameter
    # self means pointer to the class instance
    def __init__(self, name):
        self.name = name
        self.__height = float()
        self.__weight = float()

    def get_ideal_weight_factor(self):
        pass

    @property
    def weight(self) -> float:
        return self.__weight

    @weight.setter
    def set_weight(self, weight):
        self.__weight = weight

    @property
    def height(self) -> float:
        return self.__height

    @height.setter
    def set_height(self, height):
        self.__height = height

In the following code snippet, you can see the implementation of ‘get_ideal_weight_factor’ method. It’s a figure used in Ideal Body Weight calculation, different for woman and man.

The inheritance is done writing the name of parent class between parenthesis in the child class definition.

The ‘__init__’ method is called the constructor, This method is called each time you instantiate an object.

The ‘super()’ keyword is a pointer to the parent class, here we call the constructor of the parent class.

The ‘self’ keyword as first parameter of each method is mandatory, it represents a pointer to the current class instance.

from health_calculators.person import Person


class Woman(Person):
    def __init__(self, name):
        super().__init__(name)
        # naming convention for private variable with two _ at the beginning 
        # and one _ for protected variable
        self.__ideal_weight_factor = 2

    @property
    def ideal_weight_factor(self):
        return self.__ideal_weight_factor

    # getter is to read the value of the private variable
    # in Python, private or protected access doesn't exist
    # variables are public to caller
    @ideal_weight_factor.getter
    def get_ideal_weight_factor(self) -> int:
        return self.__ideal_weight_factor

    @property
    def height(self) -> float:
        return super().height

    @property
    def weight(self) -> float:
        return super().weight

Polymorphism and abstraction

Polymorphism can be seen at a method level or at a class level.

At a method level, you do that by writing several methods with the same name, but with differents parameters, it’s called ‘overloading’.

I present that in the code below. The ‘analyze’ method is different for my calculators, we can see that both have different parameters. The ‘@dispatch’ decorator has the role to orientate the caller to the right method. Furthermore, we can see that the method is designed as abstract, it means implementation is in derived classes.

# OOP polymorphism at a method level, need package 'multipledispatch'
# method overloading (different signatures) 
    @dispatch(str, str)
    @abstractmethod
    def analyze(self, result, calculator_table):
        pass

    @dispatch(float, float)
    @abstractmethod
    def analyze(self, current_weight, ideal_weight):
        pass

In the main method of the program, I use these 2 ‘analyze’ methods, depending on the presence of the variable ‘calculator_table’.

# check the existence of a local variable
        if 'calculator_table' in locals():
        # differents analyze methods are only implemented to demonstrate overloading, 
        # this could be replaced in a better way by an analyzer interface          
            status_msg = calculator.analyze(health_indicator, calculator_table)
        else:
            status_msg = calculator.analyze(calculator.person.weight, health_indicator)
        if status_msg:
            print(status_msg)

At a class level, polymorphism is very powerful, it allows to call methods of differents objects without knowing their behavior. You just know that object implements the method you call.

To realize that, we use the fourth pillar of OOP: abstraction. It means that some part of the conception is hidden from caller and simplify the use of the objet.

To implement abstraction, we first define an interface. This is a class with no code, just entry points. It is a contract between objects which implement it and the caller.

from abc import ABC, abstractmethod
from multipledispatch import dispatch

# OOP polymorphism at a class level
# interface is just entry points, implementation is in derived classes
class HealthCalculatorInterface(ABC):
    def __init__(self, name):
        self.__name = name

# OOP encapsulation 
# @property hides the private variable (not really hidden in Python)
    @property
    def name(self):
        return self.__name
# getter is to read property value (getter keyword is optional)
    @property
    def person(self):
        return self.__person
# setter is to set property value
    @person.setter
    def set_person(self, person):
        self.__person = person

    @abstractmethod
    def welcome(self, name):
        pass

    @abstractmethod
    def input_parameters(self):
        pass

# abstract method is a method without code, implementation is in derived class
    @abstractmethod
    def calc(self):
        pass

# OOP polymorphism at a method level, need package 'multipledispatch'
# method overloading (different signatures) 
    @dispatch(str, str)
    @abstractmethod
    def analyze(self, result, calculator_table):
        pass

    @dispatch(float, float)
    @abstractmethod
    def analyze(self, current_weight, ideal_weight):
        pass

    @abstractmethod
    def save(self, result):
        pass

In this code, ‘welcome’, ‘input_parameters’, ‘save’ and the 2 ‘analyze’ methods we’ve seen before are just entry points for every type of calculators.

In my program, I had a base class, because there’s some common code for each calculator, ‘welcome’ and ‘save’ methods are the same.

‘welcome’ method is just a welcome message at the start of each calculation.

‘save’ is a method which save the result of each health indicator to as text file.

This is shown in the following code.

from datetime import datetime
import os
from health_calculators.health_calculator_interface import HealthCalculatorInterface

# base class is needed because 2 methods are the same for all calculators
class HealthCalculatorBase(HealthCalculatorInterface):
    def __init__(self, name):
        # calls the parent class constructor
        super().__init__(name)

    def person(self, person):
        self.person = person
# welcome and save methods are the same for all calculators

    def _welcome(self, name):
        print("Welcome %s to my %s calculator !\n" %
              (self.person.name, self.name))

    def _save(self, indicator):
        """save save indicator to text file
        Args:
            indicator (float): indicator value
        """        ''''''
        now = datetime.now()
        today_str = now.strftime("%d-%m-%Y")
        document_path = os.path.expanduser('~\Documents')
        f = open(document_path + "\\" + self.person.name +
                 "_" + self.name + "_" + today_str + ".txt", "w")
        f.write(indicator)
        f.close()

Finally, we can see how the methods are called in the main program. For example, the ‘calc’ method is used without knowing what code is executed. We just know that we are using a calculator object and this method exists.

        calculator = object()
        cls()
        name = str(input("Hi, what's your name ?\n"))
        person = Person(name)
        calc_choice = str(
            input("Do you want to calculate your Body Mass Index (1) or Ideal Body Weight (2) ?\n"))

        if int(calc_choice) > 2:
            print("No others calculators implemented")
            return
        else:
            calculator_config = parse("calculators.ini", calc_choice)
            if 'calculator_table' in calculator_config:
                calculator_table = calculator_config['calculator_table']
            # object is dynamically created with the class name founded in an ini file
            calculator = create_instance(
                calculator_config['calculator_name'] + "()")
            calculator.person = person

        calculator.welcome(name)
        calculator.input_parameters()

        health_indicator = calculator.calc()
        result_message = "Your %s is %.2f" % (calculator.name, health_indicator)
        print(result_message)

If you read well, you can see on the first line of code upper, that the calculator is of type ‘object’. I use a little trick to dynamicly create by reading the class name in an ini file and use the ‘create_instance’ method. With this, we can imagine create objects of a known type and add some new calculators only by adding a section in the ini file. Powerful, isn’t it !

This is the end, my friend ! If you like the article, don’t forget to mention it. I you think it can be improved, I think it can be, don’t hesitate. If your read some mistakes or you don’t understand something, use my e-mail. ๐Ÿ˜‰

July 28, 2022

Tags: ,

Leave a Reply

Your email address will not be published. Required fields are marked *