top of page

25% Discount For All Pricing Plans "welcome"

A Practical Dive into Object-Oriented Programming (OOP) with Python



1. Understanding Class and Instance Data


Object-Oriented Programming (OOP) is a programming paradigm that provides a means of structuring programs so that properties and behaviors are bundled into individual objects. In Python, everything is an object. So, let's get started by discussing the two key types of data in OOP: class and instance data.


1.1 Instance-level data


Imagine a blueprint for a house. The blueprint itself isn't a house, but it contains all the information necessary to build one. In OOP, a class is like a blueprint, while an instance is the house built from that blueprint.

Instance-level data, also known as instance variables or attributes, belong to specific instances of a class. Each instance of a class can have different values for these attributes. In Python, we use self to refer to instance variables.

Here's a simple example:

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

dog1 = Dog("Buddy", 7)
dog2 = Dog("Lucy", 3)

print(dog1.name)  # Output: Buddy
print(dog2.name)  # Output: Lucy

The self.name and self.age in the constructor (__init__) are instance variables. Each Dog instance has a different name and age.


1.2 Class-level data


On the other hand, class-level data or class attributes are shared across all instances of a class. Let's stick with our house analogy. Suppose the blueprint specifies that every house built from it should have a smoke detector. In this case, the smoke detector is like a class attribute - it's common to all houses (instances).

Here's a code example:

class Dog:
    species = "Canis familiaris"  # class attribute

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

dog1 = Dog("Buddy", 7)
dog2 = Dog("Lucy", 3)

print(dog1.species)  # Output: Canis familiaris
print(dog2.species)  # Output: Canis familiaris

In this case, the species attribute is shared by all Dog instances, and it doesn't change from dog to dog.


2. Exploring Class Methods and Alternative


Constructors


In the previous section, we saw that Python objects can have both instance and class data. Now, let's talk about their behavior, specifically class methods.


2.1 Understanding class methods


While instance methods, the most common type of methods, operate on an instance of a class (hence the self parameter), class methods operate on the class itself. We indicate a class method by using the @classmethod decorator and passing the class as the first argument, conventionally named cls.


Here's an example:

class Dog:
    species = "Canis familiaris"  # class attribute

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

    @classmethod
    def common_breed(cls):
        return "Most dogs belong to the species " + cls.species

print(Dog.common_breed())  # Output: Most dogs belong to the species Canis familiaris

In the code above, common_breed() is a class method that operates on the class level data (species), not the instance data.


2.2 Alternative constructors


Imagine that you're constructing a building, and the blueprint comes in a foreign language. Instead of having to translate it every time, it would be easier if you had an alternative blueprint in a language you understand. This is where alternative constructors come in.


An alternative constructor is a class method that provides a different way to create instances of a class. The built-in __init__ method is the primary way to instantiate an object, but sometimes it's beneficial to have alternative methods that can create objects in different ways.


Here's an example of an alternative constructor for a Dog class that allows a dog to be created from a string of the form "Name-Age":

class Dog:
    species = "Canis familiaris"  # class attribute

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

    @classmethod
    def from_string(cls, data):
        name, age = data.split("-")
        return cls(name, int(age))  # create a new Dog instance

dog = Dog.from_string("Max-5")
print(dog.name)  # Output: Max
print(dog.age)  # Output: 5

In the above code, the from_string method takes a string, splits it to get the name and age, and creates a new Dog instance using them.


3. Code Reuse and Inheritance in OOP


Code reuse is a fundamental principle in software development. We've all heard the phrase "Don't Repeat Yourself (DRY)". Object-Oriented Programming supports this principle through a feature known as inheritance.


3.1 Code reuse


Suppose you have a toolbox. You could craft a new tool each time you need to fix something, but that would be time-consuming and inefficient. Instead, you use the same set of tools for different tasks. This idea is the essence of code reuse.

In Python, we can reuse code by importing modules. For instance, the numpy and pandas libraries contain functions and classes that we use frequently in data analysis.

import numpy as np
import pandas as pd

data = pd.read_csv('file.csv')  # Reading a CSV file using pandas
mean_value = np.mean(data['column'])  # Calculating mean using numpy


3.2 Inheritance in OOP


Inheritance allows us to create a new class that takes on the attributes and methods of another class, while also allowing for customization and expansion. This idea is like a child inheriting characteristics from their parent but also having their unique traits.


We can relate this to a car manufacturing scenario. There's a base model for a car. However, different variants inherit the base properties and add or override features, like a sunroof or turbocharged engine.


Let's dive into a simple inheritance example in Python:

# Parent class
class Bird:
    def __init__(self):
        print("Bird is ready")

    def whoisThis(self):
        print("Bird")

    def swim(self):
        print("Swim faster")

