Descriptors in Python are a way to manage the behavior of attributes in classes. They allow you to define how attributes are accessed, modified, or deleted in a class. A descriptor is any object that implements any of the following methods:
__get__(self, instance, owner)
__set__(self, instance, value)
__delete__(self, instance)
Here are 10 Python code snippets that demonstrate different use cases for implementing custom descriptors:
1. Basic Descriptor Example
A simple descriptor that manages the access of an attribute.
Copy
class MyDescriptor:
def __get__(self, instance, owner):
return f"Getting attribute from {owner}"
def __set__(self, instance, value):
print(f"Setting attribute to {value}")
def __delete__(self, instance):
print("Deleting attribute")
class MyClass:
attr = MyDescriptor()
# Example usage
obj = MyClass()
obj.attr = 10
print(obj.attr)
del obj.attr
Explanation:
__get__: Defines behavior when accessing the attribute.
__set__: Defines behavior when assigning a value to the attribute.
__delete__: Defines behavior when deleting the attribute.
2. Read-Only Descriptor
A descriptor that prevents modifying an attribute after it has been set.
Copy
Explanation:
The attribute is read-only because the __set__ method raises an error when trying to modify it.
3. Logging Access to Attributes
A descriptor that logs every access to the attribute.
Copy
Explanation:
__set_name__: Sets the attribute name.
The __get__ and __set__ methods print logs when the attribute is accessed or modified.
4. Property-Like Descriptor
A descriptor that works like a property but with custom logic for getting, setting, and deleting.
Copy
Explanation:
MyProperty acts like a property but gives you full control over the getter and setter methods.
5. Validating Attribute Values with Descriptors
A descriptor that validates the value before allowing it to be set.
Copy
Explanation:
The IntegerDescriptor validates that the attribute value is an integer before setting it.
6. Descriptor with Default Values
A descriptor that provides a default value if the attribute is not set.
Copy
Explanation:
DefaultDescriptor provides a default value when the attribute is accessed before being set.
7. Type-Checking Descriptor
A descriptor that ensures the value matches a specified type.
Copy
Explanation:
TypeCheckingDescriptor ensures that only values of a specified type can be assigned to the attribute.
8. Deleting an Attribute via Descriptor
A descriptor that defines custom behavior when an attribute is deleted.
Copy
Explanation:
__delete__: Custom logic is defined for deleting the attribute.
9. Lazy-Loading Attribute
A descriptor that loads an attribute value only when it's needed (lazy loading).
Copy
Explanation:
The attribute is loaded only when it is accessed for the first time.
10. Counter Attribute Descriptor
A descriptor that keeps track of how many times an attribute has been accessed.
Copy
Explanation:
CounterDescriptor tracks how many times the attribute has been accessed.
These snippets showcase the flexibility and power of descriptors in Python. Descriptors can be used for a wide variety of purposes, such as enforcing rules on attribute values, adding custom behavior to attribute access, and more.
class LoggingDescriptor:
def __get__(self, instance, owner):
print(f"Accessing {instance.__class__.__name__} attribute")
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
print(f"Setting value {value} for {instance.__class__.__name__} attribute")
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class MyClass:
attr = LoggingDescriptor()
# Example usage
obj = MyClass()
obj.attr = 10
print(obj.attr)
class MyProperty:
def __init__(self, fget=None, fset=None):
self.fget = fget
self.fset = fset
def __get__(self, instance, owner):
if self.fget is None:
raise AttributeError("Getter not defined")
return self.fget(instance)
def __set__(self, instance, value):
if self.fset is None:
raise AttributeError("Setter not defined")
self.fset(instance, value)
class MyClass:
def __init__(self, name):
self._name = name
def get_name(self):
return self._name
def set_name(self, value):
self._name = value
name = MyProperty(get_name, set_name)
# Example usage
obj = MyClass("John")
print(obj.name)
obj.name = "Doe"
print(obj.name)
class IntegerDescriptor:
def __get__(self, instance, owner):
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError("Attribute must be an integer")
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class MyClass:
num = IntegerDescriptor()
# Example usage
obj = MyClass()
obj.num = 42
print(obj.num)
obj.num = "string" # Raises ValueError
class DefaultDescriptor:
def __init__(self, default):
self.default = default
def __get__(self, instance, owner):
return instance.__dict__.get(self.name, self.default)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class MyClass:
attr = DefaultDescriptor(default="Default Value")
# Example usage
obj = MyClass()
print(obj.attr) # Outputs: Default Value
obj.attr = "New Value"
print(obj.attr) # Outputs: New Value
class TypeCheckingDescriptor:
def __init__(self, expected_type):
self.expected_type = expected_type
def __get__(self, instance, owner):
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if not isinstance(value, self.expected_type):
raise TypeError(f"Expected value of type {self.expected_type}")
instance.__dict__[self.name] = value
def __set_name__(self, owner, name):
self.name = name
class MyClass:
attr = TypeCheckingDescriptor(str)
# Example usage
obj = MyClass()
obj.attr = "Hello"
print(obj.attr)
obj.attr = 10 # Raises TypeError
class DeletableDescriptor:
def __get__(self, instance, owner):
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __delete__(self, instance):
print(f"Deleting {self.name} attribute")
del instance.__dict__[self.name]
def __set_name__(self, owner, name):
self.name = name
class MyClass:
attr = DeletableDescriptor()
# Example usage
obj = MyClass()
obj.attr = 42
del obj.attr
class LazyLoadDescriptor:
def __init__(self, load_func):
self.load_func = load_func
def __get__(self, instance, owner):
if self.name not in instance.__dict__:
instance.__dict__[self.name] = self.load_func()
return instance.__dict__[self.name]
def __set_name__(self, owner, name):
self.name = name
class MyClass:
def __init__(self):
self._data = "Expensive Data"
def load_data(self):
print("Loading data...")
return self._data
data = LazyLoadDescriptor(load_data)
# Example usage
obj = MyClass()
print(obj.data) # Loads data on first access