OOP Advanced¶
Advanced OOP patterns in Python include Abstract Base Classes for defining interfaces, descriptors for attribute access control, metaclasses for class creation customization, dataclasses for boilerplate reduction, and __slots__ for memory optimization.
Key Facts¶
- ABCs enforce interface contracts - cannot instantiate without implementing all abstract methods
- Descriptors are objects with
__get__/__set__/__delete__- they control attribute access when placed as class attributes - Data descriptors (
__set__) take priority over instance__dict__; non-data descriptors don't __slots__restricts attributes and saves 30-40% memory per instance (no__dict__)@dataclassauto-generates__init__,__repr__,__eq__from type-annotated fields__init_subclass__(Python 3.6+) is a simpler alternative to metaclasses for most use cases
Patterns¶
Abstract Base Classes¶
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
@abstractmethod
def perimeter(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius ** 2
def perimeter(self):
return 2 * 3.14159 * self.radius
# Shape() -> TypeError: Can't instantiate abstract class
Descriptors¶
class PositiveNumber:
def __set_name__(self, owner, name):
self.name = name
self.storage_name = f'_{name}'
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self.storage_name, None)
def __set__(self, obj, value):
if value <= 0:
raise ValueError(f"{self.name} must be positive")
setattr(obj, self.storage_name, value)
class Rectangle:
width = PositiveNumber() # descriptor as class attribute
height = PositiveNumber()
def __init__(self, width, height):
self.width = width # triggers __set__
self.height = height
Lookup priority: data descriptor > instance __dict__ > non-data descriptor > class __dict__.
Metaclasses¶
class ShapeMeta(type):
registry = set()
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if name != 'Shape':
mcs.registry.add(cls)
return cls
class Shape(metaclass=ShapeMeta):
pass
class Circle(Shape): pass
class Rect(Shape): pass
# ShapeMeta.registry == {Circle, Rect}
init_subclass (Simpler Alternative)¶
class Shape:
registry = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Shape.registry.append(cls)
class Circle(Shape): pass
# Shape.registry == [Circle]
new vs init¶
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.value = 42
__new__creates the instance (called before__init__)__init__initializes the instance (called after__new__)
slots¶
class Point:
__slots__ = ('x', 'y')
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(1, 2)
p.z = 3 # AttributeError
Benefits: 30-40% less memory, slightly faster access. Trade-off: no dynamic attributes, no __dict__.
Dataclasses (Python 3.7+)¶
from dataclasses import dataclass, field
@dataclass
class Point:
x: float
y: float
label: str = "origin"
tags: list = field(default_factory=list) # mutable default
def distance(self):
return (self.x**2 + self.y**2) ** 0.5
p = Point(3.0, 4.0)
print(p) # Point(x=3.0, y=4.0, label='origin', tags=[])
Options: @dataclass(frozen=True) for immutable, @dataclass(order=True) for comparison operators.
Combining Patterns¶
from abc import ABCMeta, abstractmethod
class ValidatedMeta(ABCMeta):
registry = {}
def __new__(mcs, name, bases, namespace):
cls = super().__new__(mcs, name, bases, namespace)
if not getattr(cls, '__abstractmethods__', None):
mcs.registry[name] = cls
return cls
class Shape(metaclass=ValidatedMeta):
@property
@abstractmethod
def area(self): ...
Gotchas¶
property,classmethod,staticmethodare all descriptors under the hood__slots__does not work with multiple inheritance if both parents define__slots__- Metaclasses are rarely needed - prefer
__init_subclass__or class decorators @dataclasswith mutable default (list, dict) requiresfield(default_factory=list)- Abstract methods can have implementations (subclass can call
super().method())
See Also¶
- oop fundamentals - basic OOP concepts
- magic methods - dunder method reference
- decorators - class-based decorators
- type hints - Generic classes, Protocol for structural subtyping