Skip to content

Commit 7dfa048

Browse files
gh-135228: Create __dict__ and __weakref__ descriptors for object (GH-136966)
This partially reverts #137047, keeping the tests for GC collectability of the original class that dataclass adds `__slots__` to. The reference leaks solved there are instead solved by having the `__dict__` & `__weakref__` descriptors not tied to (and referencing) their class. Instead, they're shared between all classes that need them (within an interpreter). The `__objclass__` ol the descriptors is set to `object`, since these descriptors work with *any* object. (The appropriate checks were already made in the get/set code, so the `__objclass__` check was redundant.) The repr of these descriptors (and any others whose `__objclass__` is `object`) now doesn't mention the objclass. This change required adjustment of introspection code that checks `__objclass__` to determine an object's “own” (i.e. not inherited) `__dict__`. Third-party code that does similar introspection of the internals will also need adjusting. Co-authored-by: Jelle Zijlstra <[email protected]>
1 parent 92be979 commit 7dfa048

File tree

13 files changed

+171
-120
lines changed

13 files changed

+171
-120
lines changed

Doc/whatsnew/3.15.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,13 @@ Other language changes
209209
as keyword arguments at construction time.
210210
(Contributed by Serhiy Storchaka, Oleg Iarygin, and Yoav Nir in :gh:`74185`.)
211211

212+
* The :attr:`~object.__dict__` and :attr:`!__weakref__` descriptors now use a
213+
single descriptor instance per interpreter, shared across all types that
214+
need them.
215+
This speeds up class creation, and helps avoid reference cycles.
216+
(Contributed by Petr Viktorin in :gh:`135228`.)
217+
218+
212219
New modules
213220
===========
214221

Include/internal/pycore_interp_structs.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,13 @@ struct _Py_interp_cached_objects {
691691
PyTypeObject *paramspecargs_type;
692692
PyTypeObject *paramspeckwargs_type;
693693
PyTypeObject *constevaluator_type;
694+
695+
/* Descriptors for __dict__ and __weakref__ */
696+
#ifdef Py_GIL_DISABLED
697+
PyMutex descriptor_mutex;
698+
#endif
699+
PyObject *dict_descriptor;
700+
PyObject *weakref_descriptor;
694701
};
695702

696703
struct _Py_interp_static_objects {

Include/internal/pycore_typeobject.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ extern void _PyTypes_FiniTypes(PyInterpreterState *);
4040
extern void _PyTypes_FiniExtTypes(PyInterpreterState *interp);
4141
extern void _PyTypes_Fini(PyInterpreterState *);
4242
extern void _PyTypes_AfterFork(void);
43+
extern void _PyTypes_FiniCachedDescriptors(PyInterpreterState *);
4344

4445
static inline PyObject **
4546
_PyStaticType_GET_WEAKREFS_LISTPTR(managed_static_type_state *state)

Lib/dataclasses.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,10 +1283,6 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
12831283
if '__slots__' in cls.__dict__:
12841284
raise TypeError(f'{cls.__name__} already specifies __slots__')
12851285

1286-
# gh-102069: Remove existing __weakref__ descriptor.
1287-
# gh-135228: Make sure the original class can be garbage collected.
1288-
sys._clear_type_descriptors(cls)
1289-
12901286
# Create a new dict for our new class.
12911287
cls_dict = dict(cls.__dict__)
12921288
field_names = tuple(f.name for f in fields(cls))
@@ -1304,6 +1300,11 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
13041300
# available in _MARKER.
13051301
cls_dict.pop(field_name, None)
13061302

1303+
# Remove __dict__ and `__weakref__` descriptors.
1304+
# They'll be added back if applicable.
1305+
cls_dict.pop('__dict__', None)
1306+
cls_dict.pop('__weakref__', None) # gh-102069
1307+
13071308
# And finally create the class.
13081309
qualname = getattr(cls, '__qualname__', None)
13091310
newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)

