Python Object Oriented

Object oriented Python


  Machine Learning in Python

Contents

Introduction

Object Orientation is a big subject. However, in this course, we are only going look at just enough “Object oriented Python” that will give us the necessary background to pursue Machine Learning and Deep learning further. There are many aspects of classes and objects that we will delibearately ignore in the interest of keeping this simple and to serve the purpose of learning “Just enough” of Object oriented python. With that disclaimer, let’s begin.

Imagine a bank account. If you were writing a simple python program to hold the account number, account name, account balance etc and calculate some of the commonly asked things like interest accrued, total account balance etc, how would you do it ?

In a traditional programming model, this is how you would write the program in python.

The same logic can be written in a class, by clearly separating ( as well as encompassing ) all of the above data and functions into a class. A class essentially a set of data and functions.

And the way you define the class in python is very simple.

Your first Python class

Let’s start to write it out step by step. First thing is to define the class and initialize it.

Initialization

class Account : 
    
    def __init__ ( self, number) :
        self.number  = number

That’s the bare minimum you have to do. You can now instantiate the class into an object. Think of the class as the blueprint and the object as an instantiation of the blueprint.

acc = Account()
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-75-2ef89db5cd52> in <module>
----> 1 acc = Account()

TypeError: __init__() missing 1 required positional argument: 'number'

What happened ? Well, the way you have defined the class, you need an argument to be passed to the initialization function (init)

acc = Account( 1001 )

acc.number

1001

Variables & Methods

There you go, you have created your first object. Now, let’s finish up the rest of the methods.

class Account : 
    def __init__ ( self, number ) :
        self.number = number
     
    def account_type(self) :
        # Get the account type
        if str(self.number).startswith("1") :
            self.type = "current"
        elif str(self.number).startswith("2") :
            self.type = "saving"
    
    def interest_rate(self) :
        
        self.account_type()
        if self.type == "current" :
            self.interest = 0
        elif self.type == "saving" :
            self.interest = 5
        return self.interest
acc = Account(2001)
acc.interest_rate()

5

Classes vs Objects

Just in case you didn’t know which class an object belonged to , use the type ( ) method. For example, if you wanted to know which class the object acc belonged to,

type(acc)
__main__.Account

You can get the same info from the built-in method class . More on this later, when we visit operator overloading.

acc.__class__
__main__.Account

If you wanted to check if an object belonged to an class, there is an alternate method. Use the isinstance ( ) method. It returns True if the object is an instantiaon of the class.

isinstance(acc,Account)
True

Challenge – Create a method – interest_accrued ( ) – that calculates the interest accrued this year. Assume a balance of 1000.

def interest_accrued(self) :
    
    # call interest_rate to determine the interest rate
    self.interest_rate()
    
    balance = 1000
    
    # to calculate the number of days in the year so far, include datetime above the class definition
    current_date   = date.today()
    beginning_date = datetime(current_date.year, 1,1)
    
    self.interest_accrued = balance * ((current_date - beginning_date ) / 365) * self.interest_rate / 100

And here is the complete class.

from datetime import date

class Account : 
    def __init__ ( self, number ) :
        self.number = number
     
    def account_type(self) :
        # Get the account type
        if str(self.number).startswith("1") :
            self.type = "current"
        elif str(self.number).startswith("2") :
            self.type = "saving"
    
    def interest_rate(self) :
        
        self.account_type()
        if self.type == "current" :
            self.interest = 0
        elif self.type == "saving" :
            self.interest = 5
        return self.interest
    
    def interest_accrued(self) :
    
        # call interest_rate to determine the interest rate
        self.interest_rate()

        self.balance = 1000

        # to calculate the number of days in the year so far, include datetime above the class definition
        current_date   = date.today()
        beginning_date = date(current_date.year, 1,1)

        self.interest_accrued = self.balance * (abs(current_date - beginning_date ).days / 365) * self.interest / 100
acc = Account(2001)
acc.interest_accrued()
print ( acc.balance )
acc.interest_accrued
1000
19.31506849315069

Now that we understand the basics of classes and objects, let’s revisit some of things that we have understood so far in Python with the new Object Oriented knowledge we have gained. Everything in Python is an object. This is in contrast to most programming languages like C or even other object oriented programming languages like Java or C++. Let me explain why.

age = 1
type(age)
int

age is an integer. But it is not the same integer that you see in other programming languages. It is not a fundamental data type that is pointing to an integer 1. It is a whole class in itself. How do we know that int is a class ? and not just a variable pointing to some data ? Just do an age. and wait for suggestions from your IDE.

age
1

These are just some of the methods. Try this for some more methods – age.__ and hit tab. There are so many more methods.

Point being, just by defining the variable as age = 1 , you have created an object ( of class int ). And why do so many methods exist for just an integer class ? Let’s do a simple addition of integers as we know if so far.

age_1 = 21
age_2 = 22

age_1 + age_2

43

The same can be done using the integer object’s method add as shown below.

age_1.__add__(age_2)
43

Why does it work both ways ? It is based on another concept in object oriented programming called Operator Overloading .

Operator Overloading

When you use + to add two objects ( not just numbers ) , python will automatically invoke the add method of one of the objects.

The point being, even simple operations like additions are actually object oriented methods. This gives rise to another question. Can you have your own add operation on your custom classes and perform a custom add ? Let’s give it a try.

acc_1 = Account(1001)
acc_2 = Account(2001)

acc_1.balance = 5000
acc_2.balance = 2000

total_balance = acc_1 + acc_2

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-62-c2b817a6406d> in <module>
      5 acc_2.balance = 2000
      6 
----> 7 total_balance = acc_1 + acc_2

TypeError: unsupported operand type(s) for +: 'Account' and 'Account'