# Child class
class Penguin(Bird):
    def __init__(self):
        super().__init__()  # calling the parent class constructor
        print("Penguin is ready")

    def whoisThis(self):
        print("Penguin")

    def run(self):
        print("Run faster")

peggy = Penguin()
peggy.whoisThis()  # Output: Penguin
peggy.swim()  # Output: Swim faster
peggy.run()  # Output: Run faster

In the code above, Penguin is a subclass (child class) of Bird. Penguin inherits methods from Bird and also adds its method, run. The whoisThis method is overridden in Penguin.


4. Implementing Class Inheritance


Having understood the principle of inheritance, we will now dive into the nitty-gritty of implementing inheritance in Python. We'll extend our understanding using a practical example from the banking domain.


4.1 Class Hierarchy and Code Reuse


Inheritance establishes a hierarchy of classes that allows code reuse and method overriding. This hierarchy can be related to a family tree, where the parent class is at the top, and subclasses form the next layers. The parent class is also often referred to as the superclass, and subclasses are sometimes called derived or child classes.


In our banking example, think of the parent class BankAccount as the basic banking service. All bank accounts will inherit these basic features.


4.2 Creating Subclasses - SavingsAccount and CheckingAccount


Continuing with our banking analogy, two common types of bank accounts are a SavingsAccount and a CheckingAccount. They both share common features with a BankAccount, but each has its unique characteristics.

A SavingsAccount earns interest, and a CheckingAccount allows unlimited transactions. This is a classic case for using inheritance, where SavingsAccount and CheckingAccount can inherit from the BankAccount class and add or

override features.


Let's see this in code:

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

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

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
        else:
            print(f"Insufficient funds! Current balance: {self.balance}")

    def display_balance(self):
        print(f"Balance: {self.balance}")

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

    def add_interest(self):
        interest = self.balance * self.interest_rate
        self.balance += interest

class CheckingAccount(BankAccount):
    def __init__(self, account_number, transaction_fee):
        super().__init__(account_number)
        self.transaction_fee = transaction_fee

    def withdraw(self, amount):
        if amount + self.transaction_fee <= self.balance:
            self.balance -= (amount + self.transaction_fee)
        else:
            print(f"Insufficient funds! Current balance: {self.balance}")

In this code, BankAccount is the superclass, and SavingsAccount and CheckingAccount are subclasses. SavingsAccount and CheckingAccount have customized constructors (__init__) and additional attributes (interest_rate and transaction_fee). The CheckingAccount class also overrides the withdraw method of the BankAccount class.


5. Polymorphism in OOP


The final concept we'll discuss is Polymorphism, which is a Greek word meaning "having many forms". In an object-oriented context, polymorphism refers to the ability of a variable, function, or object to take on multiple forms. Simply put, it allows us to use a single interface to represent different types of actions.


5.1 Understanding Polymorphism


A real-world analogy of polymorphism can be a universal remote control. It has a single interface, i.e., buttons like volume up/down, channel up/down, power on/off, etc., but it can control various devices like a TV, a DVD player, a music system, and more.


In Python, polymorphism lets us define methods in the child class with the same name as defined in their parent class. As we know, a child class inherits all the methods from the parent class. However, if we want to modify a method in a child class, we can do so without affecting the parent class. This is called method overriding.


5.2 Polymorphism in Banking


Let's continue with our banking example. Here, we have the withdraw method defined in the BankAccount, SavingsAccount, and CheckingAccount classes. However, it behaves slightly differently in each of them due to different rules for withdrawal. This is an example of polymorphism.

We'll create an instance of each account and call the withdraw method to observe the different behavior:

# creating instances of each class
account = BankAccount("001")
savings_account = SavingsAccount("002", 0.03)
checking_account = CheckingAccount("003", 1.5)

# depositing funds
account.deposit(1000)
savings_account.deposit(2000)
checking_account.deposit(3000)

# attempting withdrawals
account.withdraw(100)
savings_account.withdraw(200)
checking_account.withdraw(400)

# displaying remaining balances
account.display_balance()  # Balance: 900.0
savings_account.display_balance()  # Balance: 1800.0
checking_account.display_balance()  # Balance: 2598.5

As you can see, the withdraw method behaves differently for the CheckingAccount instance due to the additional transaction fee. This illustrates polymorphism, where the same method name results in different behaviors depending on the instance class.


Conclusion


In this tutorial, we've explored some of the foundational concepts of Object-Oriented Programming in Python, including classes, instances, class methods, inheritance, and polymorphism. We've discussed these concepts in detail and illustrated their practical application with a banking domain example.

Understanding these concepts will go a long way in structuring your code effectively and leveraging code reuse, leading to more efficient and maintainable applications. Remember that practice is key in mastering these concepts, so don't hesitate to experiment with them in different scenarios and projects. Keep coding, and happy learning!

コメント


bottom of page