14. Classes and Objects

Object-oriented programming

Python is an object-oriented programming language. That means it provides features that support object-oriented programming (OOP).

Up to now, some of the programs we have been writing use a procedural programming paradigm. In procedural programming the focus is on writing functions or procedures which operate on data.

In object-oriented programming the focus is on the creation of objects which contain both data and functionality together.

Object definition

Usually, each object definition corresponds to some object or concept in the real world, and the functions that operate on that object correspond to the ways real-world objects interact.

User-defined Classes

In [1]:
class Point:
    """ Point class represents and manipulates x,y coords. """

    def __init__(self):
        """ Create a new point at the origin """
        self.x = 0
        self.y = 0

Every class should have a method with the special name __init__. This initializer method is automatically called whenever a new instance is created.

It gives the programmer the opportunity to set up the attributes required within the new instance by giving them their initial state/values.

The self parameter is automatically set to reference the newly created object that needs to be initialized.

In [2]:
p = Point()         # Instantiate an object of type Point
q = Point()         # and make a second point

print(p)
print(q)

print(p is q, p == q)
<__main__.Point object at 0x7fb5b07d9c50>
<__main__.Point object at 0x7fb5b07d9c18>
False False
In [3]:
print(p.x, q.y)

p.x, p.y = 5, 6

print(p.x, p.y)
0 0
5 6

A function like Point() that creates a new object instance is called a constructor.

Every class automatically uses the name of the class as the name of the constructor function.

The definition of the constructor function is done when you write the __init__ function.

It may be helpful to think of a class as a factory for making objects.

The class itself isn’t an instance of a point, but it contains the machinery to make point instances.

Every time we call the constructor, we’re asking the factory to make us a new object. As the object comes off the production line, its initialization method is executed to get the object properly set up with its factory default settings.

The combined process of “make me a new object” and “get its settings initialized to the factory default settings” is called instantiation.

Improving our initializer

In [4]:
class Point:
    """ Point class represents and manipulates x,y coords. """

    def __init__(self, x=0, y=0):
        """ Create a new point at x, y """
        self.x = x
        self.y = y
In [5]:
p = Point(4, 2)
q = Point(6, 3)
r = Point()       # r represents the origin (0, 0)
print(p.x, q.y, r.x)
4 3 0

Adding Other Methods

A method behaves like a function but it is invoked on a specific instance, e.g. tess.right(90). Like a data attribute, methods are accessed using dot notation.

In [6]:
class Point:
    """ Create a new Point, at coordinates x, y """

    def __init__(self, x=0, y=0):
        """ Create a new point at x, y """
        self.x = x
        self.y = y

    def distance_from_origin(self):
        """ Compute my distance from the origin """
        return (self.x ** 2 + self.y ** 2) ** 0.5
In [7]:
p = Point(3, 4)
print(p.x, p.y)

print(p.distance_from_origin())

q = Point(5, 12)
print(q.x, q.y)
print(q.distance_from_origin())
3 4
5.0
5 12
13.0

Instances as arguments and parameters

In [8]:
def print_point(pt):
    print("({0}, {1})".format(pt.x, pt.y))
    
print_point(p)
(3, 4)
In [9]:
import math

def distance(point1, point2):
    xdiff = point2.x - point1.x
    ydiff = point2.y - point1.y

    dist = math.sqrt(xdiff**2 + ydiff**2)
    return dist

p = Point(4, 3)
q = Point(0, 0)
print(distance(p, q))
5.0

Converting an Object to a String

In [10]:
print(p)
str(p)
<__main__.Point object at 0x7fb5b08056a0>
Out[10]:
'<__main__.Point object at 0x7fb5b08056a0>'
In [11]:
class Point:
    """ Point class represents and manipulates x,y coords. """

    def __init__(self, x=0, y=0):
        """ Create a new point at x, y """
        self.x = x
        self.y = y
        
    def __str__(self):
        return "({},{})".format(self.x, self.y)
In [12]:
p = Point(1,2)

print(p)

print(str(p))
(1,2)
(1,2)

Instances as return values

In [13]:
class Point:   
    
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y
        
    def __str__(self):
        return "({},{})".format(self.x, self.y)
    
    def halfway(self, target):
        """ Return the halfway point between myself and the target """
        mx = (self.x + target.x)/2
        my = (self.y + target.y)/2
        return Point(mx, my)
In [14]:
p = Point(3, 4)
q = Point(5, 12)
r = p.halfway(q)
print(r)
(4.0,8.0)

A change of perspective

The original syntax for a function call, print_time(current_time), suggests that the function is the active agent.

"Hey, print_time! Here’s an object for you to print."

In object-oriented programming, the objects are considered the active agents.

current_time.print_time() says "Hey current_time! Please print yourself!"

Fractions

