Do Not Make This Mistake with Python Descriptors
Dmitry Posokhov
Posted on July 22, 2024
Descriptors provide a powerful way to manage attribute access (like as Django models style 😀), but when used in multiple instances of a class, they can lead to unexpected behavior. In this short article, I'll explain what happened in my work and how to avoid this common issue.
What are Descriptors?
Descriptors are special objects in Python that manage the access to another object's attributes. They implement the methods __get__
, __set__
, and __delete__
. When used correctly, descriptors can greatly enhance the functionality of a class.
The Problem I Noticed:
Recently, while reviewing a colleague's code, I noticed an issue with use of descriptors to manage an attribute across multiple instances of a class. Here’s a simplified version of code:
class MyDescriptor:
def __init__(self):
self.value = None
def __get__(self, instance, owner):
return self.value
def __set__(self, instance, value):
self.value = value
So what is wrong?
Try to use this descriptor in class:
class MyClass:
attr = MyDescriptor()
Then create a couple of instances of this class:
obj1 = MyClass()
obj2 = MyClass()
And so what? Ok, try to set values to 'attr' in both instances:
obj1.attr = 10
obj2.attr = 20
So, what we see when we try to use 'attr' from first one instance:
print(obj1.attr) # Expected: 10, but it was 20
print(obj2.attr) # Expected: 20
Surprisingly (or not 😏), changing the value in obj1 also changed the value in obj2. We are expecting each instance to have its own value, but it turned out that the same descriptor instance was shared across all instances of the class.
Understanding the Issue:
The issue arises because the descriptor is a class-level attribute. This means that the same descriptor instance is used for all instances of the class. Consequently, setting the value on one instance affects all other instances.
The Solution:
To ensure that each instance has its own value, we need to store the value separately for each instance. This can be achieved using a dictionary to hold values for each instance. Here’s the revised code:
class MyDescriptor:
def __init__(self):
self._values = {}
def __get__(self, instance, owner):
if instance is None:
return self
return self._values.get(instance, None)
def __set__(self, instance, value):
self._values[id(instance)] = value
class MyClass:
attr = MyDescriptor()
obj1 = MyClass()
obj2 = MyClass()
obj1.attr = 10
obj2.attr = 20
print(obj1.attr) # Now: 10
print(obj2.attr) # Now: 20
By using a dictionary to store the values, each instance now correctly maintains its own value without affecting others.
Conclusion:
Descriptors are a powerful tool in Python, but they come with their own set of challenges. When using descriptors, especially in multiple instances of a class, it’s crucial to ensure that each instance maintains its own state. Using a separate storage mechanism like a dictionary can help avoid unexpected behavior.
Posted on July 22, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.