DATA ANALYSIS  

Classes: Object-Oriented Programming Paradigms

📥
You can download the original Jupyter Notebook for this lesson: classes_paradigms.ipynb

Object-Oriented Programming (OOP) is built upon four fundamental concepts, often called the four pillars of OOP. These principles help in organizing code, reducing complexity, and making it more reusable:

  1. Inheritance: Allows a class to derive attributes and methods from another class.
  2. Abstraction: Hides complex implementation details and only shows the necessary features of an object.
  3. Polymorphism: Allows different classes to be treated as instances of the same general class through the same interface.
  4. Encapsulation: Bundles data and methods that work on that data within a single unit and restricts access to some of the object's components.

1. Inheritance (Reusing Code)

Inheritance allows a class (child class) to inherit attributes and methods from another class (parent class).

  • Benefits: Promotes code reuse and establishes a natural hierarchy.
  • Good Practices: Use inheritance when there is an 'is-a' relationship (e.g., a Student is a Person).
  • Rules: A child class can override methods of the parent class to provide specific behavior.
class Person: def __init__(self, name, year, citizenship): self.name = name self.year = year self.citizenship = citizenship def greet(self): return f"Hi, I'm {self.name}." class Student(Person): def __init__(self, name, year, citizenship, university): super().__init__(name, year, citizenship) self.university = university self.grades = [] def add_grade(self, grade): self.grades.append(grade) s1 = Student("Andrew", 1990, "Polish", "Warsaw University of Technology") s1.add_grade(5)

Tasks: Inheritance

  1. Create a Teacher class that inherits from Person and add a subject attribute.
  2. Override the greet() method in the Teacher class to include the subject they teach (e.g., "Hi, I'm [name] and I teach [subject].").
  3. Create a Staff class that inherits from Person and adds a department attribute.
  4. Create instances of Student, Teacher, and Staff, and call the greet() method on each to see the different behaviors.

2. Abstraction

Abstraction is about hiding the complex reality while exposing only the necessary parts. In Python, we achieve this using Abstract Base Classes (ABC). An abstract class cannot be instantiated directly; it serves as a template that enforces subclasses to implement specific methods.

from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass class Circle(Shape): def __init__(self, radius): self.radius = radius def area(self): return 3.14 * self.radius**2

Tasks: Abstraction

  1. Create a Square subclass of Shape that implements the area() method (remember to define side in the constructor).
  2. Add a new abstract method perimeter() to the Shape class and implement it in both Circle and Square classes.

3. Polymorphism

Polymorphism allows different classes to be treated as instances of the same general class through the same interface. The most common use is when multiple classes have the same method names but different behaviors.

Polymorphism with Abstract Classes

The following example shows how Circle and Rectangle both implement the area() method defined in the Shape interface. Even though the calculation is different, we can call area() on any object that is a Shape.

from abc import ABC, abstractmethod class Shape(ABC): @abstractmethod def area(self): pass class Circle(Shape): def __init__(self, r): self.r = r def area(self): return 3.14 * self.r ** 2 class Rectangle(Shape): def __init__(self, w, h): self.w = w self.h = h def area(self): return self.w * self.h # Different behavior of area() for Rectangle vs Circle is polymorphism

Tasks: Abstraction and Polymorphism

  1. Run the code, create objects of both Circle and Rectangle and run area() function on them for comparison.
  2. Create an abstract class Animal with an abstract method make_sound().
  3. Create subclasses Dog and Cat that implement make_sound() differently.
  4. Create a function animal_chorus(animals) that takes a list of animal instances and calls make_sound() on each. Demonstrate polymorphism by passing a list containing both a Dog and a Cat.

4. Encapsulation: Private and Protected Attributes

Encapsulation is the practice of hiding the internal state of an object and requiring all interaction to be performed through an object's methods.

In Python, encapsulation is mostly a convention:

  • A single underscore _attribute indicates that the attribute is protected. This is a hint that it should not be used on instances directly from outside; it is intended for use inside the class definition and by its subclasses.
  • A double underscore __attribute indicates that the attribute is private. It is more restricted than protected because Python uses name mangling to hide it even from subclasses.

Situations for Use: Use protected when you want to share data with child classes but hide it from the end-user. Use private when you want to ensure the data is strictly local to the parent class and cannot be accidentally overridden or accessed by subclasses.

class Person: def __init__(self, name, year, citizenship): self.name = name self._year = year # protected self.__citizenship = citizenship # private def get_age(self, current_year): return current_year - self._year def get_citizenship(self): return self.__citizenship p1 = Person("Alice", 1990, "Polish") print(f"Age: {p1.get_age(2025)}") print(f"Citizenship: {p1.get_citizenship()}") # While still possible, these are not the intended ways: print(p1._year) # accessible, but protected print(p1._Person__citizenship) # works due to name mangling

Encapsulation in Inheritance

The following example demonstrates that while a subclass can access a protected attribute (_year), it cannot directly access a private attribute (__citizenship) from the parent class.

class Employee(Person): def __init__(self, name, year, citizenship, salary): super().__init__(name, year, citizenship) self.salary = salary def display_details(self): # Accessing protected attribute works print(f"Year of birth: {self._year}") # Accessing private attribute directly would fail try: print(f"Citizenship: {self.__citizenship}") except AttributeError: print("Cannot access private attribute '__citizenship' from subclass.") emp = Employee("Bob", 1985, "German", 50000) emp.display_details()

Tasks:

  1. Create a BankAccount class with a private __balance. Implement deposit(amount) and get_balance() methods. Ensure the balance cannot be modified directly from outside.
  2. Add a withdraw(amount) method that only deducts funds if the balance is sufficient.

Contact details:

+48 790-430-860

analysislessons@gmail.com