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:
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.
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()
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?
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
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
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!
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()
ax = plt.gca()
star1.move(xy=(0.8,0.8))
star1.render(ax)
plt.show()
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.
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)
myStar
.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.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
!
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()