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
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
Pros
Cons
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.