No history yet

Object-Oriented Programming

Thinking in Objects

Object-Oriented Programming, or OOP, is a way of structuring programs by bundling related data and the functions that operate on that data into single units called objects. Think of it like building with LEGOs. Instead of a pile of loose bricks, you create pre-built components (a wheel assembly, a cockpit) that you can easily reuse and combine.

In Python, the blueprint for an object is called a class. A class defines the properties (data) and behaviors (functions, called methods) that all objects of that type will have. An object is a specific instance created from that class blueprint. You can have one Car class, but many individual car objects, each with its own color and mileage.

class Dog:
    # The __init__ method is the constructor.
    # It runs when a new object is created.
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

    # A method is a function inside a class.
    def bark(self):
        return f"{self.name} says woof!"

# Create two Dog objects (instances of the Dog class)
my_dog = Dog("Fido", "Golden Retriever")
neighbors_dog = Dog("Rex", "German Shepherd")

print(my_dog.name)
print(neighbors_dog.bark())

A class is the blueprint. An object is the actual house built from that blueprint.

The Core Principles

OOP is built on a few key ideas that help manage complexity in large programs. Understanding them is key to writing clean, effective object-oriented code.

Encapsulation

noun

The bundling of data (attributes) and the methods that operate on that data into a single unit, or class. It also involves restricting direct access to an object's components, which is known as data hiding.

Encapsulation helps protect your data from accidental modification. In Python, we can't create truly private attributes, but we use a convention: prefix an attribute name with an underscore (e.g., _voltage) to signal that it's intended for internal use and shouldn't be changed from outside the class.

class Account:
    def __init__(self, owner, starting_balance):
        self.owner = owner
        self._balance = starting_balance # Convention for a "private" attribute

    def deposit(self, amount):
        if amount > 0:
            self._balance += amount
            print(f"Added {amount}. New balance: {self._balance}")
        else:
            print("Deposit amount must be positive.")

    def get_balance(self):
        return self._balance

my_account = Account("Alice", 100)
# We can't directly set the balance to a negative number.
# We must use the deposit method.
my_account.deposit(50)
print(f"Current balance: {my_account.get_balance()}")

Next up is inheritance. This allows a new class to take on the properties and methods of an existing class. It creates an "is-a" relationship. For instance, an ElectricCar is a type of Car. The new class, called a subclass or child class, can add its own new methods or override existing ones to behave differently.

Lesson image
# Parent class
class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        raise NotImplementedError("Subclass must implement this method")

# Child class inheriting from Animal
class Cat(Animal):
    def speak(self):
        return f"{self.name} says meow"

# Another child class
class Dog(Animal):
    def speak(self):
        return f"{self.name} says woof"

whiskers = Cat("Whiskers")
fido = Dog("Fido")

print(whiskers.speak())
print(fido.speak())

The code above also demonstrates polymorphism, which means "many forms." Notice that both the Cat and Dog objects have a speak method, and we can call it in the same way for both. However, the action that occurs is different for each object. We can write code that works with any Animal without needing to know if it's a Cat or a Dog.

Polymorphism allows us to use a single interface to represent different underlying forms (data types).

Making Classes Pythonic

Python's classes have special methods that you can implement to make your objects behave like built-in types. These methods are always surrounded by double underscores, like __init__. They're often called "dunder" methods (for Double UNDERscore).

For example, the __str__ method provides a user-friendly string representation of an object. If you print() an object that has a __str__ method, Python will call it automatically.

class Book:
    def __init__(self, title, author):
        self.title = title
        self.author = author

    # Called by str() and print()
    def __str__(self):
        return f"'{self.title}' by {self.author}"

    # Called for a developer-friendly representation
    def __repr__(self):
        return f"Book(title='{self.title}', author='{self.author}')"

my_book = Book("Dune", "Frank Herbert")

print(my_book) # Uses __str__

We can also overload operators. This means we can define how standard operators like +, -, or * should work with our objects. For example, we could define the __add__ method for a Vector class to perform vector addition.

(x1y1)+(x2y2)=(x1+x2y1+y2)\begin{pmatrix} x_1 \\ y_1 \end{pmatrix} + \begin{pmatrix} x_2 \\ y_2 \end{pmatrix} = \begin{pmatrix} x_1 + x_2 \\ y_1 + y_2 \end{pmatrix}
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __repr__(self):
        return f"Vector({self.x}, {self.y})"

    # This method is called when we use the '+' operator
    def __add__(self, other):
        new_x = self.x + other.x
        new_y = self.y + other.y
        return Vector(new_x, new_y)

v1 = Vector(2, 4)
v2 = Vector(3, 1)

v3 = v1 + v2 # Python calls v1.__add__(v2) behind the scenes

print(v3)

By leveraging these core principles and special methods, you can create code that is intuitive, reusable, and much easier to manage as projects grow in complexity.

Quiz Questions 1/6

In Object-Oriented Programming, what is the primary relationship between a class and an object?

Quiz Questions 2/6

If a Poodle class is created from a more general Dog class, inheriting its traits, which core OOP principle does this represent?