Object Oriented Programming in Python

Object Oriented Programming (OOP) is a programming paradigm that allows modeling of computational solutions to programming problems in terms of objects and their interactions. It encapsulates data (i.e., attributes) and functions (i.e., behaviors) of objects into components called classes. A class acts as a template or a blueprint for creating objects that have attributes and functions associated with that class. Python is a fully object oriented programming lagnguage that support multiple inheritance. You should first read more about classes in the python documentation. This is a required reading as it will tell you, among other things, about:

  • How objects are created
  • How Python classes are created on the run time
  • How can they be modified after creation
  • How Python supports multiple inheritance
  • How everything in a class in Python is public

Below, we take a simple example of a shape class hierarchy. For drawing purposes we inherit the shape objects from corresponding shape primitives in matplotlib.patches. We create a myShape object which has a single property called name which is the name of the shape object we want to draw. It has the following behaviors:

  • Render: It takes an axes as input and draws the shape on that axes
  • __str__ and __str__ : Both these functions return a string representation of the object so that we can print it (Try your first implementation without these!)

Each class has a __init__ method. This method is called the initializer. It essentially creates an object of that class. You can specify the properties of the object within this function. For example, for the myShape object, we assign the name parameter to the self.name property of the object after checking that it is not empty or None or composed entirely of whitespaces.

The self variable represents the instance of the object itself. When you make an instance of myShape class as m = myShape (name = 'my first star'), the self object is not passed as an explicit argument. However, what Python does is that it creates an object and passes it as the first argument (self). m then becomes an alias to that object. Analogs to the self exist in other languages but for most of such languages, such C++, these are typically implicit. However, Python defines it an explicit manner as it greatly simplifies a number of things such as function overriding, multiple inheritance, naming confusions, etc.

Also note that render uses the add_artist function of the axes class. add_artist expects an object of class Artist. However, myShape is not an sub-class of Artist. As a consequence, you cannot render an object of the class myShape. You can try this! This is done by design -- we will be deriving (i.e., inheriting) classes from myShape which will also inherit from Artist or any of its sub-classes. For example, our axis aligned rectangle class myAARectangle is inherited simultaneously from myShape and matplotlib.patches.Rectangle.

The inheritance of myAARectangle from myShape allows it to have the render function and the name property which are absent from matplotlib.patches.Patches. Note that we do not explicitly set the name property within this class. This is because we make a call to the base class initializer which does this for us.

