Understanding descriptors requires a strict vocabulary because multiple objects interact at the exact same time:
- Descriptor Class: A class implementing the descriptor protocol (
__get__,__set__, or__delete__). - Descriptor Instance: An instance of the descriptor class, assigned as a class attribute of the managed class.
- Managed Class: The class where the descriptor instances are declared as class attributes.
- Managed Instance: One specific instance of the managed class.
- Storage Name: The string name of the attribute where the descriptor stores its data in the managed instance's
__dict__.
The presence of __set__ completely changes how Python handles the descriptor during the attribute lookup chain (Chapter 22).
- Overriding Descriptors (Data Descriptors): They implement
__set__. Because Python checks them before looking in the instance__dict__, they unconditionally intercept all reads and writes. A user cannot overwrite them. The@propertydecorator creates an overriding descriptor. - Non-Overriding Descriptors (Non-Data Descriptors): They only implement
__get__. Because Python checks the instance__dict__before checking non-overriding descriptors, if a user typesobj.x = 5, Python just creates a raw variableobj.__dict__['x'] = 5. The next time you callobj.x, you get 5. The descriptor is completely shadowed and hidden forever. Standard Python functions and@cached_propertyare non-overriding descriptors.
Before Python 3.6, descriptors were blind. If you typed weight = Quantity(), the Quantity instance had no idea it was called "weight". Developers had to manually pass the name: weight = Quantity("weight").
Python 3.6 introduced __set_name__(self, owner, name). When the managed class is parsed, Python automatically calls this method on every descriptor it finds, handing it the exact variable name it was bound to.