Lib/inspect.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1698,7 +1698,8 @@ def _shadowed_dict_from_weakref_mro_tuple(*weakref_mro):
16981698
class_dict = dunder_dict['__dict__']
16991699
if not (type(class_dict) is types.GetSetDescriptorType and
17001700
class_dict.__name__ == "__dict__" and
1701-
class_dict.__objclass__ is entry):
1701+
(class_dict.__objclass__ is object or
1702+
class_dict.__objclass__ is entry)):
17021703
return class_dict
17031704
return _sentinel
17041705

Lib/test/test_descr.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6013,5 +6013,69 @@ class A(metaclass=M):
60136013
pass
60146014

60156015

6016+
class TestGenericDescriptors(unittest.TestCase):
6017+
def test___dict__(self):
6018+
class CustomClass:
6019+
pass
6020+
class SlotClass:
6021+
__slots__ = ['foo']
6022+
class SlotSubClass(SlotClass):
6023+
pass
6024+
class IntSubclass(int):
6025+
pass
6026+
6027+
dict_descriptor = CustomClass.__dict__['__dict__']
6028+
self.assertEqual(dict_descriptor.__objclass__, object)
6029+
6030+
for cls in CustomClass, SlotSubClass, IntSubclass:
6031+
with self.subTest(cls=cls):
6032+
self.assertIs(cls.__dict__['__dict__'], dict_descriptor)
6033+
instance = cls()
6034+
instance.attr = 123
6035+
self.assertEqual(
6036+
dict_descriptor.__get__(instance, cls),
6037+
{'attr': 123},
6038+
)
6039+
with self.assertRaises(AttributeError):
6040+
print(dict_descriptor.__get__(True, bool))
6041+
with self.assertRaises(AttributeError):
6042+
print(dict_descriptor.__get__(SlotClass(), SlotClass))
6043+
6044+
# delegation to type.__dict__
6045+
self.assertIsInstance(
6046+
dict_descriptor.__get__(type, type),
6047+
types.MappingProxyType,
6048+
)
6049+
6050+
def test___weakref__(self):
6051+
class CustomClass:
6052+
pass
6053+
class SlotClass:
6054+
__slots__ = ['foo']
6055+
class SlotSubClass(SlotClass):
6056+
pass
6057+
class IntSubclass(int):
6058+
pass
6059+
6060+
weakref_descriptor = CustomClass.__dict__['__weakref__']
6061+
self.assertEqual(weakref_descriptor.__objclass__, object)
6062+
6063+
for cls in CustomClass, SlotSubClass:
6064+
with self.subTest(cls=cls):
6065+
self.assertIs(cls.__dict__['__weakref__'], weakref_descriptor)
6066+
instance = cls()
6067+
instance.attr = 123
6068+
self.assertEqual(
6069+
weakref_descriptor.__get__(instance, cls),
6070+
None,
6071+
)
6072+
with self.assertRaises(AttributeError):
6073+
weakref_descriptor.__get__(True, bool)
6074+
with self.assertRaises(AttributeError):
6075+
weakref_descriptor.__get__(SlotClass(), SlotClass)
6076+
with self.assertRaises(AttributeError):
6077+
weakref_descriptor.__get__(IntSubclass(), IntSubclass)
6078+
6079+
60166080
if __name__ == "__main__":
60176081
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
The :attr:`object.__dict__` and :attr:`!__weakref__` descriptors now use a
2+
single descriptor instance per interpreter, shared across all types that
3+
need them.
4+
This speeds up class creation, and helps avoid reference cycles.

Objects/descrobject.c

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,41 +39,41 @@ descr_name(PyDescrObject *descr)
3939
}
4040

