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 <jelle.zijlstra@gmail.com>
This commit is contained in:
Petr Viktorin 2025-08-18 14:25:51 +02:00 committed by GitHub
parent 92be979f64
commit 7dfa048bbb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 171 additions and 120 deletions

View file

@ -209,6 +209,13 @@ Other language changes
as keyword arguments at construction time.
(Contributed by Serhiy Storchaka, Oleg Iarygin, and Yoav Nir in :gh:`74185`.)
* The :attr:`~object.__dict__` and :attr:`!__weakref__` descriptors now use a
single descriptor instance per interpreter, shared across all types that
need them.
This speeds up class creation, and helps avoid reference cycles.
(Contributed by Petr Viktorin in :gh:`135228`.)
New modules
===========

View file

@ -691,6 +691,13 @@ struct _Py_interp_cached_objects {
PyTypeObject *paramspecargs_type;
PyTypeObject *paramspeckwargs_type;
PyTypeObject *constevaluator_type;
/* Descriptors for __dict__ and __weakref__ */
#ifdef Py_GIL_DISABLED
PyMutex descriptor_mutex;
#endif
PyObject *dict_descriptor;
PyObject *weakref_descriptor;
};
struct _Py_interp_static_objects {

View file

@ -40,6 +40,7 @@ extern void _PyTypes_FiniTypes(PyInterpreterState *);
extern void _PyTypes_FiniExtTypes(PyInterpreterState *interp);
extern void _PyTypes_Fini(PyInterpreterState *);
extern void _PyTypes_AfterFork(void);
extern void _PyTypes_FiniCachedDescriptors(PyInterpreterState *);
static inline PyObject **
_PyStaticType_GET_WEAKREFS_LISTPTR(managed_static_type_state *state)

View file

@ -1283,10 +1283,6 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
if '__slots__' in cls.__dict__:
raise TypeError(f'{cls.__name__} already specifies __slots__')
# gh-102069: Remove existing __weakref__ descriptor.
# gh-135228: Make sure the original class can be garbage collected.
sys._clear_type_descriptors(cls)
# Create a new dict for our new class.
cls_dict = dict(cls.__dict__)
field_names = tuple(f.name for f in fields(cls))
@ -1304,6 +1300,11 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
# available in _MARKER.
cls_dict.pop(field_name, None)
# Remove __dict__ and `__weakref__` descriptors.
# They'll be added back if applicable.
cls_dict.pop('__dict__', None)
cls_dict.pop('__weakref__', None) # gh-102069
# And finally create the class.
qualname = getattr(cls, '__qualname__', None)
newcls = type(cls)(cls.__name__, cls.__bases__, cls_dict)

View file

@ -1698,7 +1698,8 @@ def _shadowed_dict_from_weakref_mro_tuple(*weakref_mro):
class_dict = dunder_dict['__dict__']
if not (type(class_dict) is types.GetSetDescriptorType and
class_dict.__name__ == "__dict__" and
class_dict.__objclass__ is entry):
(class_dict.__objclass__ is object or
class_dict.__objclass__ is entry)):
return class_dict
return _sentinel

View file

