Object-Oriented Programming in Python

Gregory M. Kapfhammer

January 29, 2024

What are the main goals of object-oriented programming?

  • Write code that reflects thought and the world
  • Enable code reuse and sharing among programmers
  • Encourage the creation of well-designed software
  • Ensure that software is easy to maintain and extend

Type Inference in Python

mylist = []
print(type(mylist))
print(isinstance(mylist, list))
print(isinstance(mylist, str))
<class 'list'>
True
False
  • Key insights about this source code:
    • mylist = [] creates an empty list
    • type(mylist) returns the type of the list
    • isinstance returns True or False for provided type
    • isinstance(mylist, list) checks if mylist is a list
    • Same approach works for other data types!

Existing types available in Python

def example_function():
  return 0
print(type(example_function))
<class 'function'>

def generator_example(n):
  for i in range(n):
    yield i
print(type(generator_example))
print(type(generator_example(5)))
<class 'function'>
<class 'generator'>
  • type shows that functions and generators have their own types
  • What are the similarities and differences for these two types?
  • How do return and yield influence a function’s behavior?

What if we don’t create objects?

u = (3,4)
v = (3,6)
def add(a, b):
  return (a[0] + b[0], a[1] + b[1])
def subtract(a,b):
  return (a[0] - b[0], a[1] - b[1])
def dot(a, b):
  return (a[0] * b[0] + a[1] * b[1])
def norm(a):
  return (a[0] * a[0] + a[1] * a[1]) ** 0.5
def isvertical(a):
  return a[0] == 0
print(norm(u))
print(add(u,v))
print(u + v)
print(isvertical(subtract(v, u)))
5.0
(6, 10)
(3, 4, 3, 6)
True

An object-oriented alternative

class Vector:
  def __init__(self, x, y):
    try:
      self.x = float(x)
      self.y = float(y)
    except ValueError:
      self.x = 0.0
      self.y = 0.0
  def norm(self):
    return (self.x ** 2 + self.y ** 2) ** 0.5
  def __add__(self, other):
    newx = self.x + other.x
    newy = self.y + other.y
    return Vector(newx, newy)
  def __str__(self):
    return "(%f, %f)" %(self.x, self.y)

u = Vector(3,4)
v = Vector(3,6)
print(u + v)
(6.000000, 10.000000)

What are the principles of object-oriented programming?

  • Abstraction
  • Inheritance
  • Encapsulation
  • Polymorphism

Encapsulation with Triangle

class Triangle:
    def __init__(self, points):
        self._sides = 3
        self._points = list(points)
        if len(self._points) != 3:
            raise ValueError("Wrong number of points.")

    def sides(self):
        return 3

    def __str__(self):
        return "I’m a triangle."
  • What state does a Triangle object have?
  • How can we access the state of a Triangle object?
  • What are the benefits of encapsulation?

Encapsulation with Square

class Square:
    def __init__(self, points):
        self._sides = 4
        self._points = list(points)
        if len(self._points) != 4:
            raise ValueError("Wrong number of points.")

    def sides(self):
        return 4

    def __str__(self):
        return "I’m so square."
  • What state does a Square object have?
  • How can we access the state of a Square object?
  • Again, what are the benefits of encapsulation?

Wait, what is the relationship between a Square and a Triangle?

class Polygon:
    def __init__(self, sides, points):
        self._sides = sides
        self._points = list(points)
        if len(self._points) != self._sides:
            raise ValueError("Wrong number of points.")

    def sides(self):
        return self._sides
  • The Polygon class is a superclass of Triangle and Square
  • The Triangle and Square classes are subclasses of Polygon
  • The Polygon class is a generalization of Triangle and Square

Connecting Triangle and Square to the Polygon Superclass

class Triangle(Polygon):
    def __init__(self, points):
        Polygon.__init__(self, 3, points)

    def __str__(self):
        return "I’m a triangle."

class Square(Polygon):
    def __init__(self, points):
        Polygon.__init__(self, 4, points)

    def __str__(self):
        return "I’m so square."
  • Forms an “is-a” relationship between:
    • Triangle and Polygon and also Square and Polygon