4141
static PyObject *
42-
descr_repr(PyDescrObject *descr, const char *format)
42+
descr_repr(PyDescrObject *descr, const char *kind)
4343
{
4444
PyObject *name = NULL;
4545
if (descr->d_name != NULL && PyUnicode_Check(descr->d_name))
4646
name = descr->d_name;
4747

48-
return PyUnicode_FromFormat(format, name, "?", descr->d_type->tp_name);
48+
if (descr->d_type == &PyBaseObject_Type) {
49+
return PyUnicode_FromFormat("<%s '%V'>", kind, name, "?");
50+
}
51+
return PyUnicode_FromFormat("<%s '%V' of '%s' objects>",
52+
kind, name, "?", descr->d_type->tp_name);
4953
}
5054

5155
static PyObject *
5256
method_repr(PyObject *descr)
5357
{
54-
return descr_repr((PyDescrObject *)descr,
55-
"<method '%V' of '%s' objects>");
58+
return descr_repr((PyDescrObject *)descr, "method");
5659
}
5760

5861
static PyObject *
5962
member_repr(PyObject *descr)
6063
{
61-
return descr_repr((PyDescrObject *)descr,
62-
"<member '%V' of '%s' objects>");
64+
return descr_repr((PyDescrObject *)descr, "member");
6365
}
6466

6567
static PyObject *
6668
getset_repr(PyObject *descr)
6769
{
68-
return descr_repr((PyDescrObject *)descr,
69-
"<attribute '%V' of '%s' objects>");
70+
return descr_repr((PyDescrObject *)descr, "attribute");
7071
}
7172

7273
static PyObject *
7374
wrapperdescr_repr(PyObject *descr)
7475
{
75-
return descr_repr((PyDescrObject *)descr,
76-
"<slot wrapper '%V' of '%s' objects>");
76+
return descr_repr((PyDescrObject *)descr, "slot wrapper");
7777
}
7878

7979
static int

Objects/typeobject.c

Lines changed: 68 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4039,26 +4039,15 @@ subtype_getweakref(PyObject *obj, void *context)
40394039
return Py_NewRef(result);
40404040
}
40414041

4042-
/* Three variants on the subtype_getsets list. */
4043-
4044-
static PyGetSetDef subtype_getsets_full[] = {
4045-
{"__dict__", subtype_dict, subtype_setdict,
4046-
PyDoc_STR("dictionary for instance variables")},
4047-
{"__weakref__", subtype_getweakref, NULL,
4048-
PyDoc_STR("list of weak references to the object")},
4049-
{0}
4050-
};
4051-
4052-
static PyGetSetDef subtype_getsets_dict_only[] = {
4053-
{"__dict__", subtype_dict, subtype_setdict,
4054-
PyDoc_STR("dictionary for instance variables")},
4055-
{0}
4042+
/* getset definitions for common descriptors */
4043+
static PyGetSetDef subtype_getset_dict = {
4044+
"__dict__", subtype_dict, subtype_setdict,
4045+
PyDoc_STR("dictionary for instance variables"),
40564046
};
40574047

4058-
static PyGetSetDef subtype_getsets_weakref_only[] = {
4059-
{"__weakref__", subtype_getweakref, NULL,
4060-
PyDoc_STR("list of weak references to the object")},
4061-
{0}
4048+
static PyGetSetDef subtype_getset_weakref = {
4049+
"__weakref__", subtype_getweakref, NULL,
4050+
PyDoc_STR("list of weak references to the object"),
40624051
};
40634052

40644053
static int
@@ -4594,10 +4583,36 @@ type_new_classmethod(PyObject *dict, PyObject *attr)
45944583
return 0;
45954584
}
45964585

4586+
/* Add __dict__ or __weakref__ descriptor */
4587+
static int
4588+
type_add_common_descriptor(PyInterpreterState *interp,
4589+
PyObject **cache,
4590+
PyGetSetDef *getset_def,
4591+
PyObject *dict)
4592+
{
4593+
#ifdef Py_GIL_DISABLED
4594+
PyMutex_Lock(&interp->cached_objects.descriptor_mutex);
4595+
#endif
4596+
PyObject *descr = *cache;
4597+
if (!descr) {
4598+
descr = PyDescr_NewGetSet(&PyBaseObject_Type, getset_def);
4599+
*cache = descr;
4600+
}
4601+
#ifdef Py_GIL_DISABLED
4602+
PyMutex_Unlock(&interp->cached_objects.descriptor_mutex);
4603+
#endif
4604+
if (!descr) {
4605+
return -1;
4606+
}
4607+
if (PyDict_SetDefaultRef(dict, PyDescr_NAME(descr), descr, NULL) < 0) {
4608+
return -1;
4609+
}
4610+
return 0;
4611+
}
45974612

45984613
/* Add descriptors for custom slots from __slots__, or for __dict__ */
45994614
static int
4600-
type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type)
4615+
type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict)
46014616
{
46024617
PyHeapTypeObject *et = (PyHeapTypeObject *)type;
46034618
Py_ssize_t slotoffset = ctx->base->tp_basicsize;
@@ -4635,25 +4650,38 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type)
46354650
type->tp_basicsize = slotoffset;
46364651
type->tp_itemsize = ctx->base->tp_itemsize;
46374652
type->tp_members = _PyHeapType_GET_MEMBERS(et);
4653+
4654+
PyInterpreterState *interp = _PyInterpreterState_GET();
4655+
4656+
if (type->tp_dictoffset) {
4657+
if (type_add_common_descriptor(
4658+
interp,
4659+
&interp->cached_objects.dict_descriptor,
4660+
&subtype_getset_dict,
4661+
dict) < 0)
4662+
{
4663+
return -1;
4664+
}
4665+
}
4666+
if (type->tp_weaklistoffset) {
4667+
if (type_add_common_descriptor(
4668+
interp,
4669+
&interp->cached_objects.weakref_descriptor,
4670+
&subtype_getset_weakref,
4671+
dict) < 0)
4672+
{
4673+
return -1;
4674+
}
4675+
}
4676+
46384677
return 0;
46394678
}
46404679

