Skip to content

Commit ffae932

Browse files
authored
Descriptor HowTo: Improve the fidelity of the member object simulation (pythonGH-23475)
1 parent 2f2f9d0 commit ffae932

File tree

1 file changed

+46
-11
lines changed

1 file changed

+46
-11
lines changed

Doc/howto/descriptor.rst

+46-11
Original file line numberDiff line numberDiff line change
@@ -1079,6 +1079,8 @@ simulation where the actual C structure for slots is emulated by a private
10791079
``_slotvalues`` list. Reads and writes to that private structure are managed
10801080
by member descriptors::
10811081

1082+
null = object()
1083+
10821084
class Member:
10831085

10841086
def __init__(self, name, clsname, offset):
@@ -1091,20 +1093,28 @@ by member descriptors::
10911093
def __get__(self, obj, objtype=None):
10921094
'Emulate member_get() in Objects/descrobject.c'
10931095
# Also see PyMember_GetOne() in Python/structmember.c
1094-
return obj._slotvalues[self.offset]
1096+
value = obj._slotvalues[self.offset]
1097+
if value is null:
1098+
raise AttributeError(self.name)
1099+
return value
10951100

10961101
def __set__(self, obj, value):
10971102
'Emulate member_set() in Objects/descrobject.c'
10981103
obj._slotvalues[self.offset] = value
10991104

1105+
def __delete__(self, obj):
1106+
'Emulate member_delete() in Objects/descrobject.c'
1107+
value = obj._slotvalues[self.offset]
1108+
if value is null:
1109+
raise AttributeError(self.name)
1110+
obj._slotvalues[self.offset] = null
1111+
11001112
def __repr__(self):
11011113
'Emulate member_repr() in Objects/descrobject.c'
11021114
return f'<Member {self.name!r} of {self.clsname!r}>'
11031115

11041116
The :meth:`type.__new__` method takes care of adding member objects to class
1105-
variables. The :meth:`object.__new__` method takes care of creating instances
1106-
that have slots instead of an instance dictionary. Here is a rough equivalent
1107-
in pure Python::
1117+
variables::
11081118

11091119
class Type(type):
11101120
'Simulate how the type metaclass adds member objects for slots'
@@ -1117,20 +1127,44 @@ in pure Python::
11171127
mapping[name] = Member(name, clsname, offset)
11181128
return type.__new__(mcls, clsname, bases, mapping)
11191129

1130+
The :meth:`object.__new__` method takes care of creating instances that have
1131+
slots instead of an instance dictionary. Here is a rough simulation in pure
1132+
Python::
1133+
11201134
class Object:
11211135
'Simulate how object.__new__() allocates memory for __slots__'
11221136

11231137
def __new__(cls, *args):
11241138
'Emulate object_new() in Objects/typeobject.c'
11251139
inst = super().__new__(cls)
11261140
if hasattr(cls, 'slot_names'):
1127-
inst._slotvalues = [None] * len(cls.slot_names)
1141+
empty_slots = [null] * len(cls.slot_names)
1142+
object.__setattr__(inst, '_slotvalues', empty_slots)
11281143
return inst
11291144

1145+
def __setattr__(self, name, value):
1146+
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
1147+
cls = type(self)
1148+
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
1149+
raise AttributeError(
1150+
f'{type(self).__name__!r} object has no attribute {name!r}'
1151+
)
1152+
super().__setattr__(name, value)
1153+
1154+
def __delattr__(self, name):
1155+
'Emulate _PyObject_GenericSetAttrWithDict() Objects/object.c'
1156+
cls = type(self)
1157+
if hasattr(cls, 'slot_names') and name not in cls.slot_names:
1158+
raise AttributeError(
1159+
f'{type(self).__name__!r} object has no attribute {name!r}'
1160+
)
1161+
super().__delattr__(name)
1162+
11301163
To use the simulation in a real class, just inherit from :class:`Object` and
11311164
set the :term:`metaclass` to :class:`Type`::
11321165

11331166
class H(Object, metaclass=Type):
1167+
'Instance variables stored in slots'
11341168

11351169
slot_names = ['x', 'y']
11361170

@@ -1143,11 +1177,11 @@ At this point, the metaclass has loaded member objects for *x* and *y*::
11431177
>>> import pprint
11441178
>>> pprint.pp(dict(vars(H)))
11451179
{'__module__': '__main__',
1180+
'__doc__': 'Instance variables stored in slots',
11461181
'slot_names': ['x', 'y'],
11471182
'__init__': <function H.__init__ at 0x7fb5d302f9d0>,
11481183
'x': <Member 'x' of 'H'>,
1149-
'y': <Member 'y' of 'H'>,
1150-
'__doc__': None}
1184+
'y': <Member 'y' of 'H'>}
11511185

11521186
When instances are created, they have a ``slot_values`` list where the
11531187
attributes are stored::
@@ -1159,8 +1193,9 @@ attributes are stored::
11591193
>>> vars(h)
11601194
{'_slotvalues': [55, 20]}
11611195

1162-
Unlike the real ``__slots__``, this simulation does have an instance
1163-
dictionary just to hold the ``_slotvalues`` array. So, unlike the real code,
1164-
this simulation doesn't block assignments to misspelled attributes::
1196+
Misspelled or unassigned attributes will raise an exception::
11651197

1166-
>>> h.xz = 30 # For actual __slots__ this would raise an AttributeError
1198+
>>> h.xz
1199+
Traceback (most recent call last):
1200+
...
1201+
AttributeError: 'H' object has no attribute 'xz'

0 commit comments

Comments
 (0)