Skip to content

Week 13 - OOP & Classes & Methods

Introduction to Python Classes and Methods


Objectives

By the end of this lesson, students will:

  1. Understand how to define and use methods within classes.
  2. Differentiate between instance methods, static methods, and special methods.
  3. Learn how to overload operators in Python.
  4. Recognize the importance of invariants and how to use assertions for debugging.

Introduction to Object-Oriented Programming

  • Object Oriented Programming is a programming paradigm where objects, which are instances of classes (i.e. blueprints), model real-world or domain specific things or entities.
    • Generally we use nouns to define classes and therefore objects.
    • Verbs are the class methods or actions that alter or affect the objects of a particular class.
    • Adjectives are the attributes or instance variables that describe the data or state of an object of a particular class.
  • Core Concepts of OOP, only 2 necessary to know for this chapter:
    • Encapsulation: The concept of bundling the state/data (attributes) and methods (functions) that operate on the data into a single unit, known as a class. Encapsulation also involves restricting direct access to some of an object’s components to maintain integrity and protect the data, but Python only supports access modifiers by convention.
    • Abstraction: The concept of hiding the complex implementation details (in attributes and methods) and only exposing the necessary parts of an object via public methods. This simplifies interactions with objects and applications by providing a clear and simplified interfaces to modify instances.
    • Inheritance (Another week’s topic): The concept of creating new classes based on existing classes, allowing them to inherit attributes and methods from the parent class. It promotes code reuse and establishes a relationship between classes.
    • Polymorphism (Another week’s topic): The concept of using a single interface to represent different data types or classes. It allows objects to be treated as instances of their parent class rather than their actual class, enabling method overloading and overriding.
      • Python doesn’t really have the concept of an interface like C# or Java, however. In Python, Duck Typing is used instead to describe objects that look like each-other.

Code Example: Simple Class with Attributes

1
2
3
4
5
6
7
8
9
class Dog:

    def __init__(self, name, age):  # This is a special method that assigns values to attributes, kinda like a C# or Java constructor.
        self.name = name
        self.age = age

my_dog = Dog("Buddy", 5)  # Code outside the class to create an instance of class Dog (i.e. my_dog points at an object)
print(my_dog.name)  # Output: Buddy
print(my_dog.age)   # Output: 5


Defining Methods

  • Methods are similar to functions, however they are defined inside our classes and are named based on actions our class objects will perform or use to altr object state.
    • The first parameter of a method is always self which is a reference to the already created object instance of the class the method is in.
    • self can be used to read or update the attributes of the object it references.
  • Regular functions, on the other hand, encapsulate algorithms and processing but don’t need to occur inside a class, don’t get an object as the first parameter, and are attached to a global namespace with no direct access to object attributes.

Code Example: Method Inside a Class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def bark(d1):
    d1.bark()

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        print(f"{self.name} says woof!")

my_dog = Dog("Buddy", 5)
my_dog.bark()  # Calling the method with an object, my_dog get's passed as self
bark(my_dog)   # Calling a function

Function VS Method Summary, a Quick Compare and Contrast

  • Methods:

    • Defined within a class and associated with an object or a class.
    • Is called on instances of that class with dot notation, and the instance is passed as self.
    • There are 3 types of methods: Instance, Static, and Class methods (More on this later)
    • Can access and modify the attributes of the class or instance it belongs to.
    • Used for tasks that involve manipulating the state of an object or performing actions related to the class.
  • Functions:

    • A block of reusable code defined using the def keyword and associated with the global, or a module’s, namespace.
    • Is called independently by name or dot notation when in a module import.
    • A functions is simply a function object.
    • Cannot directly access or modify the attributes of a class or instance.
    • Used for general-purpose tasks that do not require access to class or instance-specific data.

Static Methods

  • Properties of static methods
    • No Instance/Object Required: Static methods do not require an instance of the class to be called.
    • Class-Level Operations: They are typically used for operations that are not dependent on instance-specific data, like utility methods or calculations performed in isolation of instances, but still relevant to the class..
    • Memory Efficiency: They are loaded into memory only once, making them more efficient for certain types of operations.
    • Limited Access: They cannot access instance/object variables or instance/object methods directly.
    • We define by putting these methods in the class and decorating them with @staticmethod.
    • We call these methods with the Class name, not an object instance.

Code Example: Static Method

1
2
3
4
5
6
7
class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

result = MathUtils.add(5, 10)
print(result)


Special Methods

  • Special methods have double underscores at the beginning and end of their names.
  • These methods allow you to define the behavior of objects for built-in operations like comparison or operator overloading.
  • Below I introduce the __str__ method which can be used to convert an object to a human friendly string when printing.
  • Below I also show overloading the + operator using the __add__ method.

Code Example: __str__ and __add__ Methods

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class Time:
    def __init__(self, hour, minute):
        self.hour = hour
        self.minute = minute

    def __str__(self):
        return f"{self.hour:02d}:{self.minute:02d}"

    def __add__(self, other):
        total_minutes = (self.hour + other.hour) * 60 + (self.minute + other.minute)
        hour, minute = divmod(total_minutes, 60)
        return Time(hour, minute)

t1 = Time(2, 30)
t2 = Time(1, 45)
t3 = t1 + t2
print(t3)


Using Assertions for Debugging

  • Invariants are conditions that must always hold true for an object throughout its lifetime.
    • They are essential for ensuring the consistency and correctness of an object’s state.
    • Invariants can be classified into several types:
      • Class Invariants: Conditions that must be true for all instances of a class. For example, in a class representing a rectangle, the width and height must always be non-negative.
      • Loop Invariants: Conditions that must be true at the start and end of each iteration of a loop. These are crucial for proving the correctness of algorithms.
      • Method Invariants: Conditions that must be true before and after the execution of a method. These ensure that the method does not leave the object in an inconsistent state.
    • In Python assertions are often used to enforce these variant conditions.
    • Failing assert statements will raise and AssertionError

Code Example: Using Assertions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Time:
    def __init__(self, hour, minute):
        self.hour = hour
        self.minute = minute

    def is_valid(self):
        return 0 <= self.minute < 60 and isinstance(self.hour, int)

    def add_time(self, other):
        assert self.is_valid(), "Invalid Time object"
        assert other.is_valid(), "Invalid Time object"
        total_minutes = (self.hour + other.hour) * 60 + (self.minute + other.minute)
        hour, minute = divmod(total_minutes, 60)
        return Time(hour, minute)

t1 = Time(2, 30)
t2 = Time(1, 45)
t3 = t1.add_time(t2)
print(t3)  # Output: 04:15

  • I think it’s important to note that assert statements are typically used for debugging purposes and can be disabled in production environments by running Python with the -O (optimize) flag. For critical checks that should always be enforced, it’s better to use explicit conditionals and raise exceptions.

Practice

  • Exercise 1: Write a class Rectangle that includes methods to calculate area and perimeter. Implement __str__ for string representation.
    • You should include an init method that receives a width and height.
  • Exercise 2: Create a BankAccount class with methods for depositing and withdrawing money. Add assert validation to ensure the balance doesn’t go negative.