46414680

46424681
static void
46434682
type_new_set_slots(const type_new_ctx *ctx, PyTypeObject *type)
46444683
{
4645-
if (type->tp_weaklistoffset && type->tp_dictoffset) {
4646-
type->tp_getset = subtype_getsets_full;
4647-
}
4648-
else if (type->tp_weaklistoffset && !type->tp_dictoffset) {
4649-
type->tp_getset = subtype_getsets_weakref_only;
4650-
}
4651-
else if (!type->tp_weaklistoffset && type->tp_dictoffset) {
4652-
type->tp_getset = subtype_getsets_dict_only;
4653-
}
4654-
else {
4655-
type->tp_getset = NULL;
4656-
}
4684+
type->tp_getset = NULL;
46574685

46584686
/* Special case some slots */
46594687
if (type->tp_dictoffset != 0 || ctx->nslot > 0) {
@@ -4758,7 +4786,7 @@ type_new_set_attrs(const type_new_ctx *ctx, PyTypeObject *type)
47584786
return -1;
47594787
}
47604788

4761-
if (type_new_descriptors(ctx, type) < 0) {
4789+
if (type_new_descriptors(ctx, type, dict) < 0) {
47624790
return -1;
47634791
}
47644792

@@ -6642,6 +6670,14 @@ _PyStaticType_FiniBuiltin(PyInterpreterState *interp, PyTypeObject *type)
66426670
}
66436671

66446672

6673+
void
6674+
_PyTypes_FiniCachedDescriptors(PyInterpreterState *interp)
6675+
{
6676+
Py_CLEAR(interp->cached_objects.dict_descriptor);
6677+
Py_CLEAR(interp->cached_objects.weakref_descriptor);
6678+
}
6679+
6680+
66456681
static void
66466682
type_dealloc(PyObject *self)
66476683
{

Python/clinic/sysmodule.c.h

Lines changed: 1 addition & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)