@ -6013,5 +6013,69 @@ class A(metaclass=M):
pass
class TestGenericDescriptors(unittest.TestCase):
def test___dict__(self):
class CustomClass:
pass
class SlotClass:
__slots__ = ['foo']
class SlotSubClass(SlotClass):
pass
class IntSubclass(int):
pass
dict_descriptor = CustomClass.__dict__['__dict__']
self.assertEqual(dict_descriptor.__objclass__, object)
for cls in CustomClass, SlotSubClass, IntSubclass:
with self.subTest(cls=cls):
self.assertIs(cls.__dict__['__dict__'], dict_descriptor)
instance = cls()
instance.attr = 123
self.assertEqual(
dict_descriptor.__get__(instance, cls),
{'attr': 123},
)
with self.assertRaises(AttributeError):
print(dict_descriptor.__get__(True, bool))
with self.assertRaises(AttributeError):
print(dict_descriptor.__get__(SlotClass(), SlotClass))
# delegation to type.__dict__
self.assertIsInstance(
dict_descriptor.__get__(type, type),
types.MappingProxyType,
)
def test___weakref__(self):
class CustomClass:
pass
class SlotClass:
__slots__ = ['foo']
class SlotSubClass(SlotClass):
pass
class IntSubclass(int):
pass
weakref_descriptor = CustomClass.__dict__['__weakref__']
self.assertEqual(weakref_descriptor.__objclass__, object)
for cls in CustomClass, SlotSubClass:
with self.subTest(cls=cls):
self.assertIs(cls.__dict__['__weakref__'], weakref_descriptor)
instance = cls()
instance.attr = 123
self.assertEqual(
weakref_descriptor.__get__(instance, cls),
None,
)
with self.assertRaises(AttributeError):
weakref_descriptor.__get__(True, bool)
with self.assertRaises(AttributeError):
weakref_descriptor.__get__(SlotClass(), SlotClass)
with self.assertRaises(AttributeError):
weakref_descriptor.__get__(IntSubclass(), IntSubclass)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,4 @@
The :attr:`object.__dict__` and :attr:`!__weakref__` descriptors now use a
single descriptor instance per interpreter, shared across all types that
need them.
This speeds up class creation, and helps avoid reference cycles.

View file

@ -39,41 +39,41 @@ descr_name(PyDescrObject *descr)
}
static PyObject *
descr_repr(PyDescrObject *descr, const char *format)
descr_repr(PyDescrObject *descr, const char *kind)
{
PyObject *name = NULL;
if (descr->d_name != NULL && PyUnicode_Check(descr->d_name))
name = descr->d_name;
return PyUnicode_FromFormat(format, name, "?", descr->d_type->tp_name);
if (descr->d_type == &PyBaseObject_Type) {
return PyUnicode_FromFormat("<%s '%V'>", kind, name, "?");
}
return PyUnicode_FromFormat("<%s '%V' of '%s' objects>",
kind, name, "?", descr->d_type->tp_name);
}
static PyObject *
method_repr(PyObject *descr)
{
return descr_repr((PyDescrObject *)descr,
"<method '%V' of '%s' objects>");
return descr_repr((PyDescrObject *)descr, "method");
}
static PyObject *
member_repr(PyObject *descr)
{
return descr_repr((PyDescrObject *)descr,
"<member '%V' of '%s' objects>");
return descr_repr((PyDescrObject *)descr, "member");
}
static PyObject *
getset_repr(PyObject *descr)
{
return descr_repr((PyDescrObject *)descr,
"<attribute '%V' of '%s' objects>");
return descr_repr((PyDescrObject *)descr, "attribute");
}
static PyObject *
wrapperdescr_repr(PyObject *descr)
{
return descr_repr((PyDescrObject *)descr,
"<slot wrapper '%V' of '%s' objects>");
return descr_repr((PyDescrObject *)descr, "slot wrapper");
}
static int

View file

