Python for AI/ML - Day 5 - OOP
Welcome to the 5th day of Python session. The first four sessions can be reviewed in the link outlined below.
Day 3 - Python for AI/ML - Day 3 | LinkedIn -Dictionaries
Day 4 - Python for AI/ML - Day 4 | LinkedIn - All about Functions
Day 5 is all about Object Oriented Programming using Python.
1. What is Object-Oriented Programming?
Object-Oriented Programming (OOP) is a programming paradigm centered around the concept of “objects” and “classes.” An object represents an entity with certain attributes (data) and behaviors (functions or methods), while a class provides the blueprint for creating objects.
Key Pillars of OOP
Encapsulation: Grouping related data (attributes) and behaviors (methods) into a single unit (class or modules in other programming languages).
Inheritance: The ability of a class to derive or inherit attributes and methods from another class.
Polymorphism: The ability to present the same interface for different underlying data types or classes.
Abstraction: Hiding complex implementation details behind simpler interfaces, showing only the necessary parts.
Lets also understand what are some of the key advantages of OOP.
Key Advantages of OOP
Modularity: Code is organized into distinct classes.
Reusability: Common logic can be shared or inherited across classes.
Maintainability: Changes in one class do not necessarily break other parts of the system if done correctly.
Scalability: It’s easier to add features or new object types without rewriting existing code.
Key OOP Concepts
Classes and Objects
A class is a blueprint that defines the data and behavior of a type of object.
An object (or instance) is a concrete instantiation of that class. If the class were a blueprint, the object would be the constructed building.
Encapsulation
Encapsulation is the idea of grouping together data (attributes) and methods (functions) that operate on that data within one class.
This also allows controlling how the data is accessed or modified—often by using “private” and “public” attributes/methods in some languages. (Python uses naming conventions like _attribute to indicate privacy, but does not strictly enforce it.)
Inheritance
Inheritance means a class (subclass or child) can extend or inherit attributes and methods from another class (superclass or parent).
It promotes code reuse. For example, class Manager could inherit from class Employee and add extra attributes like team.
Polymorphism
Polymorphism means different classes can present the same interface but provide different implementations.
For instance, calculate_pay() might be implemented differently in FullTimeEmployee vs. PartTimeEmployee, but both respond to the same method call in the rest of the application.
Composition
Composition (also called a “has-a” relationship) is when one class has references to objects of other classes.
E.g., a Car can have an Engine object. The car class does not necessarily inherit from Engine—it just holds an Engine instance as part of its data.
Abstraction and Abstract Base Classes
Abstraction hides internal details and shows only necessary parts. For example, you might have an abstract method calculate_pay() that leaves the exact implementation to child classes.
In Python, you can use the abc (Abstract Base Classes) module to enforce that certain methods must be overridden by subclasses.
Inheritance
Key Points
Manager calls super().__init__ to reuse Employee’s constructor.
Overriding: Manager’s get_details() extends the base version by adding team information.
This demonstrates how code reuse and extensibility are achieved with inheritance.
Properties (Getters and Setters)
In Python, the @property decorator and corresponding @<property>.setter allow you to encapsulate attributes, performing validation or transformation behind the scenes. Below is an example using a hypothetical Employee class with an internal _pay_rate.
Key Points
Encapsulation: We store the actual data in _pay_rate and _name (the underscore indicates “intended as private”).
Properties: @property is used to declare getters, and @<property>.setter is used for setters.
Validation: The setter checks for negative values, preventing invalid data
Static Methods
A static method does not receive an implicit self (instance) or cls (class) parameter. It’s a convenient place to define utility or helper functions that relate to a class conceptually but do not depend on its instance-specific data.
Below is a short example demonstrating a static method for validating an employee’s name:
Key Points
No self or cls in @staticmethod. You can call Employee.is_valid_name("A Name") without creating an instance.
Use Cases: Utility checks, data formatting, or computations that don’t require instance data or class state.
Separation of Concerns: The “employee” concept is consistent, but not all logic must be tied to an instance’s state.
Overriding __str__ and __repr__
In Python, special methods (sometimes called “dunder methods”, for double underscore) provide hooks into Python’s built-in functionalities. Two of the most common for displaying objects are:
__str__: Defines the informal or user-friendly string representation of an object.
__repr__: Defines the official or developer-focused string representation (often used for debugging).
Simple Example: Employee with __str__ and __repr__
Key Points
__str__Designed for an informal or user-facing representation of the object.Used when you do print(obj) or str(obj).
__repr__Intended as an official, unambiguous representation, often used in debugging.Used when you call repr(obj) or when an object is printed in an interactive session.
Recreate LogicA common Python convention is to have __repr__ return something that could (theoretically) be used to recreate the object. For instance, Employee(1, 'Alice') looks like a valid Python expression to construct an Employee.Note the use of !r (the repr format specifier) in the f-string to ensure we get quotes around the string fields.
Case Study: Employee Time Tracking App
We will create a time tracker that:
Holds different Employee types (Full-time vs. Part-time).
Allows them to clock in and clock out, storing these events in TimeRecord objects.
Calculates how many hours each employee has worked.
Calculates their pay based on hours and their pay structure.
We will first create a version without abstract base classes for simplicity (Part A), then refactor the code to demonstrate using the abc module (Part B).
Part A: Implementing Without Abstract Base Classes
In this approach, we’ll simply define Employee as a base class, and inherit from it to create specialized classes FullTimeEmployee and PartTimeEmployee. Because we’re not using the abc module, we’ll rely on the convention that each child class provides a calculate_pay() method.
Step A1: The Base Employee Class
Key Points
We have __init__ for basic employee data.
We define a default calculate_pay method that returns 0.0 or does minimal work.
A real-world approach might raise a NotImplementedError, but here we keep it simple to show a non-abstract approach.
Step A2: FullTimeEmployee and PartTimeEmployee (Inheritance, Polymorphism)
We’ll now create two subclasses of Employee—FullTimeEmployee and PartTimeEmployee. Both share the same interface (i.e., have calculate_pay(hours_worked)), but implement it differently.
Key Points
Inheritance: Both classes call super().__init__() to reuse the base Employee initializer.
Polymorphism:When we do employee.calculate_pay(some_hours), the correct version (full-time or part-time) is called at runtime.We do not have to check if isinstance(employee, FullTimeEmployee)—the method call dispatches automatically.
Step A3: TimeRecord and Composition in TimeTracker
TimeRecord will store one clock-in/clock-out cycle for a single employee. TimeTracker will hold multiple time records (composition).
Key Points
TimeTracker provides clock_in, clock_out methods.
Composition: TimeTracker has a list of TimeRecord objects, but these objects can exist independently too (they are not destroyed when TimeTracker is destroyed, if something else has references to them).
Step A4: Building the Payroll Service (Demonstrating Polymorphism)
We want to calculate each employee’s pay (either monthly salary or hourly wage) based on hours from TimeTracker. We do that in a separate class to keep pay logic separate from time-tracking logic (Single Responsibility Principle). We will cover about Single Responsibility Principle aka SRP in later sessions as part of SOLID principles.
Key Points
Polymorphic Method Call: employee.calculate_pay() depends on the actual subclass of Employee.
We don’t do if type(employee) == FullTimeEmployee: ... else: ...; we rely on polymorphism to pick the right method.
Step A5: Putting It All Together (Terminal App Example)
Below is a basic terminal-based example tying everything together:
Example Output (times are for illustration):
Part B: Refactoring to Use Abstract Base Classes (ABC)
Now we’ll demonstrate a more idiomatic OOP approach in Python: using the abc module to enforce that Employee is an abstract class and that its subclasses must implement calculate_pay().
Step B1: Introducing the Abstract Base Class for Employees
Key Points
We import ABC (Abstract Base Class) and abstractmethod from the abc module.
Marking Employee(ABC) and @abstractmethod means you cannot instantiate Employee directly. You must create a subclass.
Step B2: Adjust FullTimeEmployee and PartTimeEmployee
Because Employee is now an abstract class, FullTimeEmployee and PartTimeEmployee must implement the abstract method calculate_pay():
Step B3: Remainder of Code (TimeRecord, TimeTracker, PayrollService)
We don’t need to change TimeRecord, TimeTracker, or PayrollService, as the only difference is that Employee is now abstract. However, let’s provide the full code for clarity:
Step B4: Final Usage
We can show a usage example similar to Part A:
Now, if you ever tried to do employee = Employee(3, "Generic"), Python would raise an error: TypeError: Can't instantiate abstract class Employee with abstract method calculate_pay.
Conclusion and Further Extensions
We’ve built a time-tracking terminal app using Object-Oriented Programming concepts:
Encapsulation: Data (attributes) and methods are grouped within classes like Employee, TimeRecord, TimeTracker.
Inheritance: FullTimeEmployee and PartTimeEmployee inherit from a base Employee.
Polymorphism: calculate_pay is implemented differently for each subclass, but called via the same interface.
Composition: TimeTracker holds multiple TimeRecord objects.
Abstraction (with ABC): In Part B, we used Python’s abc module to enforce that Employee cannot be instantiated directly, ensuring each subclass implements its own calculate_pay.
Possible Next Steps
Add Overtime Rules: Full-time employees might get overtime pay after 40 hours.
Database Storage: Instead of just storing data in lists, save records in a database.
Manager Subclass: Could override or extend methods to approve timesheets for subordinates.
GUI or Web Interface: Replace the terminal interface with a web server or desktop GUI.
With these OOP concepts—and a functional time tracking system as a starting point—you’re well-equipped to tackle more sophisticated problems, ensuring your code is organized, extensible, and maintainable. Happy coding!
PS: This is a quick draft (editing and more clarifications will be incrementally added)
This series is part of internal sessions of Python for AI/ML at Algorisys cc: Radhika Pillai