@@ -990,3 +990,159 @@ For example, a classmethod and property could be chained together::
990
990
@property
991
991
def __doc__(cls):
992
992
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