Oop In Python

Oop In Python

Classes

All the functions in a class should have at least one parameter and by default, we call it self.

self is a reference to the current object

# CREATEING CLASS 
class Point:
    def draw(self):
        print("draw")

# CREATING OBJECT FROM THE CLASS 
point = Point()

# CHECKING THE TYPE OF THE OBJECT
print(type (point))

# ===================================

# if we do a print statement of the point object 
print(point)

# the output is 
# < __main__.Point object at 0X23143532>

# __main__ means the main module 
# Point is the name of the class

Check if an object is an instance of a class

print (isinstance (point, Point) )  # True
print (isinstance (point, int) )  # False

In python class we have some special functions that we call magic methods. init is one of them. This is the constructor method.

Constructor

class Point:
    def __int__(self, x, y):
        self.x = 0
        self.y = y

    def draw(self):
        print(self.x)

# while calling the draw function we can pass current 
# object as the argument but that is unnecessary
# like this 

point.draw(point)

Class level and Instance level attributes

Class level attributes are the same for all the objects and object-level attributes are different for all the objects

class Point:
   default_color = "red"   # this is class level attribute

        def __int__(self, x, y):
            self.x = 0    # these are instance level attributes 
            self.y = y

   def draw(self):
       print("draw")

point = Point()

point.default_color     # we can access through the object 
Point.default_color     # we can acces thourgh class 
                        # both values are same

Class level and instance level methods

to create class-level methods we use decorators

class Point:

    @classmethod        # this is a decorator
    def zero(cls):      # cls is just a convention we can use any other name
        return cls(0,0)        # this is like Point(0,0)

    def draw(self):
        print("draw")

point = Point()

Magic methods

Here is a link to a great resource about magic methods

A Guide to Python's Magic Methods

Example of magic methods:

str: this helps to convert the object into a string

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

    def __str__(self):
        return F"({self.x}, {self.y})"

point  = Point(1,2)
print (Point)

# now this will output as 
# (1,2)

# or we can just do
print(str(point ))

comparing objects

If we compare 2 objects then by default the memory address of these objects will be compared. just like JavaScript

we have different magic methods that we can use to compare objects

# magic method to check equlity
def __eq__(self, other_object):
    return self.x == other_object.x and self.y == other_object.y

# magic method to check >
def __gt__ (self, other):
    return self.x > other.x and self.y > other.y

NOTE: when we implemented gt , python will automatically implement lt operator

Performing arithmetic operators

def __add__(self, other):
    return Point(self.x + other.x, self.y + other.y)

Creating custom containers

This is how we can create a custom container with the help of magic methods

class TagCloud:
    def __init__(self):
        self.tags = {}

    def add(self, tag):
        self.tags[tag.lower()] = self.tags.get(tag.lower(), 0) + 1

    def __getitem__(self, tag):
        return self.tags.get(tag.lower(), 0)

    def __setitem__(self,tag,count):
        self.tags[tag.lower()] = count

    def __len__ (self):
        return len(self.tags)

    def __iter__(self):
        iter(self.tags)

Private members

just start the variable with __ (double underscore)

class TagCloud:
    def __init__(self):
        self.__tags = {}

cloud = tagCloud()
print(cloud.__dict__)  # this __dict__ contains all the members

Property

a property is an object that sits in front of an attribute and helps in getting and setting the attribute

class Product: 
    def __init__ (self, price):
        self.price = price

product = Product(-50)

# the python compiler will go ahead and enter a -ve
# value of price
# but we know that price can not be -ve 
# so we have to solve this problem

solution 1: we can make the price field private and then make two methods to set and get the price field

class Product: 
    def __init__ (self, price):
        # self.__price = price
        self.set_price(price)  # now do this 

    def get_price(self):
        return self.__price

    def set_price(self):
        if value < 0:
            raise ValueError("price can not be -ve.")
        self.__price = value

product = Product(-50)

This is not the pythonic way of solving this problem. In python, we have a better way.

solution 2 : using properties

class Product: 
    def __init__ (self, price):
        # self.__price = price
        self.set_price(price)  # now do this 

    def get_price(self):
        return self.__price

    def set_price(self):
        if value < 0:
            raise ValueError("price can not be -ve.")
        self.__price = value

    # we have to create a class attribute with an ideal name
    price = property(get_price, set_price)

product = Product(-50)

The property function has 4 parameters

  1. fget - a function to get the attribute
  2. gset - a function to set the attribute
  3. fdel - a function to delete the attribute
  4. doc - for documentation

we have to create these functions

# now we can use the price property
product = Product(10)
product.price = -1  # we can not do this 
print(product.price)

The Benefit

now we do not have to use the get_price and set_price functions to get and set the value of the price property.

we can just use price as a normal attribute

but

we still get the benefits of set_price and get_price functions.

This makes the interface of the object cleaner.

Making the interface even cleaner

we have to hide the functions get_price and set_price from the user. as the user of the object does not need any of these methods.

Hiding unneccessary functions

see we still have these methods in the suggestions

sulution : we can make the get_price and set_price functions, private. we can do this by adding __ in front of the functions of we can use a decorator.

class Product: 
    def __init__ (self, price):
        # self.__price = price
        self.price = price  # now jsut use the price attribute

    @property
    def price(self):  # rename the method to the ideal name
        return self.__price

    @price.setter  #ideal_name.setter
    def price(self):  # change the function name here also
        if value < 0:
            raise ValueError("price can not be -ve.")
        self.__price = value 

    # ---- insted of calling the property object explicitly
    # ---- we can use the decortor fucntion
    # price = property(get_price, set_price)

