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…