1
Object-Oriented Programming: From Basics to Practice

2024-10-12

Introduction to Object-Oriented Programming

Hello, dear friends! Today, we're going to talk about Object-Oriented Programming (OOP) in Python. You probably already know that Python, as a multi-paradigm programming language, supports both procedural and object-oriented programming styles. So what exactly is object-oriented programming? Why should we use it? Let's explore together.

Object-oriented programming is a programming paradigm that views a program as a collection of independent units, each of which is an object. Objects are instances of classes, and classes are abstract blueprints and definitions of objects. Simply put, a class is like a blueprint, and an object is an entity built according to this blueprint.

In object-oriented programming, we divide the programming process into the work of creating classes, rather than writing a large chunk of program code at once. Each object can contain data (called attributes) and code (called methods). You can think of an object as a small machine: it has its own attributes (the state of the machine) and behaviors (the functions of the machine).

You see, object-oriented programming is actually modeling concepts in the problem domain as classes and objects. For example, we can create a "bank account" class that has attributes like account number and balance, and methods like deposit and withdrawal. Then, we can create multiple "bank account" objects based on this class, each with its own unique attribute values.

Classes and Objects

Alright, after discussing so much theory, let's get hands-on and see how to define classes and create objects in Python.

class BankAccount:
    def __init__(self, account_number, balance=0):
        self.account_number = account_number
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited {amount}, current balance is {self.balance}")

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient balance, cannot withdraw")
        else:
            self.balance -= amount
            print(f"Withdrew {amount}, current balance is {self.balance}")


account1 = BankAccount("6217001912345678", 1000)
account2 = BankAccount("6217009876543210")


account1.deposit(500)
account2.withdraw(200)

See, we first defined a BankAccount class with two attributes: account_number and balance. The __init__ method is a special method used to initialize the object's attributes when creating a new object.

Then, we defined two methods, deposit and withdraw, for deposit and withdrawal operations. Notice the self parameter? It refers to the current instance of the object, allowing us to access the object's attributes and methods.

Next, we created two BankAccount objects and performed deposit and withdrawal operations on them. See how simple it is! Object-oriented programming allows us to naturally map concepts from the real world into code.

Inheritance and Polymorphism

Inheritance and polymorphism are two important concepts in object-oriented programming. Inheritance allows us to define a parent class, and child classes automatically inherit the attributes and methods of the parent class. This way, we can reuse code and avoid rewriting similar code.

Let's look at an example:

class BankAccount:
    def __init__(self, account_number, balance=0):
        self.account_number = account_number
        self.balance = balance

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient balance, cannot withdraw")
        else:
            self.balance -= amount

class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance=0, interest_rate=0.01):
        super().__init__(account_number, balance)
        self.interest_rate = interest_rate

    def add_interest(self):
        interest = self.balance * self.interest_rate
        self.deposit(interest)
        print(f"Calculated interest {interest}, deposited into account")

savings = SavingsAccount("6217007654321098", 5000, 0.05)
savings.deposit(1000)
savings.add_interest()

In this example, we defined a SavingsAccount class that inherits from the BankAccount class. The SavingsAccount class adds a new attribute interest_rate and a new method add_interest.

In the constructor of the SavingsAccount class, we use super().__init__() to call the constructor of the parent class, ensuring that the parent class's attributes are also properly initialized. Then, we can use the savings account just like a regular bank account, and we can also calculate interest.

Polymorphism allows us to override inherited methods in different child classes to achieve different behaviors. For example, we could override the withdraw method for the SavingsAccount class to charge a fee when withdrawing.

Through inheritance and polymorphism, we can write more flexible and extensible code while avoiding rewriting similar code.

Encapsulation and Abstraction

Encapsulation and abstraction are two other important concepts in object-oriented programming. Encapsulation means bundling data and behavior into a single unit (object) and controlling access to them through a strict interface. This ensures that the internal implementation details of the object are hidden from external code, thereby improving the maintainability and security of the code.

In Python, we can use underscore prefixes to define private attributes and methods, although they can still be accessed (this is a design decision in Python). For example:

class BankAccount:
    def __init__(self, account_number, balance=0):
        self._account_number = account_number
        self._balance = balance

    def deposit(self, amount):
        self._balance += amount

    def withdraw(self, amount):
        if amount > self._balance:
            print("Insufficient balance, cannot withdraw")
        else:
            self._balance -= amount

account = BankAccount("6217001234567890", 1000)
print(account._balance)  # Can be accessed, but not recommended

In the above example, we used the prefix underscore to define the _account_number and _balance attributes. Although we can access them directly, this is considered bad practice because it violates the principle of encapsulation. Instead, we should always access and modify the internal state of an object through defined methods.