What is duck typing? How does it work in Python?

  • Python is a dynamically typed language

  • Python uses duck typing to determine types

  • Remember the silly adage of “If it walks like a duck and quacks like a duck, then it must be a duck”

  • Inheritance is not only way to create an “is-a” relationship!

  • Yet, inheritance makes the “is-a” relationship explicit

  • Let’s explore an example of duck typing in Python!

Connecting Triangle and Square to the Polygon Superclass

class PolygonCollection:
    def __init__(self):
        self._triangles = []
        self._squares = []

    def add(self, polygon):
        if polygon.sides() == 3:
            self._triangles.append(polygon)
        if polygon.sides() == 4:
            self._squares.append(polygon)
  • The polygon parameter can be any object with a sides method
  • This is an example of parametric polymorphism in Python
  • Again, inheritance is not the only way to create an “is-a” relationship!

Using composition in Python

class MyLimitedList:
    def __init__(self):
        self._L = []

    def append(self, item):
        self._L.append(item)

    def __getitem__(self, index):
        return self._L[index]

limited = MyLimitedList()
limited.append(1)
limited.append(10)
limited.append(100)
print(limited[2])
100
  • Composition means that one object is part of another object
  • The MyLimitedList class is composed of a list called _L

What is the expected performance overhead of a function call?

  • Invocation of a function in Python has overhead
  • Function call overhead in Python is reputed to be high
  • Use the Python scripts in jni/performance-tests

Time overhead of function calls

import timeit, f
n = int(1e6)
num_exec = 1
for func in filter(lambda f: f.startswith('loop'), sorted(dir(f))):
    print('====== Timing function called:', func)
    print(getattr(f, func).__doc__)
    t = timeit.timeit(stmt='f.%s(n)' % func,
                      number=num_exec, globals=globals())
    per_loop = round(t / n * 1e9)
    print('Time: %ins, Theoretical max bandwidth: %.2fMB/s' %
          (per_loop, 1000 / per_loop))
====== Timing function called: loop_0_empty
Run empty loop n times.
Time: 13ns, Theoretical max bandwidth: 76.92MB/s
====== Timing function called: loop_1_f
Run loop with empty function n times.
Time: 49ns, Theoretical max bandwidth: 20.41MB/s
====== Timing function called: loop_2_f_twice
Run loop calling empty function twice per loop, n times.
Time: 84ns, Theoretical max bandwidth: 11.90MB/s

More timings for function calls

import timeit, g
n = int(1e6)
num_exec = 1
for func in filter(lambda g: g.startswith('loop'), sorted(dir(g))):
    print('====== Timing function called:', func)
    print(getattr(g, func).__doc__)
    t = timeit.timeit(stmt='g.%s(n)' % func,
                      number=num_exec, globals=globals())
    per_loop = round(t / n * 1e9)
    print('Time: %ins, Theoretical max bandwidth: %.2fMB/s' %
          (per_loop, 1000 / per_loop))
====== Timing function called: loop_3_g
Run loop with empty function taking args, n times.
Time: 64ns, Theoretical max bandwidth: 15.62MB/s
====== Timing function called: loop_5_g_arg
Run loop with empty function passing an arg, n times.
Time: 77ns, Theoretical max bandwidth: 12.99MB/s
====== Timing function called: loop_6_g_kwarg
Run loop with empty function passing a kwarg, n times.
Time: 104ns, Theoretical max bandwidth: 9.62MB/s

Let’s run this experiment on your laptop! What are the results?

  • Clone this Github repository: jni/performance-tests
  • Change into the directory called function-calls
  • Run the program called timer.py and observe the results
  • Make sure that you review the Python code in f.py and g.py
  • What trends did you observe in the performance results?
  • What are the implications of these experimental results?
  • Share (a) your results and (b) a one-sentence summary!

What are the performance trade-offs associated with using objects?

  • Questions to consider when designing an experiment:
    • Cost of calling a method or a function?
    • Cost of accessing an object’s attribute?
    • How quickly can your program process data?
    • What is the space overhead of using objects?
  • Questions to consider when building a program:
    • What data structures are best for your application?
    • How do data structures influence algorithm performance?