Creating a read only attribute

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

    @property
    def price(self):  
        return self.__price

# without any setter, the property is read only

Inheritance

class Mammal:
    def eat(self):
        print("eat")

    def walk(self):
        print(walk)

class Fish:
    def eat(self):  # WE REPEATED OUSELF 
        print("eat")  # WE NEED TO USE INHERITACE
class Animal:
    def __init__ (self):
        self.age = 1  # we can also inherit attributes

    def eat(self):  # we can inherit methods 
        print("eat")

class Mammal(Animal):  #this is how we inherit 
    def walk(self):
        print("walk")    

class Fish(Animal):
    def swim(self):
        print("swim")

m = Mammal()
m.eat()

The object class

print(isinstance(m, Mammal))
# => True

print(isinstance(m, Animal))
# => True

This is because m is an object of Mammal class and also the Animal class.

Mammal is inherited from Animal class.

The Animal class is also inherited from the object class

this happens implicitly.

class Animal(object):  # we can do this explicitly
    def eat(seld):
        print("eat")

print(isinstance(m, object))
# => True

This is how we get all those magic functions. They are inherited from the object class.

print(issubclass(Mammanl, Animal))
# => True

print(issubclass(Mammanl, object))
# => True

Method Overriding

If we create a constructor in the child class then the constructor of the parent class in not executed

class Animal:
    def __init__ (self):  # this constructor will not be executed now 
        self.age = 1  

    def eat(self):  
        print("eat")

class Mammal(Animal):  
    def __init__(self):
        self.weight = 70

    def walk(self):
        print("walk")    

m = Mammal()
print(m.age)  # this line will give error
print(m.weight)

The constructor in the base class replaced the constructor in the parent class. This is called method overriding.

solution : Use the super() function

to execute the functions in the parent class we can use the super function

class Animal:
    def __init__ (self):  
        self.age = 1  

    def eat(self):  
        print("eat")

class Mammal(Animal):  
    def __init__(self):
        super().__init__()  # this will call the __init__ functions of the parent class
        self.weight = 70

    def walk(self):
        print("walk")    

m = Mammal()
print(m.age)  # this line will not give error now
print(m.weight)

Multilevel Inheritance

Too much inheritance can increase complexity.

limit inheritance to one or 2 levels only.

class Animal:
    def __int__(self):
        print ("i an animal")

class Bird(Animal):
    def __int__(self):
        print("i can eat")

class Chicken(Bird):
    def __int__(self):
        print("i am chicken")

'''
this is an example of multilevel inheritance 
'''

Multiple Inheritance

Having multiple base classes is called multiple inheritance.

class Employee:
    def greet(self):
        print("employee")

class Person:
    def greet(self):
        print("person")

class Manager(Employee, Person):
    pass # so that i do not have to write anything inside the class

manager = Manager()
manager.greet()  # => employee
# this is because we inherited the Employee class first in the Manager class

This is a problem as if a programmer in the future changes the order of inheritance then the program will have different behavior.

Hence multiple inheritance when not used correctly, can create complexity.

Abstract base class

abstract base class is used to provide a boilerplate code to the derived classes so that all the derived classes can have the same interface.

Example : Let there be a base class Stream and two derived classes NetworkStream and FileStream.

Both the derived class have some common functions hence they are defined in the base class.

There is one function read that has a different implementation in both the derived classes.

Our purpose is to make sure that in the future if we make another derived class like AnotherStream then it should also have a read function.

Hence we use the abstract base class.

# =====================================
# **** abstract class

class Stream (ABC):
    def __init__(self):
        self.opened = False

    def open(self):
        if not self.opened:
            self.opened = False

    @abstractmethod
    def read(self):     # this is an abstract method with no implementation
        pass

class FileStream(Stream):
    def read(self):
        print("i am reading a file...")

m = Stream()        # this will give error

''' 
we can not create an instance of an abstract class 
'''

'''
If a class has been derived from an abstract class and 
it has not implemented the abstract method then
it is also considered an abstract class.
'''

Polymorphism

Polymorphism means many forms.

The same function acts differently based on the input.

# ============================================
# **** polymorphism

from abc import ABC, abstract method

# an abstract class
class UIControl(ABC):

    @abstractmethod
    def draw(self):
        pass

# derived classes 
class TextBox(UIControl):
    def draw(self):
        print("textbox")

class DropDown(UIControl):
    def draw(self):
        print("dropdown")

# the below function take a UIControl object such as 
# TextBox object and calls draw method on it 
def draw(control):
    control.draw()

dd = DropDown()
tb = TextBox()
draw(dd)        # prints => dropdown
draw(tb)        # prints => textbox

# we can make the draw function to take a list of UIControl objects 
# and then call the draw function on each on them

'''
def draw(controls):
    for control in controls:
        control.draw()

#  now we can call the funcion like this 
draw([ tb, dd ]) 
'''

'''
Using this method we can render the user interface of an application
like if we want to render a form that has text boxes and dropdowns
then we can pass a list of all the UIControl objects that we need and 
some functions like the draw function can then render it. 
'''

Here the draw() method is showing polymorphism. Based on the input it prints different things.

Creating an abstract class is a best practice but the above example will still work without it as long as the derived classes have a draw method.

Did you find this article valuable?

Support Harshit Saini by becoming a sponsor. Any amount is appreciated!