Python : Magic Methods
In python everything is an object and we are applying some operations on this objects. But every operation have a dedicated method behind the scene in corresponding type implementation.
>> x = 10
>> x + 10 is equivalent to x.__add__(10)
>> x * 2 is equivalent to x.__mul__(2)
>>> names = ['A', 'AB', 'ABC']
>>> names[0] is equivalent to names.__getitem__(0)
>>> names.__setitem__(0, 'BAB')
>>> names is now ['BAB', 'AB', 'ABC']
Now let us use this in a custom class.
class Point:
def __init__(self,x ,y):
self.x = x
self.y = y
def __add__(self,other):
return Point(self.x + other.x,self.y + other.y)
def __str__(self):
return 'Point x= {} and y = {}'.format(self.x, self.y)
p1 = Point(10,20)
p2 = Point(1,2)
p3 = p1 + p2
print(p3)
outputs :Point x= 11 and y = 22
str vs repr
While debugging if you evaluate our point object , it will be displayed like this.
>>p1
>><__main__.Point at 0x24961fbf898>`
__repr__
is used during debugging a lot, its called representation of the object and used widely for debug only purpose.
‘str’ is used for mainly for printing info.
>>d1 = datetime.date(2012, 12,24)
>>d1
outputs:
datetime.date(2012, 12, 24)
>>print(d1)
outputs:
>>2012-12-24
So now let us add this support to our point class as shown below
class Point:
def __init__(self,x ,y):
self.x = x
self.y = y
def __add__(self,other):
return Point(self.x + other.x,self.y + other.y)
def __str__(self):
return 'Point x= {} and y = {}'.format(self.x, self.y)
def __repr__(self):
return('Point({!r},{!r})'.format(self.x, self.y))
>>p1 = Point(10,20)
>>p1
outputs : Point(10,20)
>>print(p1)
outputs: Point x= 10 and y = 20
So when writing your own class ensure to include __str__
and __repr__
as best practice.
Context Managers
The basic resource management pattern we have been following so far is open resource, use it and close it
.
In case of file, we have been doing this safeguarding using with
statement.
with open(filename, 'r') as f:
Internally this is implemented using two magic methods __enter__ and __exit__
. So let us consider
our point class is a resource and needs to do some clean up after its usage. Then in that case we
can add context managers to our class as follows..
class Point(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other):
return Point(self.x + other.x, self.y + other.y)
def __str__(self):
return 'Point x= {} and y = {}'.format(self.x, self.y)
def __repr__(self):
return ('Point({!r},{!r})'.format(self.x, self.y))
def __enter__(self):
print('Entering')
return "A sample P"
def __exit__(self, ty, val, tb):
print('Exiting')
print(ty, val, tb)
As shown above (line:17)the return value of entering is enabling us to use as
statement along with
Now we can use this in with
context as shown below..
p = Point(10,20)
with p as val:
print(p)
print(val)
outputs:
Entering
Point x= 10 and y = 20
A sample P
Exiting
None None None
The paramters in __exit__
can be used to capture the exceptions and deal based on that.
p = Point(10,20)
with p as val:
print(p)
print(9/0)
print(val)
outputs:
Entering
Traceback (most recent call last):
Point x= 10 and y = 20
File "D:/Git/py-kitchen/apps/test-routines/oop.py", line 29, in <module>
Exiting
print(9/0)
Exception happend <class 'ZeroDivisionError'> division by zero <traceback object at 0x000001F94E5C4C48>
ZeroDivisionError: division by zero
So in the above case even if there was an exception in main program flow, __exit__
in point object gets
called and its aware of exception happend and handle its clean up irrespective of exception in main flow.
Example
As an example let us create a Portfolio holding collection object. So this class loads all holdings and provides a collection interface. The holding information is stored in a csv file.
Name,Date,Shares,Price
HPQ,7/11/2007,100,32.2
IBM,7/12/2007,50,91.9
GE,7/13/2007,150,83.44
CAT,7/14/2007,200,51.23
MSFT,7/15/2007,95,40.37
HPE,7/16/2007,50,65.1
AFL,7/17/2007,100,70.44
As a first step, let us create our Holding class.
class Holding(object):
def __init__(self, name, date, shares, price):
self.name = name
self.date = date
self.shares = shares
self.price = price
def __str__(self):
return 'Name = {}, Date={}, Shares={}, Price={}'.format(self.name,
self.date,
self.shares,
self.price)
Now let us create our Portfolio collection object that can hold Holdings and provides methods to access that. Here we will exercise our knowledge on some magic methods.
class Portfolio(object):
def __init__(self):
self.holdings = []
@classmethod
def load_from_csv(cls, filename):
self = cls()
with open(filename, 'r') as f:
rows = csv.reader(f)
head = next(rows) # skip header
for row in rows:
try:
self.holdings.append(Holding(name=row[0], date=row[1],
shares = int(row[2]), price=float(row[3])))
except ValueError as ve:
print('ignored{}'.format(row))
return self
def __len__(self):
return len(self.holdings)
def __getitem__(self, item):
return self.holdings[item]
def __iter__(self):
return self.holdings.__iter__()
def cost(self):
return sum([(holding.price * holding.shares) for holding in self.holdings])
So first step, we created a factory method (@classmethod) which creates a Portfolio from csv file.(line:5).
Then we added some magic methods that will provide collection like properties to Portfolio. We have
implemented __len__, __getitem__, __iter__
. In addition we have implemented a cost
method that
computes the value of the portfolio.
# program.py
portfolio = Portfolio.load_from_csv('stocks.csv')
for item in portfolio:
print (item)
print ('Total number of holdings is {}'.format(len(portfolio)))
print('Total value of portfolio is {}'.format(portfolio.cost()))
outputs..
Name = HPQ, Date=7/11/2007, Shares=100, Price=32.2
Name = IBM, Date=7/12/2007, Shares=50, Price=91.9
Name = GE, Date=7/13/2007, Shares=150, Price=83.44
Name = CAT, Date=7/14/2007, Shares=200, Price=51.23
Name = MSFT, Date=7/15/2007, Shares=95, Price=40.37
Name = HPE, Date=7/16/2007, Shares=50, Price=65.1
Name = AFL, Date=7/17/2007, Shares=100, Price=70.44
Total number of holdings is 7
Total value of portfolio is 44711.15
Coding is fun enjoy…