@ -4039,26 +4039,15 @@ subtype_getweakref(PyObject *obj, void *context)
return Py_NewRef(result);
}
/* Three variants on the subtype_getsets list. */
static PyGetSetDef subtype_getsets_full[] = {
{"__dict__", subtype_dict, subtype_setdict,
PyDoc_STR("dictionary for instance variables")},
{"__weakref__", subtype_getweakref, NULL,
PyDoc_STR("list of weak references to the object")},
{0}
/* getset definitions for common descriptors */
static PyGetSetDef subtype_getset_dict = {
"__dict__", subtype_dict, subtype_setdict,
PyDoc_STR("dictionary for instance variables"),
};
static PyGetSetDef subtype_getsets_dict_only[] = {
{"__dict__", subtype_dict, subtype_setdict,
PyDoc_STR("dictionary for instance variables")},
{0}
};
static PyGetSetDef subtype_getsets_weakref_only[] = {
{"__weakref__", subtype_getweakref, NULL,
PyDoc_STR("list of weak references to the object")},
{0}
static PyGetSetDef subtype_getset_weakref = {
"__weakref__", subtype_getweakref, NULL,
PyDoc_STR("list of weak references to the object"),
};
static int
@ -4594,10 +4583,36 @@ type_new_classmethod(PyObject *dict, PyObject *attr)
return 0;
}
/* Add __dict__ or __weakref__ descriptor */
static int
type_add_common_descriptor(PyInterpreterState *interp,
PyObject **cache,
PyGetSetDef *getset_def,
PyObject *dict)
{
#ifdef Py_GIL_DISABLED
PyMutex_Lock(&interp->cached_objects.descriptor_mutex);
#endif
PyObject *descr = *cache;
if (!descr) {
descr = PyDescr_NewGetSet(&PyBaseObject_Type, getset_def);
*cache = descr;
}
#ifdef Py_GIL_DISABLED
PyMutex_Unlock(&interp->cached_objects.descriptor_mutex);
#endif
if (!descr) {
return -1;
}
if (PyDict_SetDefaultRef(dict, PyDescr_NAME(descr), descr, NULL) < 0) {
return -1;
}
return 0;
}
/* Add descriptors for custom slots from __slots__, or for __dict__ */
static int
type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type)
type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type, PyObject *dict)
{
PyHeapTypeObject *et = (PyHeapTypeObject *)type;
Py_ssize_t slotoffset = ctx->base->tp_basicsize;
@ -4635,6 +4650,30 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type)
type->tp_basicsize = slotoffset;
type->tp_itemsize = ctx->base->tp_itemsize;
type->tp_members = _PyHeapType_GET_MEMBERS(et);
PyInterpreterState *interp = _PyInterpreterState_GET();
if (type->tp_dictoffset) {
if (type_add_common_descriptor(
interp,
&interp->cached_objects.dict_descriptor,
&subtype_getset_dict,
dict) < 0)
{
return -1;
}
}
if (type->tp_weaklistoffset) {
if (type_add_common_descriptor(
interp,
&interp->cached_objects.weakref_descriptor,
&subtype_getset_weakref,
dict) < 0)
{
return -1;
}
}
return 0;
}
@ -4642,18 +4681,7 @@ type_new_descriptors(const type_new_ctx *ctx, PyTypeObject *type)
static void
type_new_set_slots(const type_new_ctx *ctx, PyTypeObject *type)
{
if (type->tp_weaklistoffset && type->tp_dictoffset) {
type->tp_getset = subtype_getsets_full;
}
else if (type->tp_weaklistoffset && !type->tp_dictoffset) {
type->tp_getset = subtype_getsets_weakref_only;
}
else if (!type->tp_weaklistoffset && type->tp_dictoffset) {
type->tp_getset = subtype_getsets_dict_only;
}
else {
type->tp_getset = NULL;
}
type->tp_getset = NULL;
/* Special case some slots */
if (type->tp_dictoffset != 0 || ctx->nslot > 0) {
@ -4758,7 +4786,7 @@ type_new_set_attrs(const type_new_ctx *ctx, PyTypeObject *type)
return -1;
}
if (type_new_descriptors(ctx, type) < 0) {
if (type_new_descriptors(ctx, type, dict) < 0) {
return -1;
}
@ -6642,6 +6670,14 @@ _PyStaticType_FiniBuiltin(PyInterpreterState *interp, PyTypeObject *type)
}
void
_PyTypes_FiniCachedDescriptors(PyInterpreterState *interp)
{
Py_CLEAR(interp->cached_objects.dict_descriptor);
Py_CLEAR(interp->cached_objects.weakref_descriptor);
}
static void
type_dealloc(PyObject *self)
{

View file

@ -1793,37 +1793,6 @@ sys__baserepl(PyObject *module, PyObject *Py_UNUSED(ignored))
return sys__baserepl_impl(module);
}
PyDoc_STRVAR(sys__clear_type_descriptors__doc__,
"_clear_type_descriptors($module, type, /)\n"
"--\n"
"\n"
"Private function for clearing certain descriptors from a type\'s dictionary.\n"
"\n"
"See gh-135228 for context.");
#define SYS__CLEAR_TYPE_DESCRIPTORS_METHODDEF \
{"_clear_type_descriptors", (PyCFunction)sys__clear_type_descriptors, METH_O, sys__clear_type_descriptors__doc__},
static PyObject *
sys__clear_type_descriptors_impl(PyObject *module, PyObject *type);
static PyObject *
sys__clear_type_descriptors(PyObject *module, PyObject *arg)
{
PyObject *return_value = NULL;
PyObject *type;
if (!PyObject_TypeCheck(arg, &PyType_Type)) {
_PyArg_BadArgument("_clear_type_descriptors", "argument", (&PyType_Type)->tp_name, arg);
goto exit;
}
type = arg;
return_value = sys__clear_type_descriptors_impl(module, type);
exit:
return return_value;
}
PyDoc_STRVAR(sys__is_gil_enabled__doc__,
"_is_gil_enabled($module, /)\n"
"--\n"
@ -1979,4 +1948,4 @@ exit:
#ifndef SYS_GETANDROIDAPILEVEL_METHODDEF
#define SYS_GETANDROIDAPILEVEL_METHODDEF
#endif /* !defined(SYS_GETANDROIDAPILEVEL_METHODDEF) */
/*[clinic end generated code: output=9052f399f40ca32d input=a9049054013a1b77]*/
/*[clinic end generated code: output=449d16326e69dcf6 input=a9049054013a1b77]*/

View file

@ -1906,6 +1906,7 @@ finalize_interp_clear(PyThreadState *tstate)
_PyXI_Fini(tstate->interp);
_PyExc_ClearExceptionGroupType(tstate->interp);
_Py_clear_generic_types(tstate->interp);
_PyTypes_FiniCachedDescriptors(tstate->interp);
/* Clear interpreter state and all thread states */
_PyInterpreterState_Clear(tstate);

View file

@ -2644,46 +2644,6 @@ sys__baserepl_impl(PyObject *module)
Py_RETURN_NONE;
}
/*[clinic input]
sys._clear_type_descriptors
type: object(subclass_of='&PyType_Type')
/
Private function for clearing certain descriptors from a type's dictionary.
See gh-135228 for context.
[clinic start generated code]*/
static PyObject *
sys__clear_type_descriptors_impl(PyObject *module, PyObject *type)
/*[clinic end generated code: output=5ad17851b762b6d9 input=dc536c97fde07251]*/
{
PyTypeObject *typeobj = (PyTypeObject *)type;
if (_PyType_HasFeature(typeobj, Py_TPFLAGS_IMMUTABLETYPE)) {
PyErr_SetString(PyExc_TypeError, "argument is immutable");
return NULL;
}
PyObject *dict = _PyType_GetDict(typeobj);
PyObject *dunder_dict = NULL;
if (PyDict_Pop(dict, &_Py_ID(__dict__), &dunder_dict) < 0) {
return NULL;
}
PyObject *dunder_weakref = NULL;
if (PyDict_Pop(dict, &_Py_ID(__weakref__), &dunder_weakref) < 0) {
PyType_Modified(typeobj);
Py_XDECREF(dunder_dict);
return NULL;
}
PyType_Modified(typeobj);
// We try to hold onto a reference to these until after we call
// PyType_Modified(), in case their deallocation triggers somer user code
// that tries to do something to the type.
Py_XDECREF(dunder_dict);
Py_XDECREF(dunder_weakref);
Py_RETURN_NONE;
}
/*[clinic input]
sys._is_gil_enabled -> bool
@ -2881,7 +2841,6 @@ static PyMethodDef sys_methods[] = {
SYS__STATS_DUMP_METHODDEF
#endif
SYS__GET_CPU_COUNT_CONFIG_METHODDEF
SYS__CLEAR_TYPE_DESCRIPTORS_METHODDEF
SYS__IS_GIL_ENABLED_METHODDEF
SYS__DUMP_TRACELETS_METHODDEF
{NULL, NULL} // sentinel

View file

@ -67,6 +67,7 @@
'PyMethodDef',
'PyMethodDef[]',
'PyMemberDef[]',
'PyGetSetDef',
'PyGetSetDef[]',
'PyNumberMethods',
'PySequenceMethods',