mirror of
https://github.com/python/cpython.git
synced 2025-10-19 16:03:42 +00:00
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:
parent
92be979f64
commit
7dfa048bbb
13 changed files with 171 additions and 120 deletions
|
@ -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
|
||||
===========
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
33
Python/clinic/sysmodule.c.h
generated
33
Python/clinic/sysmodule.c.h
generated
|
@ -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]*/
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
'PyMethodDef',
|
||||
'PyMethodDef[]',
|
||||
'PyMemberDef[]',
|
||||
'PyGetSetDef',
|
||||
'PyGetSetDef[]',
|
||||
'PyNumberMethods',
|
||||
'PySequenceMethods',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue