Python Descriptors
This guide covers Python descriptors, their types, and how to use them effectively.
Table of Contents
Introduction
What are Descriptors?
Descriptors are objects that define how attributes are accessed, set, or deleted. They provide a way to customize attribute access and implement reusable behavior.
Key Concepts
- Attribute access control
- Reusable behavior
- Method binding
- Property-like functionality
Descriptor Protocol
Basic Descriptor
| class Descriptor:
def __get__(self, instance, owner):
if instance is None:
return self
print(f"Getting {self.__class__.__name__}")
return instance._value
def __set__(self, instance, value):
print(f"Setting {self.__class__.__name__}")
instance._value = value
def __delete__(self, instance):
print(f"Deleting {self.__class__.__name__}")
del instance._value
|
Descriptor with Name
| class NamedDescriptor:
def __init__(self, name=None):
self.name = name
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
|
Descriptor Types
Non-data Descriptor
| class NonDataDescriptor:
def __get__(self, instance, owner):
if instance is None:
return self
return "Non-data descriptor value"
|
Data Descriptor
| class DataDescriptor:
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
|
Lazy Property
| class LazyProperty:
def __init__(self, function):
self.function = function
self.name = function.__name__
def __get__(self, instance, owner):
if instance is None:
return self
value = self.function(instance)
setattr(instance, self.name, value)
return value
|
Common Patterns
Validated Property
| class ValidatedProperty:
def __init__(self, name, validator):
self.name = name
self.validator = validator
def __get__(self, instance, owner):
if instance is None:
return self
return instance.__dict__[self.name]
def __set__(self, instance, value):
if not self.validator(value):
raise ValueError(f"Invalid value: {value}")
instance.__dict__[self.name] = value
|
Cached Property
| class CachedProperty:
def __init__(self, function):
self.function = function
self.name = function.__name__
def __get__(self, instance, owner):
if instance is None:
return self
if self.name not in instance.__dict__:
instance.__dict__[self.name] = self.function(instance)
return instance.__dict__[self.name]
|
Method Binding
| from types import MethodType
class Method:
def __init__(self, name):
self.name = name
def __call__(self, instance, *args, **kwargs):
print(f"{self.name}: {instance} called with {args} and {kwargs}")
def __get__(self, instance, owner):
if instance is None:
return self
return MethodType(self, instance)
|
Best Practices
- Descriptor Design
- Keep descriptors focused and single-purpose
- Use clear and descriptive names
-
Document behavior clearly
-
Performance
- Avoid unnecessary attribute access
- Use appropriate caching strategies
-
Consider memory usage
-
Error Handling
- Provide clear error messages
- Handle edge cases
-
Validate input appropriately
-
Code Organization
- Group related descriptors
- Maintain consistent style
-
Use type hints
-
Data Model
- Properties
- Metaclasses
- Class Decorators