Python > Advanced Python Concepts > Descriptors > Understanding Descriptors (`__get__`, `__set__`, `__delete__`)

Basic Descriptor Example: Validating Attribute Type

This snippet demonstrates a simple descriptor that validates the type of an attribute being assigned. It ensures that only integers are assigned to a specific attribute of a class.

Code Implementation

This code defines a descriptor class `Integer`. The `__set_name__` method is called when the descriptor is assigned to a class attribute, storing the attribute's name. The `__get__` method retrieves the value of the attribute. The `__set__` method validates that the assigned value is an integer before storing it in the instance's `__dict__`. The `__delete__` method deletes the attribute from the instance's `__dict__`. `MyClass` uses this descriptor for its `age` attribute. Note how `__set_name__` is used in conjunction with the descriptor protocol.

class Integer:
    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):
        if not isinstance(value, int):
            raise TypeError(f'{self.name} must be an integer')
        instance.__dict__[self.name] = value

    def __delete__(self, instance):
        del instance.__dict__[self.name]

class MyClass:
    age = Integer()

    def __init__(self, age):
        self.age = age

# Example usage:
obj = MyClass(30)
print(obj.age)  # Output: 30

obj.age = 35
print(obj.age)  # Output: 35

# Trying to assign a non-integer value:
# obj.age = 'forty'  # Raises TypeError: age must be an integer

del obj.age
# print(obj.age) # Raises AttributeError: 'MyClass' object has no attribute 'age'

Concepts Behind the Snippet

Descriptors provide a way to customize attribute access (getting, setting, deleting) by implementing the descriptor protocol methods: `__get__`, `__set__`, and `__delete__`. They allow you to inject logic into the process of attribute manipulation, making them powerful tools for validation, lazy loading, and other advanced programming techniques. The descriptor is an object of a class that defines at least one of these methods. When an attribute is accessed on a class or instance, Python checks if the attribute is a descriptor. If it is, the corresponding descriptor method is called.

Real-Life Use Case

Consider a scenario where you need to manage database connections. A descriptor can be used to lazily establish a connection only when an attribute requiring the connection is accessed. This avoids unnecessary connection overhead and improves performance.

class DatabaseConnection:
    def __get__(self, instance, owner):
        if instance is None:
            return self
        if '_connection' not in instance.__dict__:
            print('Connecting to the database...')
            instance._connection = self.connect()
        return instance._connection

    def connect(self):
        # Simulate a database connection
        return 'Database Connection'

class MyModel:
    db = DatabaseConnection()

    def __init__(self):
        pass

model = MyModel()
print('Accessing database connection...')
db_connection = model.db # Connection established here
print(db_connection)
print('Accessing database connection again...')
db_connection = model.db # No new connection made

Best Practices

  • Always use `__set_name__` to store the attribute name for clarity and maintainability.
  • Handle the case where the descriptor is accessed directly from the class (instance is None in `__get__`).
  • Consider using a cache (like the instance's `__dict__`) to store the value after the first access, especially for expensive computations.

Interview Tip

Be prepared to explain the descriptor protocol methods (`__get__`, `__set__`, `__delete__`, and `__set_name__`) and how they are invoked when an attribute is accessed. Also, be ready to provide examples of how descriptors can be used to solve real-world problems, such as data validation or lazy loading.

When to Use Them

Use descriptors when you need fine-grained control over attribute access and modification. They are particularly useful for implementing read-only attributes, validating data types, and performing lazy initialization.

Memory Footprint

Descriptors themselves typically have a small memory footprint. However, their use can affect the memory footprint of the classes they are used in. For example, using a descriptor for lazy loading can delay the allocation of memory for an attribute until it is actually needed. Improper use, like caching descriptor objects inefficiently, can lead to increased memory consumption.

Alternatives

  • Properties: Use properties for simpler attribute access control, such as basic getter and setter logic. Properties are a higher-level abstraction over descriptors, suitable for simpler use cases.
  • Metaclasses: Metaclasses can be used to modify class creation behavior, including adding or modifying attributes. They are more powerful than descriptors but also more complex.

Pros

  • Code Reusability: Descriptors can be reused across multiple classes.
  • Encapsulation: Descriptors allow you to encapsulate attribute access logic.
  • Flexibility: Descriptors offer a high degree of flexibility in customizing attribute behavior.

Cons

  • Complexity: Descriptors can be more complex to understand and implement than simpler alternatives like properties.
  • Performance Overhead: Descriptor access can introduce a slight performance overhead compared to direct attribute access.

FAQ

  • What happens if I don't define all the descriptor methods?

    If you only define `__get__`, the descriptor is considered a 'non-data descriptor' or 'read-only descriptor'. Assignment to the attribute will bypass the descriptor and directly set the value in the instance's `__dict__`. If you define `__set__` or `__delete__`, the descriptor becomes a 'data descriptor' and will override the default attribute assignment and deletion behavior. The presence of `__set__` defines a data descriptor.
  • Can I use descriptors for methods?

    Yes, descriptors can also be used for methods. However, the behavior is slightly different. When a descriptor is a method, it's usually about controlling how the method is bound to an object. Think of `@classmethod` or `@staticmethod`, those are actually descriptors.