Understanding Object-Oriented Programming in Python

Python for AI, data science and machine learning Day 1

Gianpiero Andrenacci
Data Bistrot
15 min readMar 12, 2024

--

Object-Oriented Programming in Python

Python is a versatile programming language that supports multiple programming paradigms, including object-oriented programming (OOP) and procedural programming. This flexibility allows developers to choose the most suitable paradigm based on the specific needs of their project, combining the strengths of each approach to achieve the best possible outcomes.

Procedural Programming in Python

Procedural programming is based on the concept of procedure calls. It structures a program like a recipe in that it provides a set of steps, in the form of functions and code blocks, that flow sequentially in order to complete a task. This paradigm is particularly useful for straightforward tasks that can be accomplished with a series of procedural steps, making the code easy to understand and debug.

Object-Oriented Programming (OOP) in Python

Object-oriented programming (OOP) is a programming paradigm that uses objects and classes to structure software programs. It emphasizes the bundling of data (attributes) and behaviors (methods) that operate on the data into individual units called objects. This approach is beneficial for modeling real-world entities, promoting code reuse, and facilitating the development of complex software systems.

In other words, OOP is a paradigm that organizes software design around data, or objects, rather than functions and logic. An object can be defined as a data field that has unique attributes and behavior. Python’s support for OOP is extensive, allowing developers to implement classes, inheritance, polymorphism, and encapsulation (we’ll see everithing in details), thereby making it easier to create complex, reusable, and modular code that can model real-world scenarios.

When to Use OOP or Procedural Programming

Choosing between OOP and procedural programming depends on various factors, including the complexity of the application, team preferences, and the specific requirements of the software being developed.

Generally, OOP is more suited to applications with complex data models and behaviors, requiring reusable and modular code.

Procedural programming, on the other hand, might be more appropriate for simpler, task-based scripts where a straightforward sequence of actions is performed.

In later discussions, we will delve deeper into scenarios and examples that illustrate when to use OOP and when to opt for procedural programming. Understanding the strengths and weaknesses of each paradigm will enable you to make informed decisions about structuring your Python programs for maximum efficiency and clarity.

Classes

In OOP, a class is a blueprint for creating objects. It defines a set of attributes and methods that the created objects of the class will have. Classes provide a means of bundling data and functionality together.

A class in Python can be thought of like a stamp in an industrial setting. Imagine you have a stamp designed to produce a specific shape or pattern — this stamp is your class. Each time you press the stamp onto a material, you create an exact replica of the shape or pattern defined by the stamp. These replicas are akin to objects in Python.

Just as the stamp encapsulates the design details and can be used repeatedly to produce multiple copies (objects), a class encapsulates data (attributes) and behaviors (methods) and can be instantiated multiple times to create multiple objects.

Each object is a unique instance that can hold different values in its attributes, similar to how each stamp impression can be made on different materials or in different colors, yet all share the same underlying design.

Creating a class in Python is simple and straightforward:

class MyClass:
# Class attribute
attribute = "This is a class attribute."

# Initializer / Instance attributes
def __init__(self, value):
self.instance_attribute = value

# Method
def my_method(self):
return f"My instance attribute is: {self.instance_attribute}"

In this example, MyClass has a class attribute attribute, an initializer method __init__ that sets instance attributes, and a method my_method that operates on instance attributes.

Objects

An object is an instance of a class. When a class is defined, only the description for the object is defined; therefore, no memory or storage is allocated. The object is created from the class when it is instantiated.

Each object can have different values for its attributes, distinguishing it from other objects of the same class (remember the stamp example).

Creating an object is as simple as calling the class:

my_object = MyClass("Hello, Object!")
print(my_object.instance_attribute) # Accessing an instance attribute
print(my_object.my_method()) # Calling a method on the objectoo

Class Attributes

In Object-Oriented Programming (OOP), attributes are variables that belong to a class or an instance of a class. They represent the properties or characteristics of an object that help to distinguish it from other objects. Attributes are used to store information about the object, and they can be accessed or modified during the lifetime of an object.

There are two main types of attributes in OOP: class attributes and instance attributes.

Class attributes are variables that are shared across all instances of a class. They belong to the class itself, not to any individual instance. This means that if you change the value of a class attribute, the change is reflected across all instances of the class. Example of class atrribute:

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

def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age # Instance attribute

# Accessing a class attribute
print(Dog.species) # Output: Canis familiaris

