Descriptors and Attribute Access Order in Python
字数 2399
更新时间 2025-11-08 23:27:58

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:

  1. 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
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

  1. 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
class MyClass:
    pass

obj = MyClass()
obj.attr = 'Instance attribute value'
print(obj.attr)  # Output: Instance attribute value

4. Non-Data Descriptor Lookup

  1. 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
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

  1. 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

  1. 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:

  1. Data descriptors (highest priority)
  2. Instance dictionary (obj.__dict__)
  3. Non-data descriptors
  4. Class attributes (cls.__dict__)
  5. Parent class inheritance chain (according to MRO)
  6. __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.

相似文章
相似文章
 全屏