]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-135228: Create __dict__ and __weakref__ descriptors for object (GH-136966)
authorPetr Viktorin <encukou@gmail.com>
Mon, 18 Aug 2025 12:25:51 +0000 (14:25 +0200)
committerGitHub <noreply@github.com>
Mon, 18 Aug 2025 12:25:51 +0000 (14:25 +0200)
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>
13 files changed:
Doc/whatsnew/3.15.rst
Include/internal/pycore_interp_structs.h
Include/internal/pycore_typeobject.h
Lib/dataclasses.py
Lib/inspect.py
Lib/test/test_descr.py
Misc/NEWS.d/next/Core_and_Builtins/2025-08-05-10-22-15.gh-issue-136966.J5lrE0.rst [new file with mode: 0644]
Objects/descrobject.c
Objects/typeobject.c
Python/clinic/sysmodule.c.h
Python/pylifecycle.c
Python/sysmodule.c
Tools/c-analyzer/cpython/_analyzer.py

index 407606da961c16a4db717f1293b82135c0986387..81aa12184ed35c14b8a968c866f33d20bc5d7a76 100644 (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
 ===========
 
index e300732e9e58c3bcd853edd9fe02c2d1cfd32f67..2cb1b1046813008c57afcaa791cf1d0e9fa1a9ab 100644 (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 {
index 0ee7d555c56cdd80b1d299f9ea36e438f44e9e3b..24df69aa93fda286c2a678bf05ed8d483fa28340 100644 (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)
index d29f1615f276d2c2b9b2da2437c87be2fb1252dd..b98f21dcbe9220becdacde7d5eb191d8ac5986b4 100644 (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)
index 183e67fabf966ee61f774f801b678cdf1f4c8fd3..d7814bfeb2b885766ce0440b38a3102a0979f31b 100644 (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
 
index 8da6647c3f71fcccd0380cd62340eddf6a940a67..9dfeeccb81b34dd343a8b2412a5c747f61645860 100644 (file)
@@ -6013,5 +6013,69 @@ class MroTest(unittest.TestCase):
                 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()
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-08-05-10-22-15.gh-issue-136966.J5lrE0.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-08-05-10-22-15.gh-issue-136966.J5lrE0.rst
new file mode 100644 (file)
index 0000000..aafd9ca
--- /dev/null
@@ -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.
index d3d17e92b6d1e8f6d1b8d5e40fa8170027352993..06a81a4fdbd86538a79baaf332c19949ce71f2b2 100644 (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
index fb33bc747d885b07148ecdce937d921e7e901a17..9cead729b6fe7ab8bf5e7cdbdcc7ca97d8e1db56 100644 (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}
-};
-
-static PyGetSetDef subtype_getsets_dict_only[] = {
-    {"__dict__", subtype_dict, subtype_setdict,
-     PyDoc_STR("dictionary for instance variables")},
-    {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_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)
 {
index 09ce77fd12608f7c0fe2b80359686162b7c5d3f4..a47e4d11b54441dc279bddc3531ff9bf8ae7ed12 100644 (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]*/
index e22a9cc1c75050394d23e36ba732f28b53f0ce14..b6b1d2845ec2f1f97f7ae829dd0e13eccf41a5bb 100644 (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);
index 19912b4a4c61988276aab82b9a31543d2270cb67..bedbdfc489872e28e1fa0b2122aec17e82a74c40 100644 (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
index 6204353e9bd26a21452fcdf738a7e695a1476733..6f0f464892845fd2639ecff4be304825873eea35 100644 (file)
@@ -67,6 +67,7 @@ _OTHER_SUPPORTED_TYPES = {
     'PyMethodDef',
     'PyMethodDef[]',
     'PyMemberDef[]',
+    'PyGetSetDef',
     'PyGetSetDef[]',
     'PyNumberMethods',
     'PySequenceMethods',