Python
02 / 04

OOP & Advanced Python

OOP & Advanced Python

Python supports object-oriented programming with classes, special methods, and inheritance. Advanced features — decorators, generators, context managers, and dataclasses — enable elegant and reusable code.

Classes & Special Methods

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

    def __repr__(self) -> str:
        return f"Vector({self.x}, {self.y})"

    def __add__(self, other: 'Vector') -> 'Vector':
        return Vector(self.x + other.x, self.y + other.y)

    def __mul__(self, scalar: float) -> 'Vector':
        return Vector(self.x * scalar, self.y * scalar)

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Vector):
            return NotImplemented
        return self.x == other.x and self.y == other.y

    def __hash__(self) -> int:
        return hash((self.x, self.y))

    @property
    def magnitude(self) -> float:
        return (self.x**2 + self.y**2) ** 0.5

v1 = Vector(3, 4)
v2 = Vector(1, 2)
print(v1 + v2)         # Vector(4, 6)
print(v1.magnitude)    # 5.0

Inheritance & Abstract Base Classes

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self) -> float: ...

    @abstractmethod
    def perimeter(self) -> float: ...

    def describe(self) -> str:
        return (f"{type(self).__name__}: "
                f"area={self.area():.2f}, "
                f"perimeter={self.perimeter():.2f}")

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

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

    def perimeter(self) -> float:
        return 2 * 3.14159 * self.radius

class Rectangle(Shape):
    def __init__(self, w: float, h: float):
        self.w = w
        self.h = h

    def area(self) -> float:        return self.w * self.h
    def perimeter(self) -> float:   return 2 * (self.w + self.h)

shapes: list[Shape] = [Circle(5), Rectangle(4, 6)]
for shape in shapes:
    print(shape.describe())

Dataclasses

from dataclasses import dataclass, field
from typing import ClassVar

@dataclass(frozen=True)    # immutable and hashable
class Point:
    x: float
    y: float

    def distance_to(self, other: 'Point') -> float:
        return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

@dataclass
class Team:
    name: str
    max_size: ClassVar[int] = 10  # class variable, not a field
    members: list[str] = field(default_factory=list)

    def __post_init__(self):
        self.name = self.name.strip().title()

    def add_member(self, name: str) -> None:
        if len(self.members) >= self.max_size:
            raise ValueError("Team is full")
        self.members.append(name)

team = Team("  dev team  ")
team.add_member("Alice")
print(team)  # Team(name='Dev Team', members=['Alice'])

Decorators

import functools, time

# Basic decorator
def timer(func):
    @functools.wraps(func)    # preserves __name__, __doc__
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__!r}: {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

# Decorator with arguments
def retry(max_attempts: int = 3, exceptions: tuple = (Exception,)):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, max_attempts + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as e:
                    if attempt == max_attempts:
                        raise
                    print(f"Attempt {attempt} failed: {e}")
        return wrapper
    return decorator

@timer
@retry(max_attempts=3, exceptions=(ConnectionError,))
def fetch_data(url: str) -> dict:
    return {"status": "ok"}

Generators

import itertools

# Generator function — yields values lazily
def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

first_10 = list(itertools.islice(fibonacci(), 10))
# [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# yield from — delegate to sub-generator
def chain_files(*paths):
    for path in paths:
        with open(path) as f:
            yield from f   # yields each line

# Generator pipeline (memory-efficient data processing)
def read_csv(path):
    with open(path) as f:
        yield from f

def parse_rows(lines):
    return (line.strip().split(',') for line in lines)

def filter_active(rows):
    return (row for row in rows if row[2] == 'active')

# Compose the pipeline
pipeline = filter_active(parse_rows(read_csv("users.csv")))

Context Managers

from contextlib import contextmanager, suppress

@contextmanager
def timer_context(label: str):
    start = time.perf_counter()
    try:
        yield
    finally:
        print(f"{label}: {time.perf_counter() - start:.4f}s")

with timer_context("query"):
    time.sleep(0.05)

# Class-based context manager
class Transaction:
    def __init__(self, conn):
        self.conn = conn

    def __enter__(self):
        self.conn.begin()
        return self.conn

    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type:
            self.conn.rollback()
        else:
            self.conn.commit()
        return False   # don't suppress exceptions

# suppress — silently ignore specific exceptions
with suppress(FileNotFoundError):
    open('optional.txt')  # won't raise if missing

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free