Lesson 14 - Classes

The following topics are discussed in this notebook:

  • Classes and instances.
  • Attributes and methods.
  • Constructors

Classes

A class is a programmer-defined data type. Classes can contain variables, which are called attributes.

In the next cell, we will create a class called Rectangle. It will contain two attributes, corner_bl and corner_ur, which which contain the coordinates of the bottom-left and upper-right coordinates of a rectangle.

In [1]:
# Version 1

class Rectangle:  
    # Attributes
    corner_bl = [1,3]
    corner_ur = [5,6]  

Instances

The cell above does not actually create an object of the type Rectangle. It just defines the structure for later objects that we might create with this type. An object that is created with a certain class type is said to be an instance of that class. We may have several instances of the same class.

In the cell below, we will create a single instance of the Rectangle class. We will then explore some aspects of this class.

In [2]:
rect1 = Rectangle()
print(type(rect1))
<class '__main__.Rectangle'>
In [3]:
print(rect1.corner_bl)
print(rect1.corner_ur)
[1, 3]
[5, 6]

Changing Attribute Values

We can change the value of class attributes (although this is often not advisable to do so).

In [4]:
rect1.corner_ur = [3,8]

print(rect1.corner_bl)
print(rect1.corner_ur)
[1, 3]
[3, 8]

Multiple Instances

As mentioned previously, we can have multiple instances of a single class.

In [5]:
rect2 = Rectangle()

print('rect1 corners:', rect1.corner_bl, 'and', rect1.corner_ur)
print('rect2 corners:', rect2.corner_bl, 'and', rect2.corner_ur)
rect1 corners: [1, 3] and [3, 8]
rect2 corners: [1, 3] and [5, 6]

When we set a variable to be equal to an existing instance of a class, it does not create a new version of that class. Instance, the new variable simply points to the already existing instance. Recall that lists exhibit a similar behavior.

In [6]:
rect3 = rect1

print('rect1 corners:', rect1.corner_bl, 'and', rect1.corner_ur)
print('rect2 corners:', rect2.corner_bl, 'and', rect2.corner_ur)
print('rect3 corners:', rect3.corner_bl, 'and', rect3.corner_ur)
rect1 corners: [1, 3] and [3, 8]
rect2 corners: [1, 3] and [5, 6]
rect3 corners: [1, 3] and [3, 8]
In [7]:
rect1.corner_bl = [2,4]

print('rect1 corners:', rect1.corner_bl, 'and', rect1.corner_ur)
print('rect2 corners:', rect2.corner_bl, 'and', rect2.corner_ur)
print('rect3 corners:', rect3.corner_bl, 'and', rect3.corner_ur)
rect1 corners: [2, 4] and [3, 8]
rect2 corners: [1, 3] and [5, 6]
rect3 corners: [2, 4] and [3, 8]

Methods

A method is a function that is associated with a certain class. A method is always called by using a particular instance of the class, and that instance is always provided to the method as an argument.

In [8]:
# Version 2

class Rectangle:  
    # Attributes
    corner_bl = [1,3]
    corner_ur = [5,6]  
    
    # Methods
    def get_width(self):
        return self.corner_ur[0] - self.corner_bl[0]
    
    def get_height(self):
        return self.corner_ur[1] - self.corner_bl[1]
In [9]:
rect1 = Rectangle()

w = rect1.get_width()
h = rect1.get_height()

print('rect1 corners:', rect1.corner_bl, 'and', rect1.corner_ur)
print('rect1 width:  ', w)
print('rect1 height: ', h)
rect1 corners: [1, 3] and [5, 6]
rect1 width:   4
rect1 height:  3
In [10]:
rect1.corner_bl = [4,1]
rect1.corner_ur = [9,2]

w = rect1.get_width()
h = rect1.get_height()

print('rect1 corners:', rect1.corner_bl, 'and', rect1.corner_ur)
print('rect1 width:  ', w)
print('rect1 height: ', h)
rect1 corners: [4, 1] and [9, 2]
rect1 width:   5
rect1 height:  1

Methods Calling Methods

A method in a classes has access not only to the attributes within that class, but also the other methods.

In [11]:
# Version 3

class Rectangle:  
    # Attributes
    corner_bl = [1,3]
    corner_ur = [5,6]  
    
    # Methods
    def get_width(self):
        return self.corner_ur[0] - self.corner_bl[0]
    
    def get_height(self):
        return self.corner_ur[1] - self.corner_bl[1]
    
    def get_area(self):
        w = self.get_width()
        h = self.get_height()
        return h*w
In [12]:
rect1 = Rectangle()

print('rect1 corners:', rect1.corner_bl, 'and', rect1.corner_ur)
print('rect1 width:  ', rect1.get_width())
print('rect1 height: ', rect1.get_height())
print('rect1 area:   ', rect1.get_area())
rect1 corners: [1, 3] and [5, 6]
rect1 width:   4
rect1 height:  3
rect1 area:    12

