Descriptors & Attribute Hooks
In Python, attribute access (obj.attr) is not a simple dictionary lookup. It is controlled by a powerful mechanism called the Descriptor Protocol. Descriptors are the hidden gears behind properties, methods, staticmethod, and classmethod.
Understanding descriptors allows you to create reusable, intelligent attributes that can validate data, perform lazy calculation, or log access automatically.
1. The Descriptor Protocol
Section titled “1. The Descriptor Protocol”An object is a descriptor if it defines any of these three methods:
__get__(self, instance, owner): Called when you read the attribute.__set__(self, instance, value): Called when you write to the attribute.__delete__(self, instance): Called when youdelthe attribute.
class NonNegative: """A descriptor that ensures a value is never below zero.""" def __init__(self, name): self.name = name
def __get__(self, instance, owner): # 'instance' is the object using the descriptor (e.g., the car) # 'owner' is the class (e.g., Car) return instance.__dict__.get(self.name, 0)
def __set__(self, instance, value): if value < 0: raise ValueError(f"{self.name} cannot be negative!") instance.__dict__[self.name] = value
class Car: # Descriptors MUST be defined at the class level speed = NonNegative("speed")
c = Car()c.speed = 100# c.speed = -20 # Raises ValueError2. Data vs. Non-Data Descriptors
Section titled “2. Data vs. Non-Data Descriptors”This distinction is crucial for understanding how Python resolves names.
Data Descriptors
Section titled “Data Descriptors”Define both __get__ and __set__. They take precedence over an object’s __dict__. Even if you try to manually set obj.attr = 10, Python will still call the descriptor’s __set__ method.
Non-Data Descriptors
Section titled “Non-Data Descriptors”Define only __get__. They are used for methods. If an object has a variable in its __dict__ with the same name, the instance variable wins. This is how Python allows you to “overwrite” a method on a specific instance.
3. The Machinery of @property
Section titled “3. The Machinery of @property”The @property decorator is actually just a convenient way to create a descriptor.
When you write:
@propertydef x(self): return self._xPython creates a property object (a data descriptor) that stores your function and calls it inside its own __get__ method.
4. Under the Hood: Attribute Lookup Order
Section titled “4. Under the Hood: Attribute Lookup Order”When you run x = obj.attr, Python follows this high-priority search:
- Data Descriptor: Check if
attris a data descriptor in the class (or its parents). - Instance Dictionary: Check if
attrexists inobj.__dict__. - Non-Data Descriptor: Check if
attris a non-data descriptor (like a method) in the class. - Class Attribute: Check if
attris a simple variable in the class. __getattr__: If all else fails, call the__getattr__hook if it exists.
5. Summary Table
Section titled “5. Summary Table”| Method | Role | Called When… |
|---|---|---|
__get__ | Retrieval | value = obj.attr |
__set__ | Assignment | obj.attr = value |
__delete__ | Deletion | del obj.attr |
__set_name__ | Identification | Called when the class is defined (Py 3.6+). |