Descriptors and Attribute Access Order in Python
Descriptors are a powerful feature in Python that allow objects to customize the behavior of attribute access. Understanding how descriptors interact with regular attribute access rules is crucial.
1. Basics of the Descriptor Protocol
A descriptor is a class that implements specific protocol methods, which include:
__get__(self, obj, type=None): Called when getting the attribute's value__set__(self, obj, value): Called when setting the attribute's value__delete__(self, obj): Called when deleting the attribute
Based on the implemented protocol methods, descriptors are divided into two categories:
- Data descriptor: Implements
__set__or__delete__methods - Non-data descriptor: Implements only the
__get__method
2. Priority Order of Attribute Access
When accessing an attribute through an instance, Python looks it up in the following order:
- Data descriptors have the highest priority
- If the attribute is a data descriptor in the class, directly call the descriptor's
__get__method - Data descriptors override attributes of the same name in the instance dictionary
- If the attribute is a data descriptor in the class, directly call the descriptor's
class DataDescriptor:
def __get__(self, obj, type=None):
return "Value from data descriptor"
def __set__(self, obj, value):
pass
class MyClass:
attr = DataDescriptor() # Data descriptor
obj = MyClass()
obj.__dict__['attr'] = 'Value in instance dictionary'
print(obj.attr) # Output: Value from data descriptor
3. Instance Dictionary Lookup
- Instance dictionary lookup
- If the attribute is not a data descriptor, look it up in the instance's
__dict__ - This is the most common case of attribute access
- If the attribute is not a data descriptor, look it up in the instance's
class MyClass:
pass
obj = MyClass()
obj.attr = 'Instance attribute value'
print(obj.attr) # Output: Instance attribute value
4. Non-Data Descriptor Lookup
- Non-data descriptor lookup
- If there is a non-data descriptor with the same name in the class, call its
__get__method - The priority of non-data descriptors is lower than that of instance attributes
- If there is a non-data descriptor with the same name in the class, call its
class NonDataDescriptor:
def __get__(self, obj, type=None):
return "Value from non-data descriptor"
class MyClass:
attr = NonDataDescriptor() # Non-data descriptor
obj = MyClass()
print(obj.attr) # Output: Value from non-data descriptor
obj.attr = 'Instance attribute value' # Now the instance dictionary has this attribute
print(obj.attr) # Output: Instance attribute value (overrides the non-data descriptor)
5. Class Attribute Lookup
- Class attribute lookup
- If not found in either the instance or descriptors, look it up among the class attributes
- Includes class variables, methods, etc.
class MyClass:
class_attr = 'Class attribute value'
obj = MyClass()
print(obj.class_attr) # Output: Class attribute value
6. Inheritance Chain Lookup
- Inheritance chain lookup
- If not found in the current class, search the parent classes in MRO order
- Includes descriptors and class attributes in parent classes
7. Summary of the Complete Lookup Order
The complete priority order for attribute access is:
- Data descriptors (highest priority)
- Instance dictionary (
obj.__dict__) - Non-data descriptors
- Class attributes (
cls.__dict__) - Parent class inheritance chain (according to MRO)
__getattr__method (if defined)
8. Practical Application Example
class DataDescriptor:
def __get__(self, obj, type=None):
return "Data descriptor"
def __set__(self, obj, value):
print("Setting data descriptor")
class NonDataDescriptor:
def __get__(self, obj, type=None):
return "Non-data descriptor"
class Example:
data_desc = DataDescriptor() # Data descriptor
non_data_desc = NonDataDescriptor() # Non-data descriptor
class_attr = "Class attribute" # Ordinary class attribute
# Testing access order
obj = Example()
# 1. Data descriptor first
print(obj.data_desc) # Output: Data descriptor
# 2. Instance attribute overrides non-data descriptor
print(obj.non_data_desc) # Output: Non-data descriptor
obj.non_data_desc = "Instance attribute"
print(obj.non_data_desc) # Output: Instance attribute
# 3. Class attribute access
print(obj.class_attr) # Output: Class attribute
Understanding this priority order is very important for writing advanced Python code, designing frameworks, and debugging attribute-related issues.