diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 6f798f02e17899..a8f693f4879025 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -5590,8 +5590,8 @@ can be used interchangeably to index the same dictionary entry. .. seealso:: - :class:`types.MappingProxyType` can be used to create a read-only view - of a :class:`dict`. + :class:`frozendict` and :class:`types.MappingProxyType` can be used to + create a read-only view of a :class:`dict`. .. _thread-safety-dict: diff --git a/Lib/pydoc_data/topics.py b/Lib/pydoc_data/topics.py index 32cf9a995bae3d..dc09c5fd47affe 100644 --- a/Lib/pydoc_data/topics.py +++ b/Lib/pydoc_data/topics.py @@ -3676,8 +3676,8 @@ def f() -> annotation: ... * a class that inherits from any of the above - The standard library classes "dict" and "types.MappingProxyType" - are mappings. + The standard library classes "dict", "frozendict" + and "types.MappingProxyType" are mappings. [4] A string literal appearing as the first statement in the function body is transformed into the function’s "__doc__" attribute and @@ -13620,8 +13620,8 @@ class dict(iterable, /, **kwargs) See also: - "types.MappingProxyType" can be used to create a read-only view of a - "dict". + "frozendict" and "types.MappingProxyType" can be used to create a read-only + view of a "dict". Dictionary view objects diff --git a/Lib/test/pickletester.py b/Lib/test/pickletester.py index 5f627f047ea319..c4460c2e44d578 100644 --- a/Lib/test/pickletester.py +++ b/Lib/test/pickletester.py @@ -3167,6 +3167,7 @@ def test_builtin_types(self): 'bytes': (3, 0), 'BuiltinImporter': (3, 3), 'str': (3, 4), # not interoperable with Python < 3.4 + 'frozendict': (3, 15), } for t in builtins.__dict__.values(): if isinstance(t, type) and not issubclass(t, BaseException): diff --git a/Lib/test/test_dict.py b/Lib/test/test_dict.py index 1db3559a012fd3..21f8bb11071c90 100644 --- a/Lib/test/test_dict.py +++ b/Lib/test/test_dict.py @@ -1767,6 +1767,9 @@ def test_update(self): self.assertEqual(copy, frozendict({'x': 1})) def test_repr(self): + d = frozendict() + self.assertEqual(repr(d), "frozendict()") + d = frozendict(x=1, y=2) self.assertEqual(repr(d), "frozendict({'x': 1, 'y': 2})") @@ -1775,6 +1778,15 @@ class MyFrozenDict(frozendict): d = MyFrozenDict(x=1, y=2) self.assertEqual(repr(d), "MyFrozenDict({'x': 1, 'y': 2})") + def test_hash(self): + # hash() doesn't rely on the items order + self.assertEqual(hash(frozendict(x=1, y=2)), + hash(frozendict(y=2, x=1))) + + fd = frozendict(x=[1], y=[2]) + with self.assertRaisesRegex(TypeError, "unhashable type: 'list'"): + hash(fd) + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_genericalias.py b/Lib/test/test_genericalias.py index 2b9cee6433b5b8..a5969b7a47d948 100644 --- a/Lib/test/test_genericalias.py +++ b/Lib/test/test_genericalias.py @@ -101,7 +101,8 @@ class BaseTest(unittest.TestCase): """Test basics.""" - generic_types = [type, tuple, list, dict, set, frozenset, enumerate, memoryview, + generic_types = [type, tuple, list, dict, frozendict, + set, frozenset, enumerate, memoryview, slice, defaultdict, deque, SequenceMatcher, diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-22-27-11.gh-issue-141510.-4yYsf.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-22-27-11.gh-issue-141510.-4yYsf.rst new file mode 100644 index 00000000000000..b031fb3c75dea7 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-22-27-11.gh-issue-141510.-4yYsf.rst @@ -0,0 +1,2 @@ +Optimize :meth:`!frozendict.fromkeys` to avoid unnecessary thread-safety operations +in frozendict cases. Patch by Donghee Na. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 62abb793d002e0..0959e2c78a3289 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -2671,10 +2671,8 @@ _PyDict_LoadBuiltinsFromGlobals(PyObject *globals) /* Consumes references to key and value */ static int -setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value) +anydict_setitem_take2(PyDictObject *mp, PyObject *key, PyObject *value) { - ASSERT_DICT_LOCKED(mp); - assert(key); assert(value); assert(PyAnyDict_Check(mp)); @@ -2693,6 +2691,14 @@ setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value) return insertdict(mp, key, hash, value); } +/* Consumes references to key and value */ +static int +setitem_take2_lock_held(PyDictObject *mp, PyObject *key, PyObject *value) +{ + ASSERT_DICT_LOCKED(mp); + return anydict_setitem_take2(mp, key, value); +} + int _PyDict_SetItem_Take2(PyDictObject *mp, PyObject *key, PyObject *value) { @@ -3284,8 +3290,8 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) return NULL; - if (PyAnyDict_CheckExact(d)) { - if (PyAnyDict_CheckExact(iterable)) { + if (PyDict_CheckExact(d)) { + if (PyDict_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; Py_BEGIN_CRITICAL_SECTION2(d, iterable); @@ -3293,6 +3299,14 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) Py_END_CRITICAL_SECTION2(); return d; } + else if (PyFrozenDict_CheckExact(iterable)) { + PyDictObject *mp = (PyDictObject *)d; + + Py_BEGIN_CRITICAL_SECTION(d); + d = (PyObject *)dict_dict_fromkeys(mp, iterable, value); + Py_END_CRITICAL_SECTION(); + return d; + } else if (PyAnySet_CheckExact(iterable)) { PyDictObject *mp = (PyDictObject *)d; @@ -3302,6 +3316,29 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) return d; } } + else if (PyFrozenDict_CheckExact(d)) { + if (PyDict_CheckExact(iterable)) { + PyDictObject *mp = (PyDictObject *)d; + + Py_BEGIN_CRITICAL_SECTION(iterable); + d = (PyObject *)dict_dict_fromkeys(mp, iterable, value); + Py_END_CRITICAL_SECTION(); + return d; + } + else if (PyFrozenDict_CheckExact(iterable)) { + PyDictObject *mp = (PyDictObject *)d; + d = (PyObject *)dict_dict_fromkeys(mp, iterable, value); + return d; + } + else if (PyAnySet_CheckExact(iterable)) { + PyDictObject *mp = (PyDictObject *)d; + + Py_BEGIN_CRITICAL_SECTION(iterable); + d = (PyObject *)dict_set_fromkeys(mp, iterable, value); + Py_END_CRITICAL_SECTION(); + return d; + } + } it = PyObject_GetIter(iterable); if (it == NULL){ @@ -3309,7 +3346,7 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) return NULL; } - if (PyAnyDict_CheckExact(d)) { + if (PyDict_CheckExact(d)) { Py_BEGIN_CRITICAL_SECTION(d); while ((key = PyIter_Next(it)) != NULL) { status = setitem_lock_held((PyDictObject *)d, key, value); @@ -3321,7 +3358,19 @@ _PyDict_FromKeys(PyObject *cls, PyObject *iterable, PyObject *value) } dict_iter_exit:; Py_END_CRITICAL_SECTION(); - } else { + } + else if (PyFrozenDict_CheckExact(d)) { + while ((key = PyIter_Next(it)) != NULL) { + // anydict_setitem_take2 consumes a reference to key + status = anydict_setitem_take2((PyDictObject *)d, + key, Py_NewRef(value)); + if (status < 0) { + assert(PyErr_Occurred()); + goto Fail; + } + } + } + else { while ((key = PyIter_Next(it)) != NULL) { status = PyObject_SetItem(d, key, value); Py_DECREF(key); @@ -7868,6 +7917,11 @@ static PyMethodDef frozendict_methods[] = { static PyObject * frozendict_repr(PyObject *self) { + PyDictObject *mp = _PyAnyDict_CAST(self); + if (mp->ma_used == 0) { + return PyUnicode_FromFormat("%s()", Py_TYPE(self)->tp_name); + } + PyObject *repr = anydict_repr_impl(self); if (repr == NULL) { return NULL; @@ -7881,33 +7935,55 @@ frozendict_repr(PyObject *self) return res; } +static Py_uhash_t +_shuffle_bits(Py_uhash_t h) +{ + return ((h ^ 89869747UL) ^ (h << 16)) * 3644798167UL; +} + +// Code copied from frozenset_hash() static Py_hash_t frozendict_hash(PyObject *op) { PyFrozenDictObject *self = _PyFrozenDictObject_CAST(op); - Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ma_hash); - if (hash != -1) { - return hash; + Py_hash_t shash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ma_hash); + if (shash != -1) { + return shash; } - PyObject *items = _PyDictView_New(op, &PyDictItems_Type); - if (items == NULL) { - return -1; - } - PyObject *frozenset = PyFrozenSet_New(items); - Py_DECREF(items); - if (frozenset == NULL) { - return -1; + PyDictObject *mp = _PyAnyDict_CAST(op); + Py_uhash_t hash = 0; + + PyObject *key, *value; // borrowed refs + Py_ssize_t pos = 0; + while (PyDict_Next(op, &pos, &key, &value)) { + Py_hash_t key_hash = PyObject_Hash(key); + if (key_hash == -1) { + return -1; + } + hash ^= _shuffle_bits(key_hash); + + Py_hash_t value_hash = PyObject_Hash(value); + if (value_hash == -1) { + return -1; + } + hash ^= _shuffle_bits(value_hash); } - hash = PyObject_Hash(frozenset); - Py_DECREF(frozenset); - if (hash == -1) { - return -1; + /* Factor in the number of active entries */ + hash ^= ((Py_uhash_t)mp->ma_used + 1) * 1927868237UL; + + /* Disperse patterns arising in nested frozendicts */ + hash ^= (hash >> 11) ^ (hash >> 25); + hash = hash * 69069U + 907133923UL; + + /* -1 is reserved as an error code */ + if (hash == (Py_uhash_t)-1) { + hash = 590923713UL; } - FT_ATOMIC_STORE_SSIZE_RELAXED(self->ma_hash, hash); - return hash; + FT_ATOMIC_STORE_SSIZE_RELAXED(self->ma_hash, (Py_hash_t)hash); + return (Py_hash_t)hash; } diff --git a/Objects/setobject.c b/Objects/setobject.c index 5d4d1812282eed..f8713bf3d1a432 100644 --- a/Objects/setobject.c +++ b/Objects/setobject.c @@ -964,7 +964,10 @@ _shuffle_bits(Py_uhash_t h) This hash algorithm can be used on either a frozenset or a set. When it is used on a set, it computes the hash value of the equivalent - frozenset without creating a new frozenset object. */ + frozenset without creating a new frozenset object. + + If you update this code, update also frozendict_hash() which copied this + code. */ static Py_hash_t frozenset_hash_impl(PyObject *self)