Object Oriented Programming

Object Oriented Programming#

  • Programming with functions/procedures is referred to as procedural programming

    • Sufficient for most engineering computational tasks

  • Object Oriented Programming (OOP)

    • A programming paradigm where computations are centered around objects

    • Objects combine:

      • Data – referred to as attributes

      • Functions – referred to as methods

    • Enables description of real-world objects computationally

  • A class is a blueprint/template for creation of objects of a certain type

    • Objects are instances of classes

  • Python is a multi-paradigm language

    • Supports both procedural and OOP

      • Everything in Python is in fact an object of a specific type

      • E.g., int, complex, str, list, function, module

  • Other multi-paradigm languages

    • C++, Java, Matlab, C# etc.

  • C and Fortran (pre-2003) do not support OOP

  • OOP assumes that a programmer implements classes for other programmers to use

  • Key principles of OOP

    • Encapsulation

      • Mechanism for combining data with functions and restricting access to the object’s components

      • Enables redefinition/reconfiguration of the class without affecting the user (provided the mechanisms to access data and the object’s behavior remains the same)

  • Key principles of OOP

    • Inheritance

      • Mechanism to extend/reuse classes

      • Enables creation of new derived classes from base classes

    • Polymorphism

      • In the OOP context, it refers to a mechanism that determines which class method is to be used in a hierarchy of derived classes

      • A more general concept where an operator is agnostic to the data types it operates on (e.g., the len function, +/* operators)

Python Classes#

  • Classes in Python are defined using the class statement

    • With an optional base class, the class becomes a derived class and inherits attributes and methods from the base class

    • Multiple inheritance from more than one base class is possible

    • An indented block of Python statements (typically, function definitions) form the body of the class definition

class ClassName(OptionalBaseClassName):
    '''Descriptive String'''
    #class body
    statement 1
    statement 2
    .
    .
    statement n
  • The class itself is an object in python

  • A variable when defined creates an instance of a specific class

    • e.g 5 is an instance of the class int

    • e.g. [1, 3, 5] is an instance of the class list

  • type and isinstance functions used to determine class membership

  • The classes have many methods (special functions) that can be used by instances of the class

    • e.g. append is a method for lists

    • dir function used to see all methods of a class

print(dir(list))
['__add__', '__class__', '__class_getitem__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getstate__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
  • Example: create a custom Complex class

  • Note: Python classes can be modified after the initial definition

    • Not recommended

class Complex():
    '''My complex class'''
    def __init__(self, real = 0, imag = 0):
        '''Initialization method'''
        self.re = real
        self.im = imag
        self.abs = (real**2 + imag**2)**0.5
        
    def __str__(self):
        '''String form of the object'''
        if self.im < 0:
            return f"({self.re} - {abs(self.im)}j)"
        return f"({self.re} + {self.im}j)"
    
    def GetAbsVal(self):
        '''Accessor for absolute value'''
        return self.abs
    
    def conjugate(self):
        '''Returns the complex conjugate'''
        return Complex(self.re, -self.im)
        
  • Definition following the common convention

    • Typically, all the statements in a class definition are method definitions

    • The first parameter for methods is always named self and is the object itself

    • __init__ is a special method that is called when an object is to be initialized

      • Initial object state (attribute values) is defined in this method

    • __str__ is another special method that returns the string format of the object

      • Used by the print and str function if defined

    • The double underscore methods are for internal use and not to be used with the objects by convention

    • Accessor methods (GetAbsVal is an example here) to get and set attributes, typically

      • Set methods are used to alter the state of the object

        
c1 = Complex(5, -2)
c2 = c1.conjugate()
print(c1)
print(c2)
(5 - 2j)
(5 + 2j)
  • The self argument for any method call is automatically included

  • Additional attributes and methods can be added to a specific object

# Instance attribute (specific to object c1)
c1.phase = 0

Default Behavior#

  • Python provides all new classes some default behavior for a few special methods like __init__, __class__ etc.

  • This behavior can be redefined through a mechanism known as overloading

c1 = Complex(1, 2)
c2 = Complex(3, -1)
c1 + c2
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[5], line 3
      1 c1 = Complex(1, 2)
      2 c2 = Complex(3, -1)
----> 3 c1 + c2

TypeError: unsupported operand type(s) for +: 'Complex' and 'Complex'
  • Leads to an exception as + operator is not overloaded

  • Operator overloading

    • Define/redefine what an operator does with your object(s)

    • Need to overload the special methods:

      • __add__ for + (addition)

      • __sub__ for - (subtraction)

      • __mul__ for * (multiplication)

      • __truediv__ for / (division)

class Complex:
    def __init__(self, re = 0, im = 0):
        self.re = re
        self.im = im
        self.abs = (re**2 + im**2)**0.5
        
    def __str__(self):
        if self.im < 0:
            return f"{self.re} - {-self.im}j"
        return f"{self.re} + {self.im}j"
        
    def __add__(self, other):
        return Complex(self.re + other.re, \
                       self.im + other.im)
    
    def __sub__(self, other):
        return Complex(self.re - other.re, \
                       self.im - other.im)
        
    def conjugate(self):
        return Complex(self.re, -self.im)
    
        
c1 = Complex(1, 2)
c2 = Complex(3, -1)
print(c1 + c2)
print(c1 - c2)

Inheritance#

  • Subclasses (i.e., derived classes) inherit methods and functionality from superclasses (i.e., parent classes)

  • super() function allows access to superclass methods

  • Redefining a method in the subclass will override the definition from the superclass

  • Example: a Shape class for 2D shapes

    • This will serve as the base or parent class

class Shape:
    def __init__(self, name):
        self.name = name
        
    def __str__(self):
        return self.name
        
    def area(self):
        print("Placeholder")
    
    def fact(self):
        return "A two-dimensional shape"    
    
  • Derived class Square from the base class Shape

    • Overrides the __init__, area and fact methods

    • Inherits the __str__ method from the base class

class Square(Shape):
    def __init__(self, length):
        super().__init__("Square")
        self.length = length
                
    def area(self):
        return self.length**2
    
    def fact(self):
        return "Square has 4 sides"
a = Square(4)
print(a)
print(a.area())
print(a.fact())
  • Derived class Square from the base class Shape

    • Overrides the __init__ and area methods

    • Inherits the __str__ and fact methods from the base class

     
class Circle(Shape):   
    def __init__(self, radius):
        super().__init__("Circle")
        self.radius = radius
        
    def area(self):
        return 3.14159*self.radius**2  
b = Circle(2)
print(b)
print(b.area())
print(b.fact())
  • When a method is called on an object, the Python interpreter looks for the method definition in the derived class first and then the base class - this is polymorphism