__slots__.
What is a Descriptor?
A descriptor is any object that defines__get__(), __set__(), or __delete__() methods:
Simple Example
class Ten:
def __get__(self, obj, objtype=None):
return 10
class A:
x = 5 # Regular class attribute
y = Ten() # Descriptor instance
Dynamic Lookups
Descriptors run computations instead of returning constants:Managed Attributes
Control access to instance data with validation:Customized Names
Use__set_name__() to automatically capture attribute names:
Data Validation
from abc import ABC, abstractmethod
class Validator(ABC):
def __set_name__(self, owner, name):
self.private_name = '_' + name
def __get__(self, obj, objtype=None):
return getattr(obj, self.private_name)
def __set__(self, obj, value):
self.validate(value)
setattr(obj, self.private_name, value)
@abstractmethod
def validate(self, value):
pass
class OneOf(Validator):
def __init__(self, *options):
self.options = set(options)
def validate(self, value):
if value not in self.options:
raise ValueError(
f'Expected {value!r} to be one of {self.options!r}'
)
class Number(Validator):
def __init__(self, minvalue=None, maxvalue=None):
self.minvalue = minvalue
self.maxvalue = maxvalue
def validate(self, value):
if not isinstance(value, (int, float)):
raise TypeError(f'Expected {value!r} to be an int or float')
if self.minvalue is not None and value < self.minvalue:
raise ValueError(
f'Expected {value!r} to be at least {self.minvalue!r}'
)
if self.maxvalue is not None and value > self.maxvalue:
raise ValueError(
f'Expected {value!r} to be no more than {self.maxvalue!r}'
)
class String(Validator):
def __init__(self, minsize=None, maxsize=None, predicate=None):
self.minsize = minsize
self.maxsize = maxsize
self.predicate = predicate
def validate(self, value):
if not isinstance(value, str):
raise TypeError(f'Expected {value!r} to be a str')
if self.minsize is not None and len(value) < self.minsize:
raise ValueError(
f'Expected {value!r} to be no smaller than {self.minsize!r}'
)
if self.maxsize is not None and len(value) > self.maxsize:
raise ValueError(
f'Expected {value!r} to be no bigger than {self.maxsize!r}'
)
if self.predicate is not None and not self.predicate(value):
raise ValueError(
f'Expected {self.predicate} to be true for {value!r}'
)
class Component:
name = String(minsize=3, maxsize=10, predicate=str.isupper)
kind = OneOf('wood', 'metal', 'plastic')
quantity = Number(minvalue=0)
def __init__(self, name, kind, quantity):
self.name = name
self.kind = kind
self.quantity = quantity
Descriptor Types
Data vs Non-Data Descriptors
Data descriptors define both__get__() and __set__():
- Take precedence over instance dictionary
- Used for managed attributes
__get__():
- Instance dictionary takes precedence
- Used for methods and functions
Property Implementation
Here’s how Python’sproperty() works under the hood:
Common Use Cases
Lazy Properties
Compute values only when needed:Type Checking
Best Practices
When to Use Descriptors:
- ✅ Repeated validation logic across multiple classes
- ✅ Computed attributes with caching
- ✅ Attribute access logging/monitoring
- ✅ Type checking and coercion
- ❌ Simple properties (use
@propertyinstead) - ❌ One-off custom behavior (use
__getattribute__override)
Summary
Key points about descriptors:- Define
__get__(),__set__(), or__delete__()to create a descriptor - Use
__set_name__()to capture the attribute name automatically - Data descriptors override instance dictionary
- Non-data descriptors are overridden by instance dictionary
- Descriptors power properties, methods, classmethod, staticmethod, and slots
