Object oriented Python
- Your first Python Class
- Classes vs Objects
- Operator Overloading
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.
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
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()
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,
You can get the same info from the built-in method class . More on this later, when we visit operator overloading.
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.
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
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)
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.
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
The same can be done using the integer object’s method add as shown below.
Why does it work both ways ? It is based on another concept in object oriented programming called 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
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
Challenge – Create a eq method that compares the account balance of two accounts and returns True if the balances are equal.
# 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
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.
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.
- 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
- 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.
--------------------------------------------------------------------------- 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.
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.
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