Advanced OOP Features
Checking access...
Descriptors
Descriptors control attribute access with __get__, __set__, __delete__:
class ValidatedAttribute: """Descriptor that validates attribute values."""
def __init__(self, validator): self.validator = validator self.data = {}
def __get__(self, obj, objtype=None): if obj is None: return self return self.data.get(id(obj))
def __set__(self, obj, value): if not self.validator(value): raise ValueError(f"Invalid value: {value}") self.data[id(obj)] = value
def __delete__(self, obj): del self.data[id(obj)]
def positive(value): return isinstance(value, (int, float)) and value > 0
def non_empty_string(value): return isinstance(value, str) and len(value) > 0
class Product: name = ValidatedAttribute(non_empty_string) price = ValidatedAttribute(positive)
def __init__(self, name, price): self.name = name self.price = price
p = Product("Widget", 9.99)print(p.name, p.price) # Widget 9.99
# p.price = -5 # ValueError!Properties vs Descriptors
# Property — simple getter/setterclass Circle: def __init__(self, radius): self._radius = radius
@property def radius(self): return self._radius
@radius.setter def radius(self, value): if value < 0: raise ValueError("Radius must be positive") self._radius = value
# Descriptor — reusable across classes# (Use when the same validation pattern appears in multiple classes)Slots
Save memory by preventing __dict__ creation:
class Point: __slots__ = ("x", "y") # Only these attributes allowed
def __init__(self, x, y): self.x = x self.y = y
p = Point(3, 4)print(p.x, p.y) # 3 4# p.z = 5 # AttributeError!# print(p.__dict__) # AttributeError — no __dict__
# Slots save ~50% memory per instanceEnumerations
from enum import Enum, auto, IntEnum
class Color(Enum): RED = "ff0000" GREEN = "00ff00" BLUE = "0000ff"
class Priority(IntEnum): LOW = 1 MEDIUM = 2 HIGH = 3
# Usageprint(Color.RED.value) # ff0000print(Color.RED.name) # REDprint(Priority.HIGH > Priority.LOW) # True — IntEnum supports comparison
# Iterationfor color in Color: print(color)
# Auto-valuesclass Status(Enum): PENDING = auto() ACTIVE = auto() INACTIVE = auto()
print(Status.PENDING.value) # 1
# Enum as type hintdef set_status(s: Status): print(f"Status set to {s.value}")Protocols (Structural Subtyping)
Python 3.8+ has Protocol for static duck typing:
from typing import Protocol
class Drawable(Protocol): def draw(self) -> str: ...
class Circle: def draw(self) -> str: return "Drawing circle"
class Square: def draw(self) -> str: return "Drawing square"
class Triangle: def draw(self) -> str: return "Drawing triangle"
def render(shapes: list[Drawable]): for shape in shapes: print(shape.draw())
# Works without explicit inheritancerender([Circle(), Square()])Metaclasses (Advanced)
Metaclasses are classes of classes — they control class creation:
class SingletonMeta(type): """Metaclass for singleton pattern.""" _instances = {}
def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls]
class Database(metaclass=SingletonMeta): def __init__(self): self.connected = False
def connect(self): self.connected = True
# Both vars refer to the same instancedb1 = Database()db2 = Database()print(db1 is db2) # TrueOperator Overloading
class Money: exchange_rates = {"USD": 1.0, "EUR": 1.1, "GBP": 1.3}
def __init__(self, amount, currency="USD"): self.amount = amount self.currency = currency
def __add__(self, other): if isinstance(other, Money): total = self.amount + other.to_currency(self.currency).amount return Money(total, self.currency) return NotImplemented
def __sub__(self, other): if isinstance(other, Money): total = self.amount - other.to_currency(self.currency).amount return Money(total, self.currency) return NotImplemented
def __mul__(self, factor): if isinstance(factor, (int, float)): return Money(self.amount * factor, self.currency) return NotImplemented
def __rmul__(self, factor): return self.__mul__(factor)
def __repr__(self): return f"{self.currency} {self.amount:.2f}"
usd = Money(100, "USD")eur = Money(50, "EUR")
print(usd + eur) # USD 155.00 (50 EUR = 55 USD)print(eur + usd) # EUR 140.91 (100 USD = 90.91 EUR)print(usd * 2) # USD 200.00print(3 * eur) # EUR 150.00Key Takeaways
- Descriptors (
__get__/__set__) enable reusable attribute behavior __slots__reduces memory by preventing__dict__EnumandIntEnumprovide type-safe constantsProtocolenables structural subtyping (static duck typing)- Metaclasses control class creation — use sparingly
- Operator overloading (
__add__,__mul__, etc.) customizes arithmetic - Properties are simpler than descriptors for single-use cases