Descriptors Magic
Understanding Descriptors
Descriptors are a powerful mechanism in Python that define how attribute access, modification, and deletion are implemented at the class level.
Descriptor Protocol Methods
class Descriptor:
def __get__(self, instance, owner):
"""Retrieve the attribute value"""
pass
def __set__(self, instance, value):
"""Set the attribute value"""
pass
def __delete__(self, instance):
"""Delete the attribute"""
pass
Descriptor Types
Descriptor Type |
Implemented Methods |
Behavior |
Non-Data Descriptor |
__get__ |
Read-only access |
Data Descriptor |
__get__ , __set__ |
Full control |
Full Descriptor |
__get__ , __set__ , __delete__ |
Complete attribute management |
Practical Descriptor Example
class Validated:
def __init__(self, min_value=None, max_value=None):
self.min_value = min_value
self.max_value = max_value
def __set_name__(self, owner, name):
self.name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.name, None)
def __set__(self, instance, value):
if self.min_value is not None and value < self.min_value:
raise ValueError(f"Value must be at least {self.min_value}")
if self.max_value is not None and value > self.max_value:
raise ValueError(f"Value must be at most {self.max_value}")
setattr(instance, self.name, value)
class Student:
age = Validated(min_value=0, max_value=120)
def __init__(self, name, age):
self.name = name
self.age = age
## LabEx Tip: Descriptors provide powerful attribute validation
student = Student("John", 25)
try:
student.age = -5 ## Raises ValueError
except ValueError as e:
print(e)
Descriptor Lookup Mechanism
graph TD
A[Attribute Access] --> B{Is Descriptor?}
B --> |Yes| C{Data Descriptor?}
B --> |No| D[Normal Attribute Lookup]
C --> |Yes| E[Use Descriptor Methods]
C --> |No| F{Instance Attribute Exists?}
F --> |Yes| G[Use Instance Attribute]
F --> |No| H[Use Descriptor Methods]
Advanced Descriptor Techniques
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
class DataProcessor:
@LazyProperty
def complex_calculation(self):
## Simulate expensive computation
import time
time.sleep(2)
return sum(range(1000000))
## Lazy loading demonstration
processor = DataProcessor()
print(processor.complex_calculation) ## Computed only once
Key Use Cases
- Attribute validation
- Lazy loading
- Computed properties
- Access control
- Type checking
- Descriptors have slight overhead
- Best used for complex attribute management
- Avoid overusing for simple operations
Best Practices
- Keep descriptor logic minimal
- Use for cross-cutting concerns
- Prefer composition when possible
- Document descriptor behavior clearly