# Class attributes are shared by all instances
dog1 = Dog("Buddy", 5)
dog2 = Dog("Molly", 3)
print(dog1.species) # Output: Canis familiaris
print(dog2.species) # Output: Canis familiaris

One primary application of class attributes is to define global constants that are relevant to the class. For instance, consider a scenario where you’re creating a class to represent different shapes. In a Circle class, you might designate the value of pi as a class attribute, as it’s a commonly used constant within the context of circles. This helps to maintain consistency and clarity throughout your code.

Instance Attributes

Instance attributes are owned by the specific instances of a class. This means that for each object or instance of a class, the instance attributes are different (unless explicitly set to the same value).

Instance attributes are usually defined within the __init__ method (see later for detail), also known as the initializer or constructor, and they are prefixed with self to denote that they belong to the particular instance of the class.

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

def __init__(self, name, age):
self.name = name # Instance attribute
self.age = age # Instance attribute

# Each instance has its own attributes
dog1 = Dog("Buddy", 5)
dog2 = Dog("Molly", 3)

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

Key Points for Attributes

  • Attributes store data about an object.
  • Class attributes are shared by all instances of the class. Changing a class attribute affects all instances.
  • Instance attributes are unique to each instance. Changing an instance attribute only affects that specific instance.
  • Attributes can be accessed using dot notation (e.g., instance.attribute or Class.attribute).

Understanding attributes is fundamental in OOP, as they are essential for defining the state of objects and distinguishing between different instances of a class. They allow objects to maintain state across the functions that operate on them, enabling the encapsulation of data within objects.

Methods in OOP

Methods in Python are functions that are defined within a class and are used to define the behaviors of an object. They can operate on the data (attributes) that are contained by the class and can be accessed using the object of the class. Methods are essential for implementing the functionalities of objects.

There are several types of methods in Python:

  • Instance Methods: Operate on an instance of the class and have access to the instance (self) and its attributes.
  • Class Methods: Operate on the class itself, rather than instances of the class. They are marked with a @classmethod decorator and take cls as the first parameter.
  • Static Methods: Do not operate on the instance or the class. They are marked with a @staticmethod decorator and do not take self or cls as the first parameter. They are used for utility functions that do not access class or instance data.

To illustrate the use of methods in Python, let’s create a class Car that demonstrates instance methods, class methods, and static methods. This example will help clarify how different types of methods can be used within a class to manipulate data and manage behavior.

class Car:
# Class attribute
total_cars = 0

def __init__(self, make, model, year):
self.make = make
self.model = model
self.year = year
Car.total_cars += 1 # Increment the total number of cars each time a new car is created

# Instance method
def description(self):
"""Returns a description of the car."""
return f"{self.year} {self.make} {self.model}"

# Class method
@classmethod
def total_cars_created(cls):
"""Returns the total number of cars created."""
return f"Total cars created: {cls.total_cars}"

# Static method
@staticmethod
def is_vintage(year):
"""Determines if a car is vintage based on its year."""
return year < 1990

Example: Using the Car Class

Now, let’s use the Car class to create some car objects, call instance methods, use the class method to get the total number of cars created, and use the static method to check if a car is considered vintage.

# Creating car objects
car1 = Car("Toyota", "Corolla", 1985)
car2 = Car("Ford", "Mustang", 1968)
# Using an instance method
print(car1.description()) # Output: 1985 Toyota Corolla
print(car2.description()) # Output: 1968 Ford Mustang
# Using a class method
print(Car.total_cars_created()) # Output: Total cars created: 2
# Using a static method
print(Car.is_vintage(1985)) # Output: True
print(Car.is_vintage(1995)) # Output: False

Explanation of Class code

  • Instance Methods: The description method is an instance method because it operates on the attributes of an instance of the Car class (self). It provides a formatted string that describes the car.
  • Class Methods: The total_cars_created method is a class method, as indicated by the @classmethod decorator. It operates on the class itself (cls) rather than an instance of the class. This method accesses a class attribute (total_cars) to return the total number of Car instances created.
  • Static Methods: The is_vintage method is a static method, marked with the @staticmethod decorator. It does not take self or cls as parameters, making it independent of the class and instance state. It performs a simple check to determine if a given year qualifies a car as vintage, based on the criteria that a vintage car must be from before 1990.

This example demonstrates how to define and use instance methods, class methods, and static methods within a Python class, showcasing their respective uses and benefits in organizing and managing data and behavior in an object-oriented programming approach.

Details on the three types of method in python

If you’re already familiar with the three types of methods, feel free to skip this part, but remember that repetition can often aid in reinforcement.