Constructors

A constuctor is a special method that is called automatically when an instance of a class is created. This can allow us to set the values of class attributes when the instance is created. The constuctor of a class in Python is always named __init__().

In [13]:
# Version 4

class Rectangle:  
    # Attributes
    corner_bl = [1,3]
    corner_ur = [5,6]  
    
    # Methods
    def __init__(self, corner_bl, corner_ur):
        self.corner_bl = corner_bl
        self.corner_ur = corner_ur
    
    def get_width(self):
        return self.corner_ur[0] - self.corner_bl[0]
    
    def get_height(self):
        return self.corner_ur[1] - self.corner_bl[1]
    
    def get_area(self):
        w = self.get_width()
        h = self.get_height()
        return h*w
In [14]:
rect1 = Rectangle([1,2], [6,5])
rect2 = Rectangle([3,2], [4,9])

print('rect1 area:', rect1.get_area())
print('rect2 area:', rect2.get_area())
rect1 area: 15
rect2 area: 7

Methods Can Alter Attributes

Class methods are able to alter the value of the attributes of that class.

In [15]:
# Version 5

class Rectangle:  
    # Attributes
    corner_bl = [1,3]
    corner_ur = [5,6]  
    
    # Methods
    def __init__(self, corner_bl, corner_ur):
        self.corner_bl = corner_bl
        self.corner_ur = corner_ur
    
    def get_width(self):
        return self.corner_ur[0] - self.corner_bl[0]
    
    def get_height(self):
        return self.corner_ur[1] - self.corner_bl[1]
    
    def get_area(self):
        w = self.get_width()
        h = self.get_height()
        return h*w
    
    def grow(self, dw, dh):
        self.corner_ur[0] += dw
        self.corner_ur[1] += dh
        return self
In [16]:
rect1 = Rectangle([0,0], [3,4])
rect1.grow(6,1)

print('rect1 corners:', rect1.corner_bl, 'and', rect1.corner_ur)
rect1 corners: [0, 0] and [9, 5]

Passing Instances as Arguments

A class method is able to take instances of that class as parameters.

In [17]:
# Version 6

class Rectangle:  
    # Attributes
    corner_bl = [1,3]
    corner_ur = [5,6]  
    
    # Methods
    def __init__(self, corner_bl, corner_ur):
        self.corner_bl = corner_bl
        self.corner_ur = corner_ur
    
    def get_width(self):
        return self.corner_ur[0] - self.corner_bl[0]
    
    def get_height(self):
        return self.corner_ur[1] - self.corner_bl[1]
    
    def get_area(self):
        w = self.get_width()
        h = self.get_height()
        return h*w
    
    def grow(self, dw, dh):
        self.corner_ur[0] += dw
        self.corner_ur[1] += dh
        return self
    
    def is_smaller(self, other):
        if(self.get_area() < other.get_area()):
            return True
        return False
In [18]:
rect1 = Rectangle([0,0], [3,4])
rect2 = Rectangle([2,3], [4,6])

print(rect1.is_smaller(rect2))
False
In [19]:
rect1 = Rectangle([0,0], [3,4])
rect2 = Rectangle([2,3], [5,9])

print(rect1.is_smaller(rect2))
True

Returning New Instances

It is possible for a class method to create and return a new instance of the class itself.

In [20]:
# Version 6

class Rectangle:  
    # Attributes
    corner_bl = [1,3]
    corner_ur = [5,6]  
    
    # Methods
    def __init__(self, corner_bl, corner_ur):
        self.corner_bl = corner_bl
        self.corner_ur = corner_ur
    
    def get_width(self):
        return self.corner_ur[0] - self.corner_bl[0]
    
    def get_height(self):
        return self.corner_ur[1] - self.corner_bl[1]
    
    def get_area(self):
        w = self.get_width()
        h = self.get_height()
        return h*w
    
    def grow(self, dw, dh):
        self.corner_ur[0] += dw
        self.corner_ur[1] += dh
        return self
    
    def is_smaller(self, other):
        if(self.get_area() < other.get_area()):
            return True
        return False
    
    def intersect(self, other):
        x1 = max(self.corner_bl[0], other.corner_bl[0])
        y1 = max(self.corner_bl[1], other.corner_bl[1])
        x2 = min(self.corner_ur[0], other.corner_ur[0])
        y2 = min(self.corner_ur[1], other.corner_ur[1])
        new_rect = Rectangle([x1,y1], [x2,y2])
        return new_rect
In [21]:
rect1 = Rectangle([1,3], [6,8])
rect2 = Rectangle([4,2], [9,5])

rect3 = rect1.intersect(rect2)
print('rect3 corners:', rect3.corner_bl, 'and', rect3.corner_ur)
rect3 corners: [4, 3] and [6, 5]