No history yet

Object-Oriented Programming

A New Way of Thinking

Programming doesn't have to be just a list of instructions for the computer to follow. Object-Oriented Programming, or OOP, offers a different approach. It’s a way of structuring your code to model the real world, or at least the part of it your program deals with.

Instead of thinking about tasks and sub-tasks, you think about things and how they interact. These things are called objects. An object bundles together its own data (what it is) and its own behaviors (what it does). This simple shift in perspective is incredibly powerful for managing complex software.

The core idea of OOP is to create self-contained objects that group data and behavior together.

Classes and Objects

Before you can have an object, you need a blueprint. In Python, this blueprint is called a class. A class defines the general structure and capabilities that all objects of a certain type will share. An individual object created from that blueprint is called an instance.

class

noun

A blueprint for creating objects. It defines a set of attributes and methods that the created objects (instances) will all have.

Let's say we want to model a dog. The class would be Dog. It would define the attributes every dog has, like a name and breed, and the methods they can perform, like barking. When we create a specific dog, like a golden retriever named Fido, we are creating an instance, or object, of the Dog class.

# This is the blueprint (class)
class Dog:
    # The __init__ method is a special method
    # that runs when an object is created.
    def __init__(self, name, breed):
        # These are attributes
        self.name = name
        self.breed = breed

    # This is a method (a behavior)
    def bark(self):
        return f"{self.name} says woof!"

# Creating instances (objects) from the class
my_dog = Dog("Fido", "Golden Retriever")
another_dog = Dog("Lucy", "Poodle")

# Using the objects
print(my_dog.name)
# Output: Fido

print(another_dog.bark())
# Output: Lucy says woof!

Each object, my_dog and another_dog, is a Dog, but each has its own unique state (its own name and breed). They both share the same behavior defined in the class: they can both bark.

Building on Blueprints

Sometimes, you need a blueprint that's just a more specialized version of another. This is where inheritance comes in. Inheritance allows a new class to take on the attributes and methods of an existing class. The new class is called a child class (or subclass), and the existing class is the parent class (or superclass).

A Service Dog is still a Dog, but it has specialized training. We can create a ServiceDog class that inherits from Dog. It automatically gets the name, breed, and bark() method, and we can add new things specific to service dogs.

# Parent class
class Dog:
    def __init__(self, name, breed):
        self.name = name
        self.breed = breed

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

# Child class that inherits from Dog
class ServiceDog(Dog):
    def __init__(self, name, breed, task):
        # Call the parent's __init__ method
        super().__init__(name, breed)
        self.task = task

    # Add a new method
    def perform_task(self):
        return f"{self.name} is helping by {self.task}."

# Create an instance of the child class
buddy = ServiceDog("Buddy", "Labrador", "guiding")

# Buddy has methods from both classes
print(buddy.bark())
# Output: Buddy says woof!

print(buddy.perform_task())
# Output: Buddy is helping by guiding.

Inheritance promotes code reuse. Instead of copying and pasting code from the Dog class into a new ServiceDog class, we just inherit from it. If we update the bark() method in the Dog class, all of its child classes will automatically get the update.

One Name, Many Forms

The term polymorphism means "many forms." In programming, it refers to the ability of different objects to respond to the same message (or method call) in their own unique ways.

Imagine a group of different animals. If you tell each one to "speak," a dog will bark, a cat will meow, and a duck will quack. The command is the same, but the result is different depending on the object.

polymorphism

noun

The ability of an object to take on many forms. The most common use of polymorphism in OOP occurs when a parent class reference is used to refer to a child class object.

class Cat:
    def speak(self):
        return "Meow"

class Dog:
    def speak(self):
        return "Woof"

# A function that can work with any object
# that has a 'speak' method.
def make_it_speak(animal):
    print(animal.speak())

# Create instances
whiskers = Cat()
fido = Dog()

# Call the same function with different objects
make_it_speak(whiskers) # Output: Meow
make_it_speak(fido)     # Output: Woof

Notice that the make_it_speak function doesn't care if the object is a Cat or a Dog. It only cares that the object knows how to speak(). This makes your code more flexible and easier to extend. If you create a new Duck class with a speak() method, it will work with the make_it_speak function without any changes.

Private Lives and Public Faces

Encapsulation is the principle of bundling data and the methods that operate on that data within one unit, the object. It also involves restricting direct access to an object's state. Think of it like a capsule for medicine: the plastic casing holds the powder inside, protecting it from the outside world.

In programming, we want to prevent code outside of an object from randomly changing its internal data. This makes the program more predictable and easier to debug. We expose a public interface (the methods) that other parts of the code can use, while keeping the internal implementation details private.

Consider a BankAccount class. You wouldn't want to allow any part of your program to directly set the account balance to any value. That could lead to errors or fraud. Instead, you provide methods like deposit() and withdraw() that control how the balance can change.

class BankAccount:
    def __init__(self, owner, starting_balance):
        self.owner = owner
        # _balance is a 'protected' attribute by convention
        self._balance = starting_balance

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

    def withdraw(self, amount):
        if 0 < amount <= self._balance:
            self._balance -= amount
            print(f"Withdrew 💲{amount}")
        else:
            print("Invalid withdrawal amount.")

    def get_balance(self):
        # A 'getter' method to safely view the balance
        return self._balance

my_account = BankAccount("Alice", 100)

# Good: Use the public methods
my_account.deposit(50)
print(f"Current balance: 💲{my_account.get_balance()}")

# Bad: Directly accessing the internal state
# While possible in Python, this breaks encapsulation.
# my_account._balance = -500 

By using methods to interact with the _balance, we ensure that only valid operations can be performed. The object is in control of its own state.

Quiz Questions 1/6

What is the primary goal of Object-Oriented Programming (OOP)?

Quiz Questions 2/6

In Python, a class is a blueprint, and a specific object created from that class is called an __________.

These principles—classes, inheritance, polymorphism, and encapsulation—are the pillars of Object-Oriented Programming. Using them helps you write code that is more organized, reusable, and easier to maintain as it grows in complexity.