Instance Methods in Python

Instance methods are a fundamental concept in object-oriented programming (OOP) in Python. They are used to define the behaviors and actions that can be performed with an instance of a class. Understanding instance methods is crucial for designing and implementing classes that encapsulate both data and the operations on that data in Python.

What are Instance Methods?

Instance methods are functions defined inside a class that operate on an instance of that class. They can access and modify the state of the instance because they include a reference to the instance itself, typically named self. This self parameter is a convention in Python OOP, allowing instance methods to access attributes and other methods on the same object.

How to Define and Use Instance Methods

To define an instance method, you simply declare a function within a class. The first parameter of the method must be self, which is a reference to the instance that the method is being called on. This allows the method to access and manipulate the instance's attributes.

Here is a basic example to illustrate the definition and usage of instance methods:

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

# Instance method
def introduce_yourself(self):
print(f"Hello, my name is {self.name} and I am {self.age} years old.")

# Creating an instance of the Person class
person1 = Person("Alice", 30)

# Calling an instance method on the person1 instance
person1.introduce_yourself()

In this example, introduce_yourself is an instance method of the Person class. It accesses the name and age attributes of the instance person1 through the self parameter to print a greeting.

Key Points about Instance Methods

  • Access to Instance Attributes: Instance methods can freely access and modify attributes attached to the instance they belong to.
  • First Parameter is self: The self parameter in an instance method refers to the instance calling the method, which allows the method to access the instance's attributes and other methods.
  • Invoked on an Instance: Unlike class methods or static methods, instance methods are called on a specific object instance, not on the class itself.

Practical Usage in Data Projects

In the context of data science and engineering projects, instance methods can be used to encapsulate functionality related to data processing, analysis, and visualization within custom classes. For example, you might have a DataFrameWrapper class that wraps a pandas DataFrame, and instance methods on this class could include functionalities to clean data, perform calculations, or generate plots specific to your dataset.

Understanding and utilizing instance methods allow for more organized, readable, and maintainable code in data projects, adhering to the principles of encapsulation and abstraction in OOP.

Class Methods in Python

Class methods are a critical feature of Python’s object-oriented programming (OOP) that allow you to define functions that operate on the class itself, rather than on instances of the class. These methods follow the @classmethod decorator and are distinguished by the use of cls as their first parameter, which represents the class itself, similar to how self represents an instance of the class in instance methods.

Class methods have access only to the class itself and its attributes; they do not have access to instance-level data. This means that within a class method, you cannot directly access or modify instance attributes. Instead, class methods are typically used for operations that involve the class as a whole, rather than individual instances.

Defining and Using Class Methods

To define a class method, you use the @classmethod decorator above the method definition. The first parameter of a class method is conventionally named cls, which is a reference to the class on which the method was called. This allows class methods to access class variables and other class methods.

Here’s an example to demonstrate how class methods are defined and used:

class Employee:
num_of_employees = 0 # Class variable

def __init__(self, name, salary):
self.name = name
self.salary = salary
Employee.num_of_employees += 1

@classmethod
def get_num_of_employees(cls):
return f"Total number of employees: {cls.num_of_employees}"

# Accessing a class method without creating an instance
print(Employee.get_num_of_employees())

# Creating instances of Employee
emp1 = Employee("John", 50000)
emp2 = Employee("Doe", 60000)

# Accessing the class method after creating instances
print(Employee.get_num_of_employees())

In this example, get_num_of_employees is a class method that returns the total number of employees, a class variable that tracks the number of instances created from the Employee class. Notice how we can call get_num_of_employees on the Employee class itself, without needing to create an instance of the class.

Key Characteristics of Class Methods

  • Operate on the Class: Unlike instance methods, class methods work with the class itself, not with individual instances. They can modify class state that applies across all instances of the class.
  • Use of cls Parameter: The cls parameter in a class method refers to the class on which the method is called. This allows class methods to access class variables and other methods.
  • Marked by @classmethod Decorator: The @classmethod decorator is used to indicate that a method is a class method.

Practical Applications in Data Projects

Class methods are particularly useful in scenarios where you need to perform operations that are relevant to the entire class, rather than individual instances. In data science projects, class methods can be used to implement factory methods that create instances from data in different formats, to keep track of statistics related to all instances, or to configure global settings for data processing classes.

For instance, if you have a class that processes datasets, you might use a class method to set a class-level parameter that defines the default format for data files (CSV, JSON, etc.) to be processed by all instances of the class. This approach promotes a clean, organized, and modular codebase, facilitating better maintenance and scalability of data projects.

