Skip to content

Commit d4cfbfe

Browse files
Add member objects to the descriptor howto guide (GH-23084) (GH-23090)
1 parent 81dd2c0 commit d4cfbfe

File tree

1 file changed

+156
-0
lines changed

1 file changed

+156
-0
lines changed

Doc/howto/descriptor.rst

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,3 +990,159 @@ For example, a classmethod and property could be chained together::
990990
@property
991991
def __doc__(cls):
992992
return f'A doc for {cls.__name__!r}'
993+
994+
Member Objects
995+
--------------
996+
997+
When a class defines ``__slots__``, it replaces instance dictionaries with a
998+
fixed-length array of slot values. From a user point of view that has
999+
several effects:
1000+
1001+
1. Provides immediate detection of bugs due to misspelled attribute
1002+
assignments. Only attribute names specified in ``__slots__`` are allowed::
1003+
1004+
class Vehicle:
1005+
__slots__ = ('id_number', 'make', 'model')
1006+
1007+
>>> auto = Vehicle()
1008+
>>> auto.id_nubmer = 'VYE483814LQEX'
1009+
Traceback (most recent call last):
1010+
...
1011+
AttributeError: 'Vehicle' object has no attribute 'id_nubmer'
1012+
1013+
2. Helps create immutable objects where descriptors manage access to private
1014+
attributes stored in ``__slots__``::
1015+
1016+
class Immutable:
1017+
1018+
__slots__ = ('_dept', '_name') # Replace instance dictionary
1019+
1020+
def __init__(self, dept, name):
1021+
self._dept = dept # Store to private attribute
1022+
self._name = name # Store to private attribute
1023+
1024+
@property # Read-only descriptor
1025+
def dept(self):
1026+
return self._dept
1027+
1028+
@property
1029+
def name(self): # Read-only descriptor
1030+
return self._name
1031+
1032+
mark = Immutable('Botany', 'Mark Watney') # Create an immutable instance
1033+
1034+
3. Saves memory. On a 64-bit Linux build, an instance with two attributes
1035+
takes 48 bytes with ``__slots__`` and 152 bytes without. This `flyweight
1036+
design pattern <https://en.wikipedia.org/wiki/Flyweight_pattern>`_ likely only
1037+
matters when a large number of instances are going to be created.
1038+
1039+
4. Blocks tools like :func:`functools.cached_property` which require an
1040+
instance dictionary to function correctly::
1041+
1042+
from functools import cached_property
1043+
1044+
class CP:
1045+
__slots__ = () # Eliminates the instance dict
1046+
1047+
@cached_property # Requires an instance dict
1048+
def pi(self):
1049+
return 4 * sum((-1.0)**n / (2.0*n + 1.0)
1050+
for n in reversed(range(100_000)))
1051+
1052+
>>> CP().pi
1053+
Traceback (most recent call last):
1054+
...
1055+
TypeError: No '__dict__' attribute on 'CP' instance to cache 'pi' property.
1056+
1057+
It's not possible to create an exact drop-in pure Python version of
1058+
``__slots__`` because it requires direct access to C structures and control
1059+
over object memory allocation. However, we can build a mostly faithful
1060+
simulation where the actual C structure for slots is emulated by a private
1061+
``_slotvalues`` list. Reads and writes to that private structure are managed
1062+
by member descriptors::
1063+
1064+
class Member:
1065+
1066+
def __init__(self, name, clsname, offset):
1067+
'Emulate PyMemberDef in Include/structmember.h'
1068+
# Also see descr_new() in Objects/descrobject.c
1069+
self.name = name
1070+
self.clsname = clsname
1071+
self.offset = offset
1072+
1073+
def __get__(self, obj, objtype=None):
1074+
'Emulate member_get() in Objects/descrobject.c'
1075+
# Also see PyMember_GetOne() in Python/structmember.c
1076+
return obj._slotvalues[self.offset]
1077+
1078+
def __set__(self, obj, value):
1079+
'Emulate member_set() in Objects/descrobject.c'
1080+
obj._slotvalues[self.offset] = value
1081+
1082+
def __repr__(self):
1083+
'Emulate member_repr() in Objects/descrobject.c'
1084+
return f'<Member {self.name!r} of {self.clsname!r}>'
1085+
1086+
The :meth:`type.__new__` method takes care of adding member objects to class
1087+
variables. The :meth:`object.__new__` method takes care of creating instances
1088+
that have slots instead of a instance dictionary. Here is a rough equivalent
1089+
in pure Python::
1090+
1091+
class Type(type):
1092+
'Simulate how the type metaclass adds member objects for slots'
1093+
1094+
def __new__(mcls, clsname, bases, mapping):
1095+
'Emuluate type_new() in Objects/typeobject.c'
1096+
# type_new() calls PyTypeReady() which calls add_methods()
1097+
slot_names = mapping.get('slot_names', [])
1098+
for offset, name in enumerate(slot_names):
1099+
mapping[name] = Member(name, clsname, offset)
1100+
return type.__new__(mcls, clsname, bases, mapping)
1101+
1102+
class Object:
1103+
'Simulate how object.__new__() allocates memory for __slots__'
1104+
1105+
def __new__(cls, *args):
1106+
'Emulate object_new() in Objects/typeobject.c'
1107+
inst = super().__new__(cls)
1108+
if hasattr(cls, 'slot_names'):
1109+
inst._slotvalues = [None] * len(cls.slot_names)
1110+
return inst
1111+
1112+
To use the simulation in a real class, just inherit from :class:`Object` and
1113+
set the :term:`metaclass` to :class:`Type`::
1114+
1115+
class H(Object, metaclass=Type):
1116+
1117+
slot_names = ['x', 'y']
1118+
1119+
def __init__(self, x, y):
1120+
self.x = x
1121+
self.y = y
1122+
1123+
At this point, the metaclass has loaded member objects for *x* and *y*::
1124+
1125+
>>> import pprint
1126+
>>> pprint.pp(dict(vars(H)))
1127+
{'__module__': '__main__',
1128+
'slot_names': ['x', 'y'],
1129+
'__init__': <function H.__init__ at 0x7fb5d302f9d0>,
1130+
'x': <Member 'x' of 'H'>,
1131+
'y': <Member 'y' of 'H'>,
1132+
'__doc__': None}
1133+
1134+
When instances are created, they have a ``slot_values`` list where the
1135+
attributes are stored::
1136+
1137+
>>> h = H(10, 20)
1138+
>>> vars(h)
1139+
{'_slotvalues': [10, 20]}
1140+
>>> h.x = 55
1141+
>>> vars(h)
1142+
{'_slotvalues': [55, 20]}
1143+
1144+
Unlike the real ``__slots__``, this simulation does have an instance
1145+
dictionary just to hold the ``_slotvalues`` array. So, unlike the real code,
1146+
this simulation doesn't block assignments to misspelled attributes::
1147+
1148+
>>> h.xz = 30 # For actual __slots__ this would raise an AttributeError

0 commit comments

Comments
 (0)