Abstraction is hiding concrete implementation details behind a simple interface. In Python, we can use Abstract Base Classes (ABC) to define interfaces and force subclasses to implement specific methods. This ensures that all subclasses follow the same contract, improving code consistency and maintainability.

from abc import ABC, abstractmethod

class BankAccount(ABC):
    @abstractmethod
    def deposit(self, amount):
        pass

    @abstractmethod
    def withdraw(self, amount):
        pass

class SavingsAccount(BankAccount):
    def __init__(self, account_number, balance=0, interest_rate=0.01):
        self.account_number = account_number
        self.balance = balance
        self.interest_rate = interest_rate

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        if amount > self.balance:
            print("Insufficient balance, cannot withdraw")
        else:
            self.balance -= amount

In this example, we defined an abstract base class BankAccount that contains two abstract methods deposit and withdraw. Any subclass inheriting from BankAccount must implement these two methods, otherwise it cannot be instantiated.

Through encapsulation and abstraction, we can write more robust, maintainable, and extensible object-oriented code.

Design Patterns

In object-oriented programming, design patterns are proven solutions to specific problems. They are a way of writing better code that can improve the readability, maintainability, and extensibility of code.

Let's look at a simple example of the Singleton pattern, which ensures that a class has only one instance.

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class GameManager(metaclass=Singleton):
    def __init__(self):
        self.score = 0

    def add_score(self, points):
        self.score += points

game1 = GameManager()
game2 = GameManager()

print(game1 is game2)  # True
game1.add_score(100)
print(game2.score)  # 100

In this example, we defined a metaclass Singleton that checks if an instance of the class already exists when creating a new instance. If it does, it returns the existing instance; otherwise, it creates a new instance.

Then, we defined a GameManager class that uses Singleton as its metaclass. No matter how many GameManager instances we create, they will all point to the same object. This way, we can ensure that there is only one GameManager instance throughout the program, making it easy to manage the game state.

Design patterns provide us with general methods for solving common problems, which can improve the quality and maintainability of code. Besides the Singleton pattern, there are many other popular design patterns such as the Factory pattern, Observer pattern, Decorator pattern, and so on. Learning and using design patterns can make you a better object-oriented programmer.

Practice and Summary

Alright, we've discussed many important concepts of object-oriented programming. Now, let's tie all the knowledge points together through a practical example.

Suppose we're developing a simple game where there are different types of characters, such as warriors, mages, and archers. Each character has its own attributes (such as health points, attack power, etc.) and skills. We need to design an object-oriented system to represent these characters and their behaviors.

from abc import ABC, abstractmethod

class Character(ABC):
    def __init__(self, name, hp, attack):
        self.name = name
        self.hp = hp
        self.attack = attack

    @abstractmethod
    def attack_skill(self, target):
        pass

class Warrior(Character):
    def attack_skill(self, target):
        damage = self.attack * 1.2
        target.hp -= damage
        print(f"{self.name} used a slashing attack, dealing {damage} damage to {target.name}")

class Mage(Character):
    def attack_skill(self, target):
        damage = self.attack * 0.8
        target.hp -= damage
        print(f"{self.name} used fireball, dealing {damage} damage to {target.name}")

class Archer(Character):
    def attack_skill(self, target):
        damage = self.attack * 1.1
        target.hp -= damage
        print(f"{self.name} used precise shot, dealing {damage} damage to {target.name}")


warrior = Warrior("Warrior", 500, 80)
mage = Mage("Mage", 300, 100)
archer = Archer("Archer", 400, 90)


warrior.attack_skill(mage)
mage.attack_skill(archer)
archer.attack_skill(warrior)

In this example, we defined an abstract base class Character that contains attributes and an abstract method attack_skill common to all characters. Then, we defined a concrete subclass for each character type, implementing the attack_skill method in each.

Through inheritance and polymorphism, we can easily create different types of character objects and have them perform different attack skills. At the same time, we also utilized the concept of encapsulation, encapsulating the character's attributes and behaviors within the object and only exposing necessary interfaces.

Finally, we created three different character objects and had them attack each other. You see, object-oriented programming allows us to model the real world in a natural and intuitive way, and write more flexible, extensible, and maintainable code.

In conclusion, object-oriented programming provides us with a whole new way of thinking, allowing us to better organize and manage code. By mastering concepts such as classes, objects, inheritance, polymorphism, encapsulation, and abstraction, as well as the application of design patterns, you can become a true expert in object-oriented programming! Let's work hard together and keep progressing on our programming journey!