Skip to content
Open
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
4 changes: 4 additions & 0 deletions Doc/c-api/dict.rst
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,10 @@ Frozen dictionary objects

Create an empty dictionary if *iterable* is ``NULL``.

.. impl-detail::
If *iterable* is a :class:`frozendict`, return the same object
unmodified.


Ordered dictionaries
^^^^^^^^^^^^^^^^^^^^
Expand Down
10 changes: 10 additions & 0 deletions Lib/test/test_capi/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,16 @@ def test_frozendict_new(self):
self.assertEqual(dct, frozendict(x=1, y=2))
self.assertIs(type(dct), frozendict)

# PyFrozenDict_New(frozendict) returns the same object unmodified
fd = frozendict(a=1, b=2, c=3)
fd2 = frozendict_new(fd)
self.assertIs(fd2, fd)

fd = FrozenDictSubclass(a=1, b=2, c=3)
fd2 = frozendict_new(fd)
self.assertIsNot(fd2, fd)
self.assertEqual(fd2, fd)

# PyFrozenDict_New(NULL) creates an empty dictionary
dct = frozendict_new(NULL)
self.assertEqual(dct, frozendict())
Expand Down
7 changes: 7 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -1829,6 +1829,13 @@ def test_constructor(self):
with self.assertRaises(TypeError):
dict.__init__(d, x=1)

# Avoid copy if it's frozendict type
d2 = frozendict(d)
self.assertIs(d2, d)
d2 = FrozenDict(d)
self.assertIsNot(d2, d)
self.assertEqual(d2, d)

def test_copy(self):
d = frozendict(x=1, y=2)
d2 = d.copy()
Expand Down
53 changes: 45 additions & 8 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -5169,15 +5169,47 @@ dict_vectorcall(PyObject *type, PyObject * const*args,
return NULL;
}

PyObject *self;
if (Py_Is((PyTypeObject*)type, &PyFrozenDict_Type)
|| PyType_IsSubtype((PyTypeObject*)type, &PyFrozenDict_Type))
{
self = frozendict_new(_PyType_CAST(type), NULL, NULL);
PyObject *self = dict_new(_PyType_CAST(type), NULL, NULL);
if (self == NULL) {
return NULL;
}
else {
self = dict_new(_PyType_CAST(type), NULL, NULL);
if (nargs == 1) {
if (dict_update_arg(self, args[0]) < 0) {
Py_DECREF(self);
return NULL;
}
args++;
}
if (kwnames != NULL) {
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(kwnames); i++) {
PyObject *key = PyTuple_GET_ITEM(kwnames, i); // borrowed
if (PyDict_SetItem(self, key, args[i]) < 0) {
Py_DECREF(self);
return NULL;
}
}
}
return self;
}

static PyObject *
frozendict_vectorcall(PyObject *type, PyObject * const*args,
size_t nargsf, PyObject *kwnames)
{
Py_ssize_t nargs = PyVectorcall_NARGS(nargsf);
if (!_PyArg_CheckPositional("frozendict", nargs, 0, 1)) {
return NULL;
}

if (nargs == 1 && kwnames == NULL
&& PyFrozenDict_CheckExact(args[0])
&& Py_Is((PyTypeObject*)type, &PyFrozenDict_Type))
{
// frozendict(frozendict) returns the same object unmodified
return Py_NewRef(args[0]);
}

PyObject *self = frozendict_new(_PyType_CAST(type), NULL, NULL);
if (self == NULL) {
return NULL;
}
Expand Down Expand Up @@ -8171,6 +8203,11 @@ PyObject*
PyFrozenDict_New(PyObject *iterable)
{
if (iterable != NULL) {
if (PyFrozenDict_CheckExact(iterable)) {
// PyFrozenDict_New(frozendict) returns the same object unmodified
return Py_NewRef(iterable);
}
Comment on lines +8206 to +8209
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is how I'd like it, but do note that PyTuple_New or PyFrozenSet_New will always return a new object.

It's probably worth noting this in documentation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, usually we don't document such optimization, but ok, I added it to PyFrozenDict_New() documentation as a "CPython implementation detail".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why document it if it is an internal implementation detail anyways? It gives the wrong sense that users can rely on this in CPython


PyObject *args = PyTuple_Pack(1, iterable);
if (args == NULL) {
return NULL;
Expand Down Expand Up @@ -8228,6 +8265,6 @@ PyTypeObject PyFrozenDict_Type = {
.tp_alloc = _PyType_AllocNoTrack,
.tp_new = frozendict_new,
.tp_free = PyObject_GC_Del,
.tp_vectorcall = dict_vectorcall,
.tp_vectorcall = frozendict_vectorcall,
.tp_version_tag = _Py_TYPE_VERSION_FROZENDICT,
};
Loading