In [16]:
class Fraction:

    def __init__(self, top, bottom):
        self.num = top        # the numerator is on top
        self.den = bottom     # the denominator is on the bottom

    def __str__(self):
        return str(self.num) + "/" + str(self.den)

    def getNum(self):
        return self.num

    def getDen(self):
        return self.den
In [17]:
myfraction = Fraction(3, 4)
print(myfraction)

print(myfraction.getNum())
print(myfraction.getDen())
3/4
3
4

Objects are mutable

In [18]:
myfraction = Fraction(3, 4)

myfraction.num +=2

print(myfraction)
5/4

get lowest terms

12/16 == 3/4

In [19]:
def gcd(m, n):
    while m % n != 0:
        m, n = n, m % n
    return n

print(gcd(12, 16))
4
In [20]:
class Fraction:

    def __init__(self, top, bottom):

        self.num = top        # the numerator is on top
        self.den = bottom     # the denominator is on the bottom

    def __str__(self):
        return str(self.num) + "/" + str(self.den)

    def simplify(self):
        common = gcd(self.num, self.den)
        self.num = self.num // common
        self.den = self.den // common
In [21]:
myfraction = Fraction(12, 16)

print(myfraction)
myfraction.simplify()
print(myfraction)
12/16
3/4

Sameness

In [22]:
f1 = Fraction(12, 16)
f2 = Fraction(12, 16)
f3 = Fraction(3, 4)

print(f1 == f2)
print(f1 == f3)
False
False
In [23]:
class Fraction:

    def __init__(self, top, bottom):
        self.num = top        # the numerator is on top
        self.den = bottom     # the denominator is on the bottom

    def __str__(self):
        return str(self.num) + "/" + str(self.den)
    
    def __eq__(self, target):
        self.simplify()
        target.simplify()
        return self.num == target.num and self.den == target.den

    def simplify(self):
        common = gcd(self.num, self.den)

        self.num = self.num // common
        self.den = self.den // common
In [24]:
f1 = Fraction(12, 16)
f2 = Fraction(12, 16)
f3 = Fraction(3, 4)

print(f1 == f2)
print(f1 == f3)
True
True

Arithmetic Methods

def __add__(self,otherfraction):

    newnum = self.num*otherfraction.den + self.den*otherfraction.num
    newden = self.den * otherfraction.den

    common = gcd(newnum,newden)

    return Fraction(newnum//common,newden//common)
In [25]:
def gcd(m, n):
    while m % n != 0:
        m, n = n, m % n

    return n

class Fraction:

    def __init__(self, top, bottom):

        self.num = top        # the numerator is on top
        self.den = bottom     # the denominator is on the bottom

    def __str__(self):
        return str(self.num) + "/" + str(self.den)
    
    def __eq__(self, target):
        self.simplify()
        target.simplify()
        
        return self.num == target.num and self.den == target.den


    def simplify(self):
        common = gcd(self.num, self.den)

        self.num = self.num // common
        self.den = self.den // common

    def __add__(self,otherfraction):

        newnum = self.num*otherfraction.den + self.den*otherfraction.num
        newden = self.den * otherfraction.den

        common = gcd(newnum, newden)

        return Fraction(newnum // common, newden // common)
In [26]:
f1 = Fraction(1, 2)
f2 = Fraction(1, 4)

f3 = f1 + f2    # calls the __add__ method of f1
print(f3)
3/4

Make object iterable

In [27]:
class Fraction:

    def __init__(self, top, bottom):

        self.num = top        # the numerator is on top
        self.den = bottom     # the denominator is on the bottom

    def __iter__(self):
        return (i for i in (self.num, self.den))
    
In [30]:
f1 = Fraction(1,3)
num, den = f1
print(num,den)
1 3

Make object hashable

In [33]:
class Fraction:

    def __init__(self, top, bottom):
        self.num = top        # the numerator is on top
        self.den = bottom     # the denominator is on the bottom
        

    def __hash__(self):
        return hash(self.num) ^ hash(self.den)
    
print(len(set([Fraction(1,2),Fraction(3,4)])))
2

Copying

In [34]:
import copy

f1 = Fraction(3, 4)
f2 = copy.copy(f1) # shallow copy

print(f1 is f2)
print(f1 == f2)
False
False

To copy a simple object like a Point, which doesn’t contain any embedded objects, copy is sufficient. This is called shallow copying.

In [35]:
import copy

f1 = Fraction(3, 4)
f2 = copy.deepcopy(f1) # deep copy

print(f1 is f2)
print(f1 == f2)
False
False

deepcopy copies not only the object but also any embedded objects.

c struct like

Sometimes it is useful to have a data type similar to the Pascal “record” or C “struct”, bundling together a few named data items.

In [36]:
class Employee:
    pass

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000
print(john.name)
John Doe

Advanced topics

  • Inheritance
  • Override
  • Descriptor
In [ ]: