Skip to content

Commit 2d03b73

Browse files
authored
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.
1 parent f1c6ae3 commit 2d03b73

File tree

2 files changed

+32
-9
lines changed

2 files changed

+32
-9
lines changed

Lib/test/test_descr.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4923,6 +4923,23 @@ def __new__(cls):
49234923
cls.lst = [2**i for i in range(10000)]
49244924
X.descr
49254925

4926+
def test_remove_subclass(self):
4927+
# bpo-46417: when the last subclass of a type is deleted,
4928+
# remove_subclass() clears the internal dictionary of subclasses:
4929+
# set PyTypeObject.tp_subclasses to NULL. remove_subclass() is called
4930+
# when a type is deallocated.
4931+
class Parent:
4932+
pass
4933+
self.assertEqual(Parent.__subclasses__(), [])
4934+
4935+
class Child(Parent):
4936+
pass
4937+
self.assertEqual(Parent.__subclasses__(), [Child])
4938+
4939+
del Child
4940+
gc.collect()
4941+
self.assertEqual(Parent.__subclasses__(), [])
4942+
49264943

49274944
class DictProxyTests(unittest.TestCase):
49284945
def setUp(self):

Objects/typeobject.c

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4137,29 +4137,28 @@ _PyType_GetSubclasses(PyTypeObject *self)
41374137
return NULL;
41384138
}
41394139

4140-
// Hold a strong reference to tp_subclasses while iterating on it
4141-
PyObject *dict = Py_XNewRef(self->tp_subclasses);
4142-
if (dict == NULL) {
4140+
PyObject *subclasses = self->tp_subclasses; // borrowed ref
4141+
if (subclasses == NULL) {
41434142
return list;
41444143
}
4145-
assert(PyDict_CheckExact(dict));
4144+
assert(PyDict_CheckExact(subclasses));
4145+
// The loop cannot modify tp_subclasses, there is no need
4146+
// to hold a strong reference (use a borrowed reference).
41464147

41474148
Py_ssize_t i = 0;
41484149
PyObject *ref; // borrowed ref
4149-
while (PyDict_Next(dict, &i, NULL, &ref)) {
4150+
while (PyDict_Next(subclasses, &i, NULL, &ref)) {
41504151
assert(PyWeakref_CheckRef(ref));
41514152
PyObject *obj = PyWeakref_GET_OBJECT(ref); // borrowed ref
41524153
if (obj == Py_None) {
41534154
continue;
41544155
}
41554156
assert(PyType_Check(obj));
41564157
if (PyList_Append(list, obj) < 0) {
4157-
Py_CLEAR(list);
4158-
goto done;
4158+
Py_DECREF(list);
4159+
return NULL;
41594160
}
41604161
}
4161-
done:
4162-
Py_DECREF(dict);
41634162
return list;
41644163
}
41654164

@@ -6568,6 +6567,13 @@ remove_subclass(PyTypeObject *base, PyTypeObject *type)
65686567
PyErr_Clear();
65696568
}
65706569
Py_XDECREF(key);
6570+
6571+
if (PyDict_Size(dict) == 0) {
6572+
// Delete the dictionary to save memory. _PyStaticType_Dealloc()
6573+
// callers also test if tp_subclasses is NULL to check if a static type
6574+
// has no subclass.
6575+
Py_CLEAR(base->tp_subclasses);
6576+
}
65716577
}
65726578

65736579
static void

0 commit comments

Comments
 (0)