Skip to content

Commit

Permalink
Issue python#13521: dict.setdefault() now does only one lookup for th…
Browse files Browse the repository at this point in the history
…e given key, making it "atomic" for many purposes.

Patch by Filip Gruszczyński.
  • Loading branch information
pitrou committed Feb 26, 2012
2 parents 6181b39 + e965d97 commit 70d2717
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 42 deletions.
20 changes: 20 additions & 0 deletions Lib/test/test_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,26 @@ def __hash__(self):
x.fail = True
self.assertRaises(Exc, d.setdefault, x, [])

def test_setdefault_atomic(self):
# Issue #13521: setdefault() calls __hash__ and __eq__ only once.
class Hashed(object):
def __init__(self):
self.hash_count = 0
self.eq_count = 0
def __hash__(self):
self.hash_count += 1
return 42
def __eq__(self, other):
self.eq_count += 1
return id(self) == id(other)
hashed1 = Hashed()
y = {hashed1: 5}
hashed2 = Hashed()
y.setdefault(hashed2, [])
self.assertEqual(hashed1.hash_count, 1)
self.assertEqual(hashed2.hash_count, 1)
self.assertEqual(hashed1.eq_count + hashed2.eq_count, 1)

def test_popitem(self):
# dict.popitem()
for copymode in -1, +1:
Expand Down
3 changes: 3 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ What's New in Python 3.3 Alpha 1?
Core and Builtins
-----------------

- Issue #13521: dict.setdefault() now does only one lookup for the given key,
making it "atomic" for many purposes. Patch by Filip Gruszczyński.

- PEP 409, Issue #6210: "raise X from None" is now supported as a means of
suppressing the display of the chained exception context. The chained
context still remains available as the __context__ attribute.
Expand Down
112 changes: 70 additions & 42 deletions Objects/dictobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -517,27 +517,16 @@ _PyDict_MaybeUntrack(PyObject *op)
_PyObject_GC_UNTRACK(op);
}


/*
Internal routine to insert a new item into the table.
Used both by the internal resize routine and by the public insert routine.
Eats a reference to key and one to value.
Returns -1 if an error occurred, or 0 on success.
Internal routine to insert a new item into the table when you have entry object.
Used by insertdict.
*/
static int
insertdict(register PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
insertdict_by_entry(register PyDictObject *mp, PyObject *key, Py_hash_t hash,
PyDictEntry *ep, PyObject *value)
{
PyObject *old_value;
register PyDictEntry *ep;
typedef PyDictEntry *(*lookupfunc)(PyDictObject *, PyObject *, Py_hash_t);

assert(mp->ma_lookup != NULL);
ep = mp->ma_lookup(mp, key, hash);
if (ep == NULL) {
Py_DECREF(key);
Py_DECREF(value);
return -1;
}
MAINTAIN_TRACKING(mp, key, value);
if (ep->me_value != NULL) {
old_value = ep->me_value;
Expand All @@ -560,6 +549,28 @@ insertdict(register PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *v
return 0;
}


/*
Internal routine to insert a new item into the table.
Used both by the internal resize routine and by the public insert routine.
Eats a reference to key and one to value.
Returns -1 if an error occurred, or 0 on success.
*/
static int
insertdict(register PyDictObject *mp, PyObject *key, Py_hash_t hash, PyObject *value)
{
register PyDictEntry *ep;

assert(mp->ma_lookup != NULL);
ep = mp->ma_lookup(mp, key, hash);
if (ep == NULL) {
Py_DECREF(key);
Py_DECREF(value);
return -1;
}
return insertdict_by_entry(mp, key, hash, ep, value);
}

/*
Internal routine used by dictresize() to insert an item which is
known to be absent from the dict. This routine also assumes that
Expand Down Expand Up @@ -783,39 +794,26 @@ PyDict_GetItemWithError(PyObject *op, PyObject *key)
return ep->me_value;
}

/* CAUTION: PyDict_SetItem() must guarantee that it won't resize the
* dictionary if it's merely replacing the value for an existing key.
* This means that it's safe to loop over a dictionary with PyDict_Next()
* and occasionally replace a value -- but you can't insert new keys or
* remove them.
*/
int
PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
static int
dict_set_item_by_hash_or_entry(register PyObject *op, PyObject *key,
Py_hash_t hash, PyDictEntry *ep, PyObject *value)
{
register PyDictObject *mp;
register Py_hash_t hash;
register Py_ssize_t n_used;

if (!PyDict_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
assert(key);
assert(value);
mp = (PyDictObject *)op;
if (!PyUnicode_CheckExact(key) ||
(hash = ((PyASCIIObject *) key)->hash) == -1)
{
hash = PyObject_Hash(key);
if (hash == -1)
return -1;
}
assert(mp->ma_fill <= mp->ma_mask); /* at least one empty slot */
n_used = mp->ma_used;
Py_INCREF(value);
Py_INCREF(key);
if (insertdict(mp, key, hash, value) != 0)
return -1;
if (ep == NULL) {
if (insertdict(mp, key, hash, value) != 0)
return -1;
}
else {
if (insertdict_by_entry(mp, key, hash, ep, value) != 0)
return -1;
}
/* If we added a key, we can safely resize. Otherwise just return!
* If fill >= 2/3 size, adjust size. Normally, this doubles or
* quaduples the size, but it's also possible for the dict to shrink
Expand All @@ -835,6 +833,36 @@ PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
return dictresize(mp, (mp->ma_used > 50000 ? 2 : 4) * mp->ma_used);
}

/* CAUTION: PyDict_SetItem() must guarantee that it won't resize the
* dictionary if it's merely replacing the value for an existing key.
* This means that it's safe to loop over a dictionary with PyDict_Next()
* and occasionally replace a value -- but you can't insert new keys or
* remove them.
*/
int
PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
{
register Py_hash_t hash;

if (!PyDict_Check(op)) {
PyErr_BadInternalCall();
return -1;
}
assert(key);
assert(value);
if (PyUnicode_CheckExact(key)) {
hash = ((PyASCIIObject *) key)->hash;
if (hash == -1)
hash = PyObject_Hash(key);
}
else {
hash = PyObject_Hash(key);
if (hash == -1)
return -1;
}
return dict_set_item_by_hash_or_entry(op, key, hash, NULL, value);
}

int
PyDict_DelItem(PyObject *op, PyObject *key)
{
Expand Down Expand Up @@ -1803,9 +1831,9 @@ dict_setdefault(register PyDictObject *mp, PyObject *args)
return NULL;
val = ep->me_value;
if (val == NULL) {
val = failobj;
if (PyDict_SetItem((PyObject*)mp, key, failobj))
val = NULL;
if (dict_set_item_by_hash_or_entry((PyObject*)mp, key, hash, ep,
failobj) == 0)
val = failobj;
}
Py_XINCREF(val);
return val;
Expand Down

0 comments on commit 70d2717

Please sign in to comment.