Skip to content

Commit 03d0d11

Browse files
committed
defaultdict: remove __missing__, add __getitem__
1 parent 0985fd1 commit 03d0d11

9 files changed

Lines changed: 102 additions & 104 deletions

File tree

Doc/library/collections.rst

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -740,43 +740,38 @@ stack manipulations such as ``dup``, ``drop``, ``swap``, ``over``, ``pick``,
740740
arguments.
741741

742742

743-
:class:`defaultdict` objects support the following method in addition to the
744-
standard :class:`dict` operations:
743+
:class:`defaultdict` overrides the following method:
745744

746-
.. method:: __missing__(key, /)
745+
.. method:: __getitem__(key, /)
747746

748-
If the :attr:`default_factory` attribute is ``None``, this raises a
749-
:exc:`KeyError` exception with the *key* as argument.
747+
Return ``self[key]``. If the item doesn't exist, it is automatically created.
748+
The value is generated by calling either the :meth:`~object.__missing__` method
749+
(if it exists) or the :attr:`default_factory` attribute (if it isn't None). If
750+
neither can be called, a :exc:`KeyError` is raised.
750751

751-
If :attr:`default_factory` is not ``None``, it is called without arguments
752-
to provide a default value for the given *key*, this value is inserted in
753-
the dictionary for the *key*, and returned.
754-
755-
If calling :attr:`default_factory` raises an exception this exception is
756-
propagated unchanged.
757-
758-
This method is called by the :meth:`~object.__getitem__` method of the
759-
:class:`dict` class when the requested key is not found; whatever it
760-
returns or raises is then returned or raised by :meth:`~object.__getitem__`.
761-
762-
Note that :meth:`__missing__` is *not* called for any operations besides
763-
:meth:`~object.__getitem__`. This means that :meth:`~dict.get` will, like
764-
normal dictionaries, return ``None`` as a default rather than using
765-
:attr:`default_factory`.
752+
When :term:`free threading` is enabled, the defaultdict is locked while the
753+
key is being looked up and the default value is being generated.
766754

767755

768756
:class:`defaultdict` objects support the following instance variable:
769757

770-
771758
.. attribute:: default_factory
772759

773-
This attribute is used by the :meth:`~defaultdict.__missing__` method;
774-
it is initialized from the first argument to the constructor, if present,
775-
or to ``None``, if absent.
760+
This attribute is called by the :meth:`defaultdict.__getitem__` method
761+
if the requested key isn't in the dictionary. It must be either a
762+
callable that takes no arguments, or :const:`None`.
763+
776764

777765
.. versionchanged:: 3.9
778-
Added merge (``|``) and update (``|=``) operators, specified in
779-
:pep:`584`.
766+
Added merge (``|``) and update (``|=``) operators, specified in
767+
:pep:`584`.
768+
769+
.. versionchanged:: 3.15
770+
The built-in :meth:`defaultdict.__missing__` method no longer exists. A
771+
custom :meth:`~object.__missing__` method should no longer insert the
772+
generated value into the dictionary, as this is done by the new
773+
:meth:`__getitem__` method. defaultdict is now safe to use with
774+
:term:`free threading`.
780775

781776

782777
:class:`defaultdict` Examples

Include/internal/pycore_dict.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ PyAPI_FUNC(Py_ssize_t) _Py_dict_lookup(PyDictObject *mp, PyObject *key, Py_hash_
123123
extern Py_ssize_t _Py_dict_lookup_threadsafe(PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject **value_addr);
124124
extern Py_ssize_t _Py_dict_lookup_threadsafe_stackref(PyDictObject *mp, PyObject *key, Py_hash_t hash, _PyStackRef *value_addr);
125125

126+
extern void _Py_dict_unhashable_type(PyObject *op, PyObject *key);
127+
126128
extern int _PyDict_GetMethodStackRef(PyDictObject *dict, PyObject *name, _PyStackRef *method);
127129

128130
// Exported for external JIT support

Lib/importlib/metadata/__init__.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import sys
2323
import textwrap
2424
import types
25+
from collections import defaultdict
2526
from collections.abc import Iterable, Mapping
2627
from contextlib import suppress
2728
from importlib import import_module
@@ -30,7 +31,7 @@
3031
from typing import Any
3132

3233
from . import _meta
33-
from ._collections import FreezableDefaultDict, Pair
34+
from ._collections import Pair
3435
from ._context import ExceptionTrap
3536
from ._functools import method_cache, noop, pass_none, passthrough
3637
from ._itertools import always_iterable, bucket, unique_everseen
@@ -889,8 +890,8 @@ def __init__(self, path: FastPath):
889890

890891
base = os.path.basename(path.root).lower()
891892
base_is_egg = base.endswith(".egg")
892-
self.infos = FreezableDefaultDict(list)
893-
self.eggs = FreezableDefaultDict(list)
893+
self.infos = defaultdict(list)
894+
self.eggs = defaultdict(list)
894895

895896
for child in path.children():
896897
low = child.lower()
@@ -904,20 +905,17 @@ def __init__(self, path: FastPath):
904905
legacy_normalized = Prepared.legacy_normalize(name)
905906
self.eggs[legacy_normalized].append(path.joinpath(child))
906907

907-
self.infos.freeze()
908-
self.eggs.freeze()
909-
910908
def search(self, prepared: Prepared):
911909
"""
912910
Yield all infos and eggs matching the Prepared query.
913911
"""
914912
infos = (
915-
self.infos[prepared.normalized]
913+
self.infos.get(prepared.normalized, ())
916914
if prepared
917915
else itertools.chain.from_iterable(self.infos.values())
918916
)
919917
eggs = (
920-
self.eggs[prepared.legacy_normalized]
918+
self.eggs.get(prepared.legacy_normalized, ())
921919
if prepared
922920
else itertools.chain.from_iterable(self.eggs.values())
923921
)

Lib/importlib/metadata/_collections.py

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,6 @@
1-
import collections
21
import typing
32

43

5-
# from jaraco.collections 3.3
6-
class FreezableDefaultDict(collections.defaultdict):
7-
"""
8-
Often it is desirable to prevent the mutation of
9-
a default dict after its initial construction, such
10-
as to prevent mutation during iteration.
11-
12-
>>> dd = FreezableDefaultDict(list)
13-
>>> dd[0].append('1')
14-
>>> dd.freeze()
15-
>>> dd[1]
16-
[]
17-
>>> len(dd)
18-
1
19-
"""
20-
21-
def __missing__(self, key):
22-
return getattr(self, '_frozen', super().__missing__)(key)
23-
24-
def freeze(self):
25-
self._frozen = lambda key: self.default_factory()
26-
27-
284
class Pair(typing.NamedTuple):
295
name: str
306
value: str

Lib/pydoc_data/topics.py

Lines changed: 1 addition & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Lib/test/test_defaultdict.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ class TestDefaultDict(unittest.TestCase):
1414
def test_basic(self):
1515
d1 = defaultdict()
1616
self.assertEqual(d1.default_factory, None)
17+
self.assertRaises(KeyError, d1.__getitem__, 42)
1718
d1.default_factory = list
1819
d1[12].append(42)
1920
self.assertEqual(d1, {12: [42]})
@@ -48,14 +49,15 @@ def test_basic(self):
4849
self.assertRaises(TypeError, defaultdict, 1)
4950

5051
def test_missing(self):
51-
d1 = defaultdict()
52-
self.assertRaises(KeyError, d1.__missing__, 42)
53-
d1.default_factory = list
54-
v1 = d1.__missing__(42)
55-
self.assertEqual(v1, [])
56-
v2 = d1.__missing__(42)
57-
self.assertEqual(v2, [])
58-
self.assertIsNot(v2, v1)
52+
# Check that __missing__ is called when it exists
53+
class A(defaultdict):
54+
def __missing__(self, key):
55+
return []
56+
d1 = A()
57+
self.assertEqual(d1.__missing__(1), [])
58+
# Check that default_factory isn't called when __missing__ exists
59+
d1.default_factory = dict
60+
self.assertEqual(d1.__missing__(2), [])
5961

6062
def test_repr(self):
6163
d1 = defaultdict()

Modules/_collectionsmodule.c

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,31 +2222,56 @@ typedef struct {
22222222

22232223
static PyType_Spec defdict_spec;
22242224

2225-
PyDoc_STRVAR(defdict_missing_doc,
2226-
"__missing__(key) # Called by __getitem__ for missing key; pseudo-code:\n\
2227-
if self.default_factory is None: raise KeyError((key,))\n\
2228-
self[key] = value = self.default_factory()\n\
2229-
return value\n\
2225+
PyDoc_STRVAR(defdict_getitem_doc,
2226+
"__getitem__($self, key, /)\n--\n\n\
2227+
Return self[key]. If the item doesn't exist, it is automatically created.\n\
2228+
The value is generated by calling either the __missing__ method (if it exists)\n\
2229+
or the default_factory attribute (if it isn't None). If neither can be called,\n\
2230+
a KeyError is raised.\
22302231
");
22312232

22322233
static PyObject *
2233-
defdict_missing(PyObject *op, PyObject *key)
2234+
defdict_subscript(PyObject *op, PyObject *key)
22342235
{
2235-
defdictobject *dd = defdictobject_CAST(op);
2236-
PyObject *factory = dd->default_factory;
2236+
PyDictObject *mp = (PyDictObject *)op;
2237+
Py_ssize_t ix;
2238+
Py_hash_t hash;
22372239
PyObject *value;
2238-
if (factory == NULL || factory == Py_None) {
2239-
/* XXX Call dict.__missing__(key) */
2240-
_PyErr_SetKeyError(key);
2240+
2241+
hash = _PyObject_HashFast(key);
2242+
if (hash == -1) {
2243+
_Py_dict_unhashable_type(op, key);
22412244
return NULL;
22422245
}
2243-
value = _PyObject_CallNoArgs(factory);
2244-
if (value == NULL)
2245-
return value;
2246-
if (PyObject_SetItem(op, key, value) < 0) {
2247-
Py_DECREF(value);
2248-
return NULL;
2246+
Py_BEGIN_CRITICAL_SECTION(op);
2247+
ix = _Py_dict_lookup(mp, key, hash, &value);
2248+
if (value != NULL) {
2249+
Py_INCREF(value);
2250+
} else if (ix != DKIX_ERROR) {
2251+
/* Try to call self.__missing__(key) */
2252+
PyObject *missing;
2253+
int ret = PyObject_GetOptionalAttr(op, &_Py_ID(__missing__), &missing);
2254+
if (ret == 1) {
2255+
value = PyObject_CallOneArg(missing, key);
2256+
Py_DECREF(missing);
2257+
} else if (ret == 0) {
2258+
/* Try to call self.default_factory() */
2259+
PyObject *factory = defdictobject_CAST(op)->default_factory;
2260+
if (factory != NULL && factory != Py_None) {
2261+
value = _PyObject_CallNoArgs(factory);
2262+
} else {
2263+
_PyErr_SetKeyError(key);
2264+
}
2265+
}
2266+
/* Try to insert the new value in the dict */
2267+
if (value != NULL) {
2268+
ret = _PyDict_SetItem_KnownHash_LockHeld(mp, Py_NewRef(key),
2269+
Py_NewRef(value), hash);
2270+
if (ret < 0)
2271+
value = NULL;
2272+
}
22492273
}
2274+
Py_END_CRITICAL_SECTION();
22502275
return value;
22512276
}
22522277

@@ -2327,8 +2352,8 @@ defdict_reduce(PyObject *op, PyObject *Py_UNUSED(dummy))
23272352
}
23282353

23292354
static PyMethodDef defdict_methods[] = {
2330-
{"__missing__", defdict_missing, METH_O,
2331-
defdict_missing_doc},
2355+
{"__getitem__", defdict_subscript, METH_O|METH_COEXIST,
2356+
defdict_getitem_doc},
23322357
{"copy", defdict_copy, METH_NOARGS,
23332358
defdict_copy_doc},
23342359
{"__copy__", defdict_copy, METH_NOARGS,
@@ -2343,7 +2368,7 @@ static PyMethodDef defdict_methods[] = {
23432368
static PyMemberDef defdict_members[] = {
23442369
{"default_factory", _Py_T_OBJECT,
23452370
offsetof(defdictobject, default_factory), 0,
2346-
PyDoc_STR("Factory for default value called by __missing__().")},
2371+
PyDoc_STR("Factory for default value, called by __getitem__().")},
23472372
{NULL}
23482373
};
23492374

@@ -2507,6 +2532,7 @@ static PyType_Slot defdict_slots[] = {
25072532
{Py_tp_init, defdict_init},
25082533
{Py_tp_alloc, PyType_GenericAlloc},
25092534
{Py_tp_free, PyObject_GC_Del},
2535+
{Py_mp_subscript, defdict_subscript},
25102536
{0, NULL},
25112537
};
25122538

0 commit comments

Comments
 (0)