2. OOP Advanced: Descriptors, Properties, Metaclasses
Leverage Python's object model to build concise, powerful abstractions with explicit control over attribute access and class creation.
Question: What is the descriptor protocol in Python?
Answer: The descriptor protocol allows an object's attribute access to be customized. An object that implements any of the __get__
, __set__
, or __delete__
methods is considered a descriptor and can control the behavior of attributes in an owner class.
Explanation: This is a powerful, low-level feature that underpins much of Python's object model. Properties, methods, @staticmethod
, and @classmethod
are all implemented using descriptors. It's the mechanism that allows attribute access to do more than just retrieve a value from a dictionary.
class Positive:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, owner=None):
if obj is None: return self
return obj.__dict__[self.name]
def __set__(self, obj, value):
if value <= 0: raise ValueError("must be positive")
obj.__dict__[self.name] = value
class Product:
price = Positive()
def __init__(self, price: float):
self.price = price
Question: What is a metaclass in Python and what is a practical use case?
Answer: A metaclass is the "class of a class"; it defines how a class is created. While you would rarely need to create a custom metaclass in application code, it's a powerful tool used in frameworks to implement patterns like registration, validation, or ORM model creation.
Explanation: When you define a class, Python uses a metaclass (by default, type
) to create that class object. By creating a custom metaclass, you can intercept the class creation process to automatically modify the class. ORMs like SQLAlchemy and Django use metaclasses extensively to create database models from class definitions.
class Registry(type):
registry = {}
def __new__(mcls, name, bases, ns):
cls = super().__new__(mcls, name, bases, ns)
if name != "Base":
mcls.registry[name] = cls
return cls
class Base(metaclass=Registry):
pass
class Foo(Base):
pass
# Registry.registry => {"Foo": <class Foo>}
Question: How would you implement a decorator from scratch, and what is the purpose of
functools.wraps
?
Answer: A decorator is a function that takes another function, wraps it in an inner function to add functionality, and returns the inner function. functools.wraps
is a helper decorator that should be applied to the inner function to preserve the original function's metadata (like its name and docstring).
Explanation: Without @wraps
, the decorated function would lose its original identity, which can cause issues with introspection and debugging. The example below shows a simple timing decorator.
from functools import wraps
import time
def timing(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return fn(*args, **kwargs)
finally:
end = time.perf_counter()
print(f"{fn.__name__} took {end - start:.4f}s")
return wrapper
@timing
def do_work():
time.sleep(0.1)
Question: How do abstract base classes (ABCs) compare to
Protocol
s, and when use each?
Answer: ABCs enforce nominal subtyping via inheritance and can include default behavior. Protocol
s enable structural typing: any class with the right shape matches.
Explanation: Use ABCs when you need shared implementation or isinstance
checks without @runtime_checkable
. Use Protocol
for flexible libraries that accept any conforming type.
from abc import ABC, abstractmethod
class Repo(ABC):
@abstractmethod
def get(self, key: str) -> str: ...
Question: When should you use
@property
vsfunctools.cached_property
?
Answer: Use @property
for cheap, computed attributes. Use cached_property
for expensive, immutable computations that should be evaluated once per instance.
Explanation: cached_property
caches the value; clear or recompute when underlying state changes.
from functools import cached_property
class Report:
def __init__(self, data): self.data = data
@cached_property
def summary(self):
return expensive_reduce(self.data)
Question: What is
__init_subclass__
and a practical use?
Answer: __init_subclass__
is a hook called on a base whenever a subclass is created, letting you enforce or register things at subclass definition time.
Explanation: Useful for auto-registration, validation, or enforcing required attributes.
class Plugin:
registry = {}
def __init_subclass__(cls, name: str, **kw):
super().__init_subclass__(**kw)
Plugin.registry[name] = cls
class CSVPlugin(Plugin, name="csv"): pass
# Plugin.registry["csv"] is CSVPlugin