From 2d03b73cc9c0dada3243eab1373a46dbd98d24a0 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 22 Jan 2022 16:53:30 +0100 Subject: [PATCH] bpo-46417: remove_subclass() clears tp_subclasses (GH-30793) The remove_subclass() function now deletes the dictionary when removing the last subclass (if the dictionary becomes empty) to save memory: set PyTypeObject.tp_subclasses to NULL. remove_subclass() is called when a type is deallocated. _PyType_GetSubclasses() no longer holds a reference to tp_subclasses: its loop cannot modify tp_subclasses. --- Lib/test/test_descr.py | 17 +++++++++++++++++ Objects/typeobject.c | 24 +++++++++++++++--------- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index 707c93140e2..791cf714d46 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4923,6 +4923,23 @@ order (MRO) for bases """ cls.lst = [2**i for i in range(10000)] X.descr + def test_remove_subclass(self): + # bpo-46417: when the last subclass of a type is deleted, + # remove_subclass() clears the internal dictionary of subclasses: + # set PyTypeObject.tp_subclasses to NULL. remove_subclass() is called + # when a type is deallocated. + class Parent: + pass + self.assertEqual(Parent.__subclasses__(), []) + + class Child(Parent): + pass + self.assertEqual(Parent.__subclasses__(), [Child]) + + del Child + gc.collect() + self.assertEqual(Parent.__subclasses__(), []) + class DictProxyTests(unittest.TestCase): def setUp(self): diff --git a/Objects/typeobject.c b/Objects/typeobject.c index 2b47afe30e6..b3c305e0bf4 100644 --- a/Objects/typeobject.c +++ b/Objects/typeobject.c @@ -4137,16 +4137,17 @@ _PyType_GetSubclasses(PyTypeObject *self) return NULL; } - // Hold a strong reference to tp_subclasses while iterating on it - PyObject *dict = Py_XNewRef(self->tp_subclasses); - if (dict == NULL) { + PyObject *subclasses = self->tp_subclasses; // borrowed ref + if (subclasses == NULL) { return list; } - assert(PyDict_CheckExact(dict)); + assert(PyDict_CheckExact(subclasses)); + // The loop cannot modify tp_subclasses, there is no need + // to hold a strong reference (use a borrowed reference). Py_ssize_t i = 0; PyObject *ref; // borrowed ref - while (PyDict_Next(dict, &i, NULL, &ref)) { + while (PyDict_Next(subclasses, &i, NULL, &ref)) { assert(PyWeakref_CheckRef(ref)); PyObject *obj = PyWeakref_GET_OBJECT(ref); // borrowed ref if (obj == Py_None) { @@ -4154,12 +4155,10 @@ _PyType_GetSubclasses(PyTypeObject *self) } assert(PyType_Check(obj)); if (PyList_Append(list, obj) < 0) { - Py_CLEAR(list); - goto done; + Py_DECREF(list); + return NULL; } } -done: - Py_DECREF(dict); return list; } @@ -6568,6 +6567,13 @@ remove_subclass(PyTypeObject *base, PyTypeObject *type) PyErr_Clear(); } Py_XDECREF(key); + + if (PyDict_Size(dict) == 0) { + // Delete the dictionary to save memory. _PyStaticType_Dealloc() + // callers also test if tp_subclasses is NULL to check if a static type + // has no subclass. + Py_CLEAR(base->tp_subclasses); + } } static void