The inheritance of myAARectangle from matplotlib.patches.Rectangle allows us to utilize matplotlib to render the objects. This minimizes code re-writing. Also notice that myAARectangle has no properties of its own -- all its properties and functions (except __init__) are inherited. This is because we re-use the properties of matplotlib.patches.Rectangle such as height, width, xy (location of the rectangle's bottom left corner in the axis). One can possibly specify other properties (such as color or fill) of matplotlib.patches.Rectangle as keyword arguments. However, the use of height, width and xy in __init__ forces these to be specified even if as default argument values. This is done by making a call to the base class initializer as Rectangle.__init__(self,xy=xy,width=width,height=height,**kwargs).

When an object of myAARectangle is created using rg = myAARectangle(name='rect-1',xy=[0, 0], width=0.5, height=0.5,color='r'), the object is named rect-1 and it is located at [0 0] with a width equal to height of 0.5 and red color. This statement calls the initializer of myAARectangle which does the rest of the processing by making calls to base class initializers.

When we call rg.render(ax) with ax as an argument, the rectangle gets added to the axes ax through a call to the baseclass render which now works because the object being passed fulfills the requirements of the add_artist function of Axes.

The myCircle class makes use of exactly similar concepts.

In [27]:
from matplotlib.patches import Rectangle,Circle, Polygon
import matplotlib.pyplot as plt
import math

class myShape:
    def __init__(self,name):  
        if name is None or type(name)!=type('') or  not name.strip():
            raise ValueError('Shape name cannot be empty!')            
        self.name=name
        
    def render(self,ax):
        """
        Add the current object to the figure axes
        """
        ax.add_artist(self)
        
    def __str__(self):
        """
        Return a string form of the object
        """
        return self.name
    
    def __repr__(self):
        """
        Return the representation of the object
        """
        return self.__str__()        
    
class myAARectangle(myShape,Rectangle):
    """
    Axis Aligned Rectangle
    """
    def __init__(self,name,xy=(0,0),width=1,height=None,**kwargs):
        if height is None:
            height=width
        myShape.__init__(self,name=name) #calling myShape initializer for setting name properly
        # Note that we pass `self` as an explicit argument in this call.
        Rectangle.__init__(self,xy=xy,width=width,height=height,**kwargs) #Rectangle initializer for drawing
        # Note that we pass `self` as an explicit argument in this call.
        
class myCircle(myShape,Circle):
    def __init__(self,name,xy=(0,0),radius=1,**kwargs):        
        myShape.__init__(self,name=name)        
        Circle.__init__(self,xy=xy,radius=radius,**kwargs)
        
      
if __name__=='__main__':
    
    rgb_green=(0,0.4,0.2)
    rg=myAARectangle(name='rect',xy=[0, 0], width=0.5, height=0.5,color=rgb_green)
    # Note that we pass `self` as an implicit argument in this call.
    yc=myCircle(name='circle',xy=[0.5, 0.5], radius=0.2, color='y')
    print rg
    print yc
    
    #Let's make a figure, specify its axis ranges
    plt.figure(0)       
    plt.axis('image')
    plt.axis([0,1,0,1])
    
    #let's get the axes and render our objects on it
    ax = plt.gca()
    rg.render(ax)
    yc.render(ax)
    
    
    plt.show()
rect
circle

Assignment

Draw 10 stars of random number of edges at random locations with random sizes and colors. Each star should have a name which is assigned by the user. After drawing, print all the properties (name, location, color and edges) of all stars. First develop a function based implementation of this task.

Then impelementd a class myStar which can generate an n-pointed star (the star in Pakistan's flag has n = 5) at a given location with its first pointed tip oriented at a given angle from the x-axis (for the star in Paksitan's flag, this angle is 45 degrees).

Hint: A star is a polygon.

You should also do a mental comparison of the implementation of this as a class and as a function. How is the class implementation better than the function?

Solution

Function Implementation

In [28]:
from random import random as rnd
from random import randint as rndi  
def makeStar(ax, xy=(0,0),n=5,theta=45,ri=0.25,ro=0.5,color='r',**kwargs):
    dt=2*math.pi/n
    t=theta*math.pi/180.0
    x0,y0=xy
    P=[]
    for i in range(n):
        pout=x0+ro*math.cos(t),y0+ro*math.sin(t)
        pin=x0+ri*math.cos(t+dt/2.0),y0+ri*math.sin(t+dt/2.0)
        t+=dt
        P.extend([pout,pin])    
    star = Polygon(P, closed=True,color=color,**kwargs)
    ax.add_artist(star)


#properties of the star I want
smax=10 # number of stars
rmax=0.2 # maximum radius
# Let's draw
plt.figure(0)       
plt.axis('image')
plt.axis([0,1,0,1])
#let's get the axes and render our objects on it
ax = plt.gca()
Names = []
for sno in range(smax):
        name = 'rand-'+str(sno)        
        n = rndi(2,20)
        c = (rnd(),rnd(),rnd()) #color
        xy = (rnd(),rnd())
        theta = rnd()*360
        ri = rnd()*rmax
        ro = rnd()*rmax
        makeStar(ax,xy=xy,n=n,theta=theta,ri=ri,ro=ro,color=c)   
        Names.append(name)

print Names
['rand-0', 'rand-1', 'rand-2', 'rand-3', 'rand-4', 'rand-5', 'rand-6', 'rand-7', 'rand-8', 'rand-9']

Class Implementation

In [29]:
from random import random as rnd
from random import randint as rndi  
class myStar(myShape,Polygon):
    def __init__(self,name,xy=(0,0),n=5,theta=45,ri=0.25,ro=0.5,color='r'):    
        myShape.__init__(self,name=name)
        self.edges = n
        self.color = color        
        
        dt=2*math.pi/n
        t=theta*math.pi/180.0
        x0,y0=xy
        P=[]
        for i in range(n):
            pout=x0+ro*math.cos(t),y0+ro*math.sin(t)
            pin=x0+ri*math.cos(t+dt/2.0),y0+ri*math.sin(t+dt/2.0)
            t+=dt
            P.extend([pout,pin])
        self.P=P # list of points
        Polygon.__init__(self,self.P, closed=True,color = color)
    def __repr__(self):
        return self.name + ' : ' + str(self.edges) + ' ' + str(self.color) + ' ' + str(self.xy[0])

#properties of the star I want
smax=10 # number of stars
rmax=0.2 # maximum radius
# Let's draw
plt.figure(0)       
plt.axis('image')
plt.axis([0,1,0,1])
#let's get the axes and render our objects on it
ax = plt.gca()
Stars = []
for sno in range(smax):
        name = 'rand-'+str(sno)        
        n = rndi(2,20)
        c = (rnd(),rnd(),rnd()) #color
        xy = (rnd(),rnd())
        theta = rnd()*360
        ri = rnd()*rmax
        ro = rnd()*rmax
        star = myStar(name,xy=xy,n=n,theta=theta,ri=ri,ro=ro,color=c)
        Stars.append(star)
        star.render(ax)

print Stars
Stars[0].color = 'r' # print color of the first star
[rand-0 : 5 (0.2718243743002856, 0.7852852091686071, 0.7185928254488492) [ 0.68948724  0.38879682], rand-1 : 8 (0.006450660671438846, 0.6369253483591193, 0.8573534943027019) [ 0.30947324  0.3306786 ], rand-2 : 3 (0.0026414330479413994, 0.08920406819115756, 0.5387486548037038) [ 0.85263616  0.21194561], rand-3 : 16 (0.30704930024689847, 0.719528489061925, 0.7061641002629729) [ 0.84183384  0.67943855], rand-4 : 4 (0.017375982404427837, 0.6275570559579229, 0.6839311143270719) [ 0.28523798  0.53672   ], rand-5 : 11 (0.936865199493178, 0.16600224493064297, 0.8159528081778056) [ 0.28664692  0.3144011 ], rand-6 : 7 (0.15286283011857815, 0.8791271617748918, 0.8299536399976313) [ 0.23073774  0.66100898], rand-7 : 5 (0.8869487846367423, 0.7835697660804558, 0.43626185462985445) [ 0.27945123  0.48202088], rand-8 : 4 (0.8333133627455516, 0.8506444713325404, 0.36543854900375505) [ 0.25470865  0.78060703], rand-9 : 13 (0.04592633864040019, 0.1682478140300181, 0.921709148319112) [ 0.11187536  0.80197566]]

The above experiment should have revealed to you that objects can be used to store and organize data which is hard to do when function programming is used and there a number of data to be saved. However, this is a very limited use of the object. The real power of the objects lies in their ability to change their behavior. For example, it is really easy to move a star from its current location using OOP. It also encapsulates (hides away the implementation details). However, function programming is not without its benefits. For example, we have created a function which allows us to write lesser code. This flexibility of Python to mix different programming paradigms is really useful!

In [30]:
def getStarPoints(xy,n,theta,ri,ro):
    dt=2*math.pi/n
    t=theta*math.pi/180.0
    x0,y0=xy
    P=[]
    for i in range(n):
        pout=x0+ro*math.cos(t),y0+ro*math.sin(t)
        pin=x0+ri*math.cos(t+dt/2.0),y0+ri*math.sin(t+dt/2.0)
        t+=dt
        P.extend([pout,pin])
    return P

class myStar(myShape,Polygon):
    def __init__(self,name,xy=(0,0),n=5,theta=45,ri=0.25,ro=0.5,color='r'):    
        myShape.__init__(self,name=name)
        self.edges = n        
        self.theta = theta        
        self.ri = ri
        self.ro = ro
        self.color = color
        P = getStarPoints(xy,self.edges,self.theta,self.ri,self.ro)
        Polygon.__init__(self,P, closed=True,color = color)
    def __repr__(self):
        return self.name + ' : ' + str(self.edges) + ' ' + str(self.color) + ' ' + str(self.loc)
    def move(self,xy):
        P = getStarPoints(xy,self.edges,self.theta,self.ri,self.ro) #calculate the new points based on new xy
        P.append(P[0])
        self.set_xy(P)
        
        
plt.figure(0)       
plt.axis('image')
plt.axis([0,1,0,1])
#let's get the axes and render our objects on it
ax = plt.gca()       

star1 = myStar('close star',xy=(0.2,0.2))
star2 = myStar('close star',xy=(0.4,0.2),color='k')    
star1.render(ax)   
star2.render(ax)   
plt.show()
In [31]:
ax = plt.gca()

star1.move(xy=(0.8,0.8))    
star1.render(ax)   
plt.show()

Exercise: Make a scene

Implemente a class myScene whose objects can display collections of graphical primitves as a scene. For example: the code below should generate the same figure as above:

scene = myScene(name = 'simple scene',contents=[rg,yc],vport=[0,1,0,1]) scene.show(figid = 0)

Here, name is the name of the scene, contents is a list (or tuple) containing the objects on the scene in the order in which they will be drawn and vport is the viewport, i.e., the extents of the scene. figid is the id of the figure on which the scene is to be drawn.

Solution:

In [32]:
class myScene:
    def __init__(self,name,contents=[],vport=[0,1,0,1]):
        if name is None or type(name)!=type('') or not name.strip():
            raise ValueError('Scene name cannot be empty!')
        self.name=name
        self.contents=contents
        self.vport=vport
    def show(self,figid=0):
        plt.figure(figid) 
        plt.axis('image')  
        plt.axis(self.vport) 
        ax = plt.gca()
        for s in self.contents:
            s.render(ax)               
        plt.show()
    def __str__(self):
        s=str(self.__class__)+' instance: '+self.name
        s+=' with '+str(len(self.contents))+' objects'
        return s
    def __repr__(self):
        return self.__str__()    

rgb_green=(0,0.4,0.2)
rg=myAARectangle(name='rect',xy=[0, 0], width=0.5, height=0.5,color=rgb_green)
yc=myCircle(name='circle',xy=[0.5, 0.5], radius=0.2, color='y')    
scene = myScene('simple scene',contents=[rg,yc,star1,star2],vport=[0,1,0,1])
scene.show(figid=1)

Exercise: Make the Pakistani Flag

  1. Use the class myStar.
  2. Implement a class myCrescent. A crescent can be drawn by an overlap of two circles with one cricle in drawn in the foreground color and the other one on top of this one in the background color.
  3. Make the Flag (or any other scene of your liking!). The details for making the flag are given below.

alt text

In [33]:
class myCrescent(myShape):
    def __init__(self,name,c0,r0,fcolor,c1,r1,bcolor):
        myShape.__init__(self,name=name)
        self.c1=myCircle(name=name+':c1',xy=c0,radius=r0,color=fcolor)
        self.c2=myCircle(name=name+':c2',xy=c1,radius=r1,color=bcolor)
    def render(self,ax):
        self.c1.render(ax)
        self.c2.render(ax)
        
     
def raisePakFlag():
    height=2/3.
    width=1.
    gwidth=0.75*width
    wwidth=width-gwidth
    rgb_green=(0,0.4,0.2)
    
    rg=myAARectangle(name='r-green',xy=[0, 0], width=width, height=height,color=rgb_green)
    rw=myAARectangle(name='r-white',xy=[0, 0], width=0.25*width, height=height,color='w')
    cr=myCrescent(name='crescent',c0=(wwidth+gwidth/2.0,height/2.),r0=(3/10.)*height,fcolor='w',c1=(0.67*width,0.56*height), \
                  r1=(11/40.)*height,bcolor=rgb_green)
    x=myStar('star',xy=(0.722*width,0.634*height),ro=0.1*height,ri=0.04*height,color='w')
    
    
    
    flag=myScene(name='flag',contents=[rg,rw,cr,x],vport=[0,width,0,height])
    flag.show()       
    
raisePakFlag()    

To celbrate the end of this lecture, let's make a scene of random stars using a generator!

In [34]:
def starGenerator(smax=10,rmax=0.2):   
    from random import random as rnd
    from random import randint as rndi     
    for sno in range(smax):
        name = 'rand:'+str(sno)        
        n = rndi(2,20)
        c = (rnd(),rnd(),rnd()) #color
        xy = (rnd(),rnd())
        theta = rnd()*360
        ri = rnd()*rmax
        ro = rnd()*rmax
        s = myStar(name=name,xy=xy,n=n,theta=theta,ri=ri,ro=ro,color=c)
        yield s

rscene=myScene(name='Stars',contents=starGenerator(smax=20),vport=[0,1,0,1])
rscene.show()