As you can see, objects cannot be added together. Now, let’s try to give the Account class an add method and give the same operation a try again.

# define a function
def __add__(self,account):
    return self.balance + account.balance

# Dynamically set the function as a method of the class "Account"
setattr(Account, '__add__', __add__)

total_balance = acc_1 + acc_2

total_balance
7000

With integers, the action add is relatively straight forward. However, Add might mean different things to different objects. For example, in the case above, when you add two accounts, the balances should be added. Whether balances of the accounts are added or the interests accrued are added is based on the business scenario and you can write the code either way.

These special methods ( like add ) are not limited to just addition. There is a whole host of methods. You can see the entire list on Python help page’s Special Methods . Whenever you think of these special methods, think of operator overloading. For example, here are some of these special methods.

lt – less than 
gt – greater than 
eq – equals to 

etc

Challenge – Create a eq method that compares the account balance of two accounts and returns True if the balances are equal.

Solution

# define the function
def __eq__(self,account):
    if self.balance == account.balance :
        return True
    else :
        return False

# Dynamically set the function as a method of the class "Account"
setattr(Account, '__eq__', __eq__)

acc_1.balance = 2000
acc_2.balance = 2000

acc_1 == acc_2
True

Why Operator Overloading

The key advantage of operator overloading is that you can make any object work like a simple built-in data type. Sure you can do without it, but it makes writing code a bit more concise. In programming, this is also called syntactic sugar.

Inheritance

One of the prominent features of Object Oriented programming is Inheritance. Yes – it is very much similar to how you inherit your traits (genes) from your parents or grandparents. In case of programming, these traits are just variables and methods. Continuing with our bank account example, say, there are special accounts like this.

  • Brokerage(demat),
  • Salary account(savings account with special features for salaried individuals),
  • 3-in-1 account(saving, brokerage, IRA or PF bundled into one account )

All of these accounts have the same basic features – like

  • account number
  • account name
  • account balance etc

On top of this, each of these special accounts have more specific details like

  • Brokerage account
    • Demat ID
    • Portfolio
    • Pending orders etc
  • Salary account
    • special interest rate
    • direct debit facility etc
  • 3-in-1 account
    • all of the above plus a couple more freebies thrown in

To model these different type of classes, essentially, Python allows us to build a hierarchy of classes based on Inheritance

In terms of the specific variables and methods that are inherited (based on the example above), the following picture can act as a good visual.

Just to make things concrete, lets’s create all of these variables and methods with just dummy contents.

class Account :
    
    def __init__ (self,number,name,balance) :
        self.account_number  = number
        self.account_name    = name
        self.account_balance = balance
        
    def calculate_balance(self) :
        # calculate account balance
        pass
    
    def calculate_interest(self) :
        # calculate interest amount on the balance so far
        pass
        
    def calculate_tax (self) :
        # calculate income tax on the balance for the fiscal year   
        pass
class Demat(Account) :
    def calc_capitalgains(self) :
        # calculate capital gains on the total investment portfolio
        pass
    
    def calc_cash_position(self) :
        # calculate the current cash position based on amount available and pending orders
        pass
    
    def get_stockpicks (self) :
        # get the stock picks based on recommendations from an API
        pass
    
    def calc_open_positions(self) :
        # calculate the open positions to check how much is invested so far
        pass
# Create an Account object
acc = Account ( 1001, "Ajay Tech",  0)

# Create a demat account object
acc_d = Demat ( 1001, "Ajay Tech Business", 0 )

The parent Account object does not have the calc_capitalgains() method.

acc.calculate_balance()
acc.calc_capitalgains()

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-105-d71e4fa09cce> in <module>
      1 acc.calculate_balance()
----> 2 acc.calc_capitalgains()

AttributeError: 'Account' object has no attribute 'calc_capitalgains'

because, it is only available in the inherited Demat class.

acc_d.calculate_balance()
acc_d.calc_capitalgains()

So far so good. What if you are not happy with the inherited method ? Say, the way you calculate the account balance is different between the different accounts ? Say this is the logic to calculate the account balance among the different type of accounts

  • demat account
    • based on total cash – pending orders
  • salary account
    • based on total cash
  • 3-in-1 account
    • based on total cash – pending orders – pf/ira orders

So, it is common for the child class to have an implementation of methods specifically tailored to its needs while the definition and signature might probably remain the same. That is where polymorphism comes in.

Polymorphism

Although the child class inherits the members/methods from the parent class, the implementation of the method need not always be the same. For example, the way the account balanace is calculated could be different between a regular savings account and a brokerage account. So, in the case of a brokerage(demat) account, the definition of calculate_balance ( ) method as inherited from the parent Account class will be overridden with a new implementation that is specific to the Brokerage (demat) account.

Let’s create the definition of account_balance() specifically for demat account.

ef calculate_balance(self) :
    # calculate balance based on total cash - pending orders
    print ( "based on cash")

def calculate_balance_d(self) :
    # calculate balance based on total cash - pending orders
    print ( "based on cash - pending orders")
    
setattr(Account,"calculate_balance",calculate_balance)    
setattr(Demat  ,"calculate_balance",calculate_balance_d)
acc_d = Demat(1001,"Ajay Tech", 0)
acc_d.calculate_balance()

based on cash – pending orders

acc = Account(1001,"Ajay Tech", 0)
acc.calculate_balance()

based on cash

This is useful to learn because many times you see that there are custom implementations of standard library classes giving a twist to the way the implementation is done.

import time 

sum = 0

#before
start_time = time.time()

for num in range(10000000) :
    sum = sum + num

end_time = time.time()

print ( "sum = ", sum)

python_time = end_time - start_time

python_time

sum =  49999995000000
1.3357877731323242
%d bloggers like this: