Docs

oop

09 - Object-Oriented Programming (OOP)

📌 What You'll Learn

  • Classes and objects
  • Constructors (__init__)
  • Instance vs class variables
  • Methods (instance, class, static)
  • Encapsulation (public, protected, private)
  • Inheritance
  • Polymorphism
  • Abstraction
  • Magic/dunder methods
  • Property decorators

🏗️ What is OOP?

Object-Oriented Programming is a programming paradigm that organizes code into objects that contain both data (attributes) and behavior (methods).

Key Concepts

  • Class: A blueprint for creating objects
  • Object: An instance of a class
  • Attribute: Data stored in an object
  • Method: Function defined inside a class

📦 Classes and Objects

Basic Class

class Dog:
    # Class attribute (shared by all instances)
    species = "Canis familiaris"

    # Constructor (initializer)
    def __init__(self, name, age):
        # Instance attributes (unique to each instance)
        self.name = name
        self.age = age

    # Instance method
    def bark(self):
        return f"{self.name} says Woof!"

    def description(self):
        return f"{self.name} is {self.age} years old"

# Create objects (instances)
buddy = Dog("Buddy", 5)
max = Dog("Max", 3)

# Access attributes
print(buddy.name)      # Buddy
print(buddy.species)   # Canis familiaris

# Call methods
print(buddy.bark())        # Buddy says Woof!
print(buddy.description()) # Buddy is 5 years old

🔧 Constructor and self

class Person:
    def __init__(self, name, age):
        """
        Constructor - called when creating an object.
        'self' refers to the instance being created.
        """
        self.name = name
        self.age = age
        self.email = None  # Default value

    def greet(self):
        """
        Instance method - first parameter is always 'self'.
        """
        return f"Hello, I'm {self.name}"

# Create instance
person = Person("Alice", 25)

# self is automatically passed
print(person.greet())  # Same as Person.greet(person)

📊 Instance vs Class Variables

class Employee:
    # Class variable - shared by ALL instances
    company = "TechCorp"
    employee_count = 0

    def __init__(self, name, salary):
        # Instance variables - unique to each instance
        self.name = name
        self.salary = salary

        # Modify class variable
        Employee.employee_count += 1

    def get_info(self):
        return f"{self.name} works at {Employee.company}"

# Create employees
emp1 = Employee("Alice", 50000)
emp2 = Employee("Bob", 60000)

print(Employee.employee_count)  # 2

# Class variable is shared
print(emp1.company)  # TechCorp
print(emp2.company)  # TechCorp

# Changing class variable affects all
Employee.company = "MegaCorp"
print(emp1.company)  # MegaCorp
print(emp2.company)  # MegaCorp

🔄 Types of Methods

Instance Methods

class Circle:
    def __init__(self, radius):
        self.radius = radius

    # Instance method - operates on instance data
    def area(self):
        return 3.14159 * self.radius ** 2

Class Methods

class Circle:
    pi = 3.14159

    def __init__(self, radius):
        self.radius = radius

    # Class method - operates on class data
    @classmethod
    def from_diameter(cls, diameter):
        """Alternative constructor"""
        return cls(diameter / 2)

    @classmethod
    def set_pi(cls, value):
        cls.pi = value

# Use class method as factory
circle = Circle.from_diameter(10)
print(circle.radius)  # 5.0

Static Methods

class MathUtils:
    @staticmethod
    def add(a, b):
        """Static method - no access to class or instance"""
        return a + b

    @staticmethod
    def is_positive(n):
        return n > 0

# Call without creating instance
print(MathUtils.add(3, 5))
print(MathUtils.is_positive(-5))

🔒 Encapsulation

Control access to attributes and methods.

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner           # Public
        self._account_type = "savings"  # Protected (convention)
        self.__balance = balance     # Private (name mangling)

    # Getter
    def get_balance(self):
        return self.__balance

    # Setter with validation
    def deposit(self, amount):
        if amount > 0:
            self.__balance += amount
            return True
        return False

    def withdraw(self, amount):
        if 0 < amount <= self.__balance:
            self.__balance -= amount
            return True
        return False

account = BankAccount("Alice", 1000)

# Public - accessible
print(account.owner)  # Alice

# Protected - accessible but shouldn't be modified directly
print(account._account_type)  # savings

# Private - name mangled
# print(account.__balance)  # AttributeError!
print(account.get_balance())  # 1000

# Still accessible via mangled name (don't do this!)
print(account._BankAccount__balance)  # 1000

🎭 Property Decorator

Pythonic way to create getters/setters.

class Temperature:
    def __init__(self, celsius=0):
        self._celsius = celsius

    @property
    def celsius(self):
        """Getter for celsius"""
        return self._celsius

    @celsius.setter
    def celsius(self, value):
        """Setter with validation"""
        if value < -273.15:
            raise ValueError("Temperature below absolute zero!")
        self._celsius = value

    @property
    def fahrenheit(self):
        """Read-only computed property"""
        return self._celsius * 9/5 + 32

# Use like attributes
temp = Temperature(25)
print(temp.celsius)     # 25
print(temp.fahrenheit)  # 77.0

temp.celsius = 30
print(temp.celsius)     # 30

# temp.fahrenheit = 100  # AttributeError - read-only

👪 Inheritance

Create new classes based on existing ones.

# Parent class (base class)
class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def speak(self):
        return "Some sound"

    def description(self):
        return f"{self.name} is {self.age} years old"

# Child class (derived class)
class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)  # Call parent constructor
        self.breed = breed

    def speak(self):  # Override parent method
        return f"{self.name} says Woof!"

    def fetch(self):  # Add new method
        return f"{self.name} is fetching!"

class Cat(Animal):
    def speak(self):
        return f"{self.name} says Meow!"

# Create instances
dog = Dog("Buddy", 5, "Golden Retriever")
cat = Cat("Whiskers", 3)

print(dog.description())  # From parent
print(dog.speak())        # Overridden
print(dog.fetch())        # New method

print(cat.speak())        # Overridden

Multiple Inheritance

class Flyable:
    def fly(self):
        return "Flying!"

class Swimmable:
    def swim(self):
        return "Swimming!"

class Duck(Animal, Flyable, Swimmable):
    def speak(self):
        return "Quack!"

duck = Duck("Donald", 2)
print(duck.fly())    # From Flyable
print(duck.swim())   # From Swimmable
print(duck.speak())  # From Duck

# Method Resolution Order
print(Duck.__mro__)

🎭 Polymorphism

Same interface, different implementations.

class Shape:
    def area(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14159 * self.radius ** 2

# Polymorphism in action
shapes = [Rectangle(4, 5), Circle(3), Rectangle(2, 6)]

for shape in shapes:
    print(f"Area: {shape.area()}")
    # Each shape calculates area differently!

🎨 Abstract Classes

Define interface that subclasses must implement.

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        """Subclasses MUST implement this"""
        pass

    @abstractmethod
    def perimeter(self):
        pass

    def description(self):
        """Concrete method - optional to override"""
        return "I am a shape"

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

# shape = Shape()  # TypeError - can't instantiate abstract class
rect = Rectangle(4, 5)
print(rect.area())
print(rect.description())

✨ Magic/Dunder Methods

Special methods for operator overloading and object behavior.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    # String representation
    def __str__(self):
        """For print() and str()"""
        return f"Vector({self.x}, {self.y})"

    def __repr__(self):
        """For debugging"""
        return f"Vector({self.x!r}, {self.y!r})"

    # Arithmetic operators
    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __sub__(self, other):
        return Vector(self.x - other.x, self.y - other.y)

    def __mul__(self, scalar):
        return Vector(self.x * scalar, self.y * scalar)

    # Comparison
    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __lt__(self, other):
        return (self.x**2 + self.y**2) < (other.x**2 + other.y**2)

    # Length
    def __len__(self):
        return int((self.x**2 + self.y**2) ** 0.5)

    # Boolean
    def __bool__(self):
        return self.x != 0 or self.y != 0

v1 = Vector(3, 4)
v2 = Vector(1, 2)

print(v1)           # Vector(3, 4)
print(v1 + v2)      # Vector(4, 6)
print(v1 * 2)       # Vector(6, 8)
print(v1 == v2)     # False
print(len(v1))      # 5
print(bool(v1))     # True

Common Magic Methods

MethodPurpose
__init__Constructor
__str__String for users
__repr__String for developers
__add__+ operator
__sub__- operator
__mul__* operator
__eq__== operator
__lt__, __gt__< and > operators
__len__len() function
__getitem__[] indexing
__setitem__[] assignment
__iter__Iteration
__call__() calling

📋 Summary

ConceptDescription
ClassBlueprint for objects
ObjectInstance of a class
selfReference to current instance
__init__Constructor method
Instance varUnique to each object
Class varShared by all objects
@classmethodMethod with class access
@staticmethodMethod without class/instance access
@propertyGetter/setter decorator
InheritanceDerive classes from parent
PolymorphismSame interface, different behavior
AbstractionHide complexity, show essentials
EncapsulationRestrict access to internals

🎯 Next Steps

After mastering OOP, proceed to 10_advanced_functions to learn about closures, decorators in depth, and generators!

Oop - Python Tutorial | DeepML