Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e965d97

Browse files
committed
Issue #13521: dict.setdefault() now does only one lookup for the given key, making it "atomic" for many purposes.
Patch by Filip Gruszczyński.
1 parent 3bbdc8e commit e965d97

3 files changed

Lines changed: 93 additions & 42 deletions

File tree

Lib/test/test_dict.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,26 @@ def __hash__(self):
299299
x.fail = True
300300
self.assertRaises(Exc, d.setdefault, x, [])
301301

302+
def test_setdefault_atomic(self):
303+
# Issue #13521: setdefault() calls __hash__ and __eq__ only once.
304+
class Hashed(object):
305+
def __init__(self):
306+
self.hash_count = 0
307+
self.eq_count = 0
308+
def __hash__(self):
309+
self.hash_count += 1
310+
return 42
311+
def __eq__(self, other):
312+
self.eq_count += 1
313+
return id(self) == id(other)
314+
hashed1 = Hashed()
315+
y = {hashed1: 5}
316+
hashed2 = Hashed()
317+
y.setdefault(hashed2, [])
318+
self.assertEqual(hashed1.hash_count, 1)
319+
self.assertEqual(hashed2.hash_count, 1)
320+
self.assertEqual(hashed1.eq_count + hashed2.eq_count, 1)
321+
302322
def test_popitem(self):
303323
# dict.popitem()
304324
for copymode in -1, +1:

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ What's New in Python 3.2.3 release candidate 1?
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #13521: dict.setdefault() now does only one lookup for the given key,
14+
making it "atomic" for many purposes. Patch by Filip Gruszczyński.
15+
1316
- Issue #13703: oCERT-2011-003: add -R command-line option and PYTHONHASHSEED
1417
environment variable, to provide an opt-in way to protect against denial of
1518
service attacks due to hash collisions within the dict and set types. Patch

Objects/dictobject.c

Lines changed: 70 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -510,27 +510,16 @@ _PyDict_MaybeUntrack(PyObject *op)
510510
_PyObject_GC_UNTRACK(op);
511511
}
512512

