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.0Inheritance & 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 missingKeep your own version of these notes — editable, searchable, and organised by your stack.
Start free