Static Methods in Python

Static methods in Python’s object-oriented programming (OOP) are a type of method that neither operates on an instance of the class (like instance methods) nor on the class itself (like class methods). They are utility functions that belong to a class but do not access or modify class or instance-specific data. Static methods are marked with the @staticmethod decorator and do not take self or cls as the first parameter.

Defining and Using Static Methods

To define a static method, you use the @staticmethod decorator above the method definition. Since static methods do not operate on the class or instance, they do not require self or cls parameters. They can take any number of parameters or none at all, depending on what the method is designed to do.

Here’s an example demonstrating the definition and usage of static methods:

class MathOperations:

@staticmethod
def add(x, y):
return x + y

@staticmethod
def multiply(x, y):
return x * y

# Using static methods without creating an instance of the class
print(MathOperations.add(5, 7)) # Output: 12
print(MathOperations.multiply(3, 4)) # Output: 12

In this example, add and multiply are static methods of the MathOperations class that perform addition and multiplication, respectively. These methods are called on the class itself, without the need to instantiate an object of the class.

Key Characteristics of Static Methods

  • Independence from Class and Instance: Static methods do not access or modify the class state or instance state. They operate independently, acting as utility functions.
  • No self or cls Parameter: Static methods do not include self or cls parameters because they do not operate on an instance or the class itself.
  • Marked by @staticmethod Decorator: The @staticmethod decorator is used to indicate that a method is a static method.

Practical Applications in Data Projects

Static methods are ideal for utility or helper functions that perform tasks not directly associated with a class’s core responsibilities. In data science and engineering projects, static methods can be used for calculations, data validation, or processing steps that are generic and not tied to the specific state of a class or its instances.

For example, if you have a class that handles data transformations, you might include static methods for validating data formats, cleaning strings, or converting between measurement units. These methods are related to the class’s domain but do not require access to the class or instance data, making them perfect candidates for static methods.

Using static methods helps keep your code organized, modular, and easier to understand, as it clearly distinguishes between operations that are part of an object’s behavior and those that are general-purpose utilities.

Conclusion: Laying the Foundation for OOP in Data Science and Beyond

Object-oriented programming in Python provides a powerful and intuitive way to model real-world entities and build complex applications. By understanding and using classes, objects, and methods, developers can write code that is more modular, reusable, and easy to understand.

This article marks the beginning of an enlightening series designed to explore the application of Object-Oriented Programming (OOP) within the realms of data science, machine learning, deep learning, and large language models (LLMs).

Our journey through the intricacies of Python OOP is not just about understanding syntax or memorizing patterns; it’s about embracing a paradigm that can fundamentally change how we approach problem-solving in these advanced fields.

OOP offers a structured and scalable way to manage complex software projects, including data-intensive applications. By encapsulating data and behaviors into objects, we can model real-world phenomena more naturally, making our code more intuitive, maintainable, and reusable.

This is particularly relevant in data science and machine learning, where the complexity of data and algorithms demands a level of organization and abstraction that OOP is uniquely qualified to provide.

As we delve deeper into this series, we will explore how OOP principles can be applied to design robust data models, implement machine learning algorithms, manage large datasets, and build scalable data pipelines. We will also look at how OOP can enhance the development of tools and frameworks for LLMs, facilitating the creation of more sophisticated and user-friendly interfaces for interacting with these powerful models.

From the foundational concepts of classes, objects, and methods to more advanced topics like inheritance, polymorphism, and design patterns, this series aims to equip you with the knowledge and skills to leverage OOP in your data science and machine learning projects. Whether you are analyzing vast datasets, building predictive models, or developing cutting-edge AI applications, understanding OOP will provide a strong foundation to support your work.

Remember, the journey through OOP is akin to mastering a new way of thinking. It’s about seeing the world of programming through the lens of objects and their interactions, a perspective that can lead to more elegant and efficient solutions to complex problems.

So, whether you’re a beginner eager to learn the basics or an experienced programmer looking to deepen your understanding of OOP in the context of data science and machine learning, this series promises to be a valuable resource. Join us as we embark on this exciting exploration, and let’s unlock the full potential of OOP for data science, ML, and beyond.

This is just the beginning, and I can’t wait to share more of the insights from my study notes and experiences as we continue this journey together.

--

--

Gianpiero Andrenacci
Data Bistrot

Data Analyst & Scientist, ML enthusiast. Avid reader & writer, passionate about philosophy. Ex-BJJ master competitor, national & international title holder.