513-
514513
/*
515-
Internal routine to insert a new item into the table.
516-
Used both by the internal resize routine and by the public insert routine.
517-
Eats a reference to key and one to value.
518-
Returns -1 if an error occurred, or 0 on success.
514+
Internal routine to insert a new item into the table when you have entry object.
515+
Used by insertdict.
519516
*/
520517
static int
521-
insertdict(register PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
518+
insertdict_by_entry(register PyDictObject *mp, PyObject *key, Py_hash_t hash,
519+
PyDictEntry *ep, PyObject *value)
522520
{
523521
PyObject *old_value;
524-
register PyDictEntry *ep;
525-
typedef PyDictEntry *(*lookupfunc)(PyDictObject *, PyObject *, Py_hash_t);
526522

527-
assert(mp->ma_lookup != NULL);
528-
ep = mp->ma_lookup(mp, key, hash);
529-
if (ep == NULL) {
530-
Py_DECREF(key);
531-
Py_DECREF(value);
532-
return -1;
533-
}
534523
MAINTAIN_TRACKING(mp, key, value);
535524
if (ep->me_value != NULL) {
536525
old_value = ep->me_value;
@@ -553,6 +542,28 @@ insertdict(register PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *v
553542
return 0;
554543
}
555544

545+
546+
/*
547+
Internal routine to insert a new item into the table.
548+
Used both by the internal resize routine and by the public insert routine.
549+
Eats a reference to key and one to value.
550+
Returns -1 if an error occurred, or 0 on success.
551+
*/
552+
static int
553+
insertdict(register PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
554+
{
555+
register PyDictEntry *ep;
556+
557+
assert(mp->ma_lookup != NULL);
558+
ep = mp->ma_lookup(mp, key, hash);
559+
if (ep == NULL) {
560+
Py_DECREF(key);
561+
Py_DECREF(value);
562+
return -1;
563+
}
564+
return insertdict_by_entry(mp, key, hash, ep, value);
565+
}
566+
556567
/*
557568
Internal routine used by dictresize() to insert an item which is
558569
known to be absent from the dict. This routine also assumes that
@@ -776,39 +787,26 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key)
776787
return ep->me_value;
777788
}
778789

779-
/* CAUTION: PyDict_SetItem() must guarantee that it won't resize the
780-
* dictionary if it's merely replacing the value for an existing key.
781-
* This means that it's safe to loop over a dictionary with PyDict_Next()
782-
* and occasionally replace a value -- but you can't insert new keys or
783-
* remove them.
784-
*/
785-
int
786-
PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
790+
static int
791+
dict_set_item_by_hash_or_entry(register PyObject *op, PyObject *key,
792+
Py_hash_t hash, PyDictEntry *ep, PyObject *value)
787793
{
788794
register PyDictObject *mp;
789-
register Py_hash_t hash;
790795
register Py_ssize_t n_used;
791796

792-
if (!PyDict_Check(op)) {
793-
PyErr_BadInternalCall();
794-
return -1;
795-
}
796-
assert(key);
797-
assert(value);
798797
mp = (PyDictObject *)op;
799-
if (!PyUnicode_CheckExact(key) ||
800-
(hash = ((PyUnicodeObject *) key)->hash) == -1)
801-
{
802-
hash = PyObject_Hash(key);
803-
if (hash == -1)
804-
return -1;
805-
}
806798
assert(mp->ma_fill <= mp->ma_mask); /* at least one empty slot */
807799
n_used = mp->ma_used;
808800
Py_INCREF(value);
809801
Py_INCREF(key);
810-
if (insertdict(mp, key, hash, value) != 0)
811-
return -1;
802+
if (ep == NULL) {
803+
if (insertdict(mp, key, hash, value) != 0)
804+
return -1;
805+
}
806+
else {
807+
if (insertdict_by_entry(mp, key, hash, ep, value) != 0)
808+
return -1;
809+
}
812810
/* If we added a key, we can safely resize. Otherwise just return!
813811
* If fill >= 2/3 size, adjust size. Normally, this doubles or
814812
* quaduples the size, but it's also possible for the dict to shrink
@@ -828,6 +826,36 @@ PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
828826
return dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used);
829827
}
830828

829+
/* CAUTION: PyDict_SetItem() must guarantee that it won't resize the
830+
* dictionary if it's merely replacing the value for an existing key.
831+
* This means that it's safe to loop over a dictionary with PyDict_Next()
832+
* and occasionally replace a value -- but you can't insert new keys or
833+
* remove them.
834+
*/
835+
int
836+
PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
837+
{
838+
register Py_hash_t hash;
839+
840+
if (!PyDict_Check(op)) {
841+
PyErr_BadInternalCall();
842+
return -1;
843+
}
844+
assert(key);
845+
assert(value);
846+
if (PyUnicode_CheckExact(key)) {
847+
hash = ((PyUnicodeObject *) key)->hash;
848+
if (hash == -1)
849+
hash = PyObject_Hash(key);
850+
}
851+
else {
852+
hash = PyObject_Hash(key);
853+
if (hash == -1)
854+
return -1;
855+
}
856+
return dict_set_item_by_hash_or_entry(op, key, hash, NULL, value);
857+
}
858+
831859
int
832860
PyDict_DelItem(PyObject *op, PyObject *key)
833861
{
@@ -1797,9 +1825,9 @@ dict_setdefault(register PyDictObject *mp, PyObject *args)
17971825
return NULL;
17981826
val = ep->me_value;
17991827
if (val == NULL) {
1800-
val = failobj;
1801-
if (PyDict_SetItem((PyObject*)mp, key, failobj))
1802-
val = NULL;
1828+
if (dict_set_item_by_hash_or_entry((PyObject*)mp, key, hash, ep,
1829+
failobj) == 0)
1830+
val = failobj;
18031831
}
18041832
Py_XINCREF(val);
18051833
return val;

0 commit comments

Comments
 (0)