Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.15.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
===========

Expand Down
7 changes: 7 additions & 0 deletions Include/internal/pycore_interp_structs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_typeobject.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions Lib/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion Lib/inspect.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
64 changes: 64 additions & 0 deletions Lib/test/test_descr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -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.
20 changes: 10 additions & 10 deletions Objects/descrobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
100 changes: 68 additions & 32 deletions Objects/typeobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -4635,25 +4650,38 @@ 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;
}


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) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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)
{
Expand Down
33 changes: 1 addition & 32 deletions Python/clinic/sysmodule.c.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Python